Skip to content

feat: add token metadata proxy endpoint#1265

Draft
0xApotheosis wants to merge 5 commits intodevelopfrom
feat/token-metadata-proxy-endpoint
Draft

feat: add token metadata proxy endpoint#1265
0xApotheosis wants to merge 5 commits intodevelopfrom
feat/token-metadata-proxy-endpoint

Conversation

@0xApotheosis
Copy link
Member

@0xApotheosis 0xApotheosis commented Feb 26, 2026

Summary

  • Add /api/v1/tokens/metadata pass-through proxy endpoint for token metadata lookups via Alchemy
  • Supports EVM chains (Ethereum, Optimism, Polygon, Base, Arbitrum) via alchemy_getTokenMetadata and Solana mainnet via getAsset
  • Accepts chainId + tokenAddress query params, injects the Alchemy API key, and returns a flat normalized response: { chainId, tokenAddress, name, symbol, decimals, logo }
  • Responses include Cache-Control: public, max-age=86400 header
  • Degrades gracefully with 503 if ALCHEMY_API_KEY is not configured
  • Add ALCHEMY_API_KEY to sample.env

Testing

  • yarn eslint node/proxy/api/src/tokenMetadata.ts node/proxy/api/src/app.ts — passes
  • Manual local endpoint smoke checks (/health, /api/v1/tokens/metadata)

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Adds a new GET /api/v1/tokens/metadata endpoint with IP rate limiting, chain-specific routing for EVM and Solana (via Alchemy RPC), query validation, metadata retrieval, and comprehensive error mapping and responses.

Changes

Cohort / File(s) Summary
API Route Registration
node/proxy/api/src/app.ts
Imported TokenMetadata, set app.set('trust proxy', 1), and registered GET /api/v1/tokens/metadata route using TokenMetadata.handler(...).
Token Metadata Handler
node/proxy/api/src/tokenMetadata.ts
New exported TokenMetadata class implementing a rate-limited handler: validates chainId and tokenAddress, routes to EVM (Alchemy RPC) or Solana (Alchemy Solana RPC) fetchers, normalizes responses, maps Axios/upstream errors, and returns 200/404/422/400/500 as appropriate. Includes network mapping and Solana address validation.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as TokenMetadata Handler
    participant RateLimit as Rate Limiter
    participant Router as Chain Router
    participant EVM as Alchemy EVM RPC
    participant Solana as Alchemy Solana RPC

    Client->>Handler: GET /api/v1/tokens/metadata?chainId=X&tokenAddress=Y
    Handler->>Handler: Validate query params
    alt Missing params
        Handler->>Client: 400 Bad Request
    else Valid params
        Handler->>RateLimit: Check IP rate limit
        alt Rate limit exceeded
            RateLimit->>Handler: Limit exceeded
            Handler->>Client: 429 Too Many Requests
        else Within limit
            RateLimit->>Handler: Allowed
            Handler->>Router: Route by chainId
            alt EVM chain (eip155:*)
                Router->>Handler: EVM routing
                Handler->>Handler: Validate address
                Handler->>EVM: alchemy_getTokenMetadata RPC
                EVM->>Handler: Metadata response
            else Solana chain (solana:*)
                Router->>Handler: Solana routing
                Handler->>Handler: Validate mint address
                Handler->>Solana: getAsset RPC call
                Solana->>Handler: Asset metadata response
            else Unsupported chain
                Handler->>Client: 422 Unprocessable Entity
            end
            alt Metadata found
                Handler->>Client: 200 OK + Token metadata
            else Not found
                Handler->>Client: 404 Not Found
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped along the proxy trail today,
New metadata blooms for EVM and Solana's way,
Rate limits guard the garden gate,
Logos, names, decimals—neatly aggregate,
A tiny rabbit cheers the API bouquet!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: add token metadata proxy endpoint' directly and concisely summarizes the main change—introduction of a new token metadata proxy endpoint for both EVM and Solana chains.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/token-metadata-proxy-endpoint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0xApotheosis 0xApotheosis marked this pull request as ready for review February 26, 2026 04:49
@0xApotheosis 0xApotheosis requested a review from a team as a code owner February 26, 2026 04:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 82-101: The in-memory requestsByIp map in isRateLimited can grow
unbounded because expired buckets aren't removed; update isRateLimited (or add a
helper it calls) to evict stale entries by iterating requestsByIp and deleting
any entry whose resetAt <= now before inserting or counting a request (use the
same now timestamp and retain existing behavior for count/reset logic),
referencing requestsByIp, isRateLimited, windowMs, and maxRequestsPerWindow so
stale IP buckets are removed and memory stays bounded.
- Line 84: The rate-limiting using req.ip in tokenMetadata.ts will see the
proxy's IP unless Express is configured with trust proxy; update the app
initialization to call app.set('trust proxy', <appropriate value>) (for example
'loopback', 1, or a custom function) in the module that creates the Express app
(where the Express instance is named app) so req.ip returns the client IP behind
your ingress/proxy and per-IP rate limits work correctly.
- Line 23: Replace the current regex-only validator in isValidSolanaAddress with
a lightweight Base58 decoder that decodes the address and verifies the resulting
byte array is exactly 32 bytes (the size of a Solana public key); keep the
function name isValidSolanaAddress and ensure it returns false on decode errors
or wrong length, avoiding adding `@solana/web3.js` as a dependency. This change
will reject malformed Base58 strings that happen to match the regex and yield a
proper 422 validation failure before calling the RPC.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce8ce4f and bd02d80.

📒 Files selected for processing (2)
  • node/proxy/api/src/app.ts
  • node/proxy/api/src/tokenMetadata.ts

@0xApotheosis
Copy link
Member Author

0xApotheosis commented Feb 26, 2026

Addressed the CodeRabbit feedback in this branch:

  • Replaced regex-only Solana validation with lightweight Base58 decoding plus strict 32-byte public key length verification in node/proxy/api/src/tokenMetadata.ts. This now rejects malformed Base58 payloads earlier and returns 422 validation failures before reaching Alchemy RPC.
  • Added stale bucket eviction to the in-memory per-IP rate limiter in node/proxy/api/src/tokenMetadata.ts so expired entries are periodically deleted and the map does not grow unbounded under high-cardinality traffic.
  • Enabled proxy-aware IP handling with app.set('trust proxy', 1) in node/proxy/api/src/app.ts so req.ip reflects client IP behind a single ingress/proxy and per-IP limiting works as intended.

Validation:

  • yarn eslint node/proxy/api/src/tokenMetadata.ts node/proxy/api/src/app.ts passes.
  • Full workspace build still reports unrelated pre-existing websocket/common-api TypeScript errors outside this PR.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
node/proxy/api/src/tokenMetadata.ts (1)

123-154: Rate limiter is process-local; consider shared enforcement for multi-replica deploys.

This implementation works per instance, but limits can be bypassed (or uneven) behind load balancing. Consider Redis or ingress-level rate limiting for consistent global behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/proxy/api/src/tokenMetadata.ts` around lines 123 - 154, The current
isRateLimited method (and its in-memory requestsByIp map, nextCleanupAt,
cleanupIntervalMs, windowMs, maxRequestsPerWindow) enforces limits only per
process which fails for multi-replica deployments; replace the in-process logic
with a shared store or external rate-limiter: migrate the counter/reset logic to
a Redis-backed implementation (e.g., use INCR with EXPIRE or a Lua script for
atomic sliding-window semantics) or delegate to ingress/edge rate limiting,
updating isRateLimited to call the shared Redis helper (or external API) instead
of reading/writing requestsByIp and remove the cleanup loop. Ensure keys are
derived from req.ip (or X-Forwarded-For) to maintain the same behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 181-191: The code currently accepts any chainId starting with
"solana:" but always fetches mainnet metadata; update the chainId validation in
the block that handles chainId.startsWith('solana:') to explicitly allow only
supported Solana identifiers (e.g. 'solana:mainnet' or whatever supported list
you maintain) before calling isValidSolanaAddress or getSolanaTokenMetadata, and
if the chainId is unsupported call sendValidationError to return a 422-style
validation error; modify the logic around isValidSolanaAddress,
getSolanaTokenMetadata, and sendValidationError to first check the exact chainId
membership and only proceed to address validation and getSolanaTokenMetadata for
supported chain IDs.

---

Nitpick comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 123-154: The current isRateLimited method (and its in-memory
requestsByIp map, nextCleanupAt, cleanupIntervalMs, windowMs,
maxRequestsPerWindow) enforces limits only per process which fails for
multi-replica deployments; replace the in-process logic with a shared store or
external rate-limiter: migrate the counter/reset logic to a Redis-backed
implementation (e.g., use INCR with EXPIRE or a Lua script for atomic
sliding-window semantics) or delegate to ingress/edge rate limiting, updating
isRateLimited to call the shared Redis helper (or external API) instead of
reading/writing requestsByIp and remove the cleanup loop. Ensure keys are
derived from req.ip (or X-Forwarded-For) to maintain the same behavior.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd02d80 and 70c0f9d.

📒 Files selected for processing (2)
  • node/proxy/api/src/app.ts
  • node/proxy/api/src/tokenMetadata.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • node/proxy/api/src/app.ts

Comment on lines +181 to +191
if (chainId.startsWith('solana:')) {
if (!isValidSolanaAddress(tokenAddress)) {
sendValidationError(res, {
tokenAddress: 'Invalid Solana mint address',
})
return null
}

const metadata = await this.getSolanaTokenMetadata(chainId, tokenAddress)
return metadata
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject unsupported Solana chain IDs explicitly.

Any solana:* value is currently accepted, but metadata is always fetched from Solana mainnet. This can return incorrect data for unsupported Solana references instead of a 422 validation error.

🔧 Suggested fix
+const SUPPORTED_SOLANA_CHAIN_IDS = new Set<string>([
+  // Add the Solana CAIP-2 chain IDs this endpoint actually supports.
+  // e.g. 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'
+])
+
   if (chainId.startsWith('solana:')) {
+    if (!SUPPORTED_SOLANA_CHAIN_IDS.has(chainId)) {
+      sendValidationError(res, {
+        chainId: `Unsupported chainId: ${chainId}`,
+      })
+      return null
+    }
+
     if (!isValidSolanaAddress(tokenAddress)) {
       sendValidationError(res, {
         tokenAddress: 'Invalid Solana mint address',
       })
       return null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/proxy/api/src/tokenMetadata.ts` around lines 181 - 191, The code
currently accepts any chainId starting with "solana:" but always fetches mainnet
metadata; update the chainId validation in the block that handles
chainId.startsWith('solana:') to explicitly allow only supported Solana
identifiers (e.g. 'solana:mainnet' or whatever supported list you maintain)
before calling isValidSolanaAddress or getSolanaTokenMetadata, and if the
chainId is unsupported call sendValidationError to return a 422-style validation
error; modify the logic around isValidSolanaAddress, getSolanaTokenMetadata, and
sendValidationError to first check the exact chainId membership and only proceed
to address validation and getSolanaTokenMetadata for supported chain IDs.

@0xApotheosis 0xApotheosis marked this pull request as draft February 26, 2026 05:25
0xApotheosis and others added 3 commits February 26, 2026 17:00
Remove hand-rolled Base58 decoder, custom IP rate limiter, viem address
validation, error sanitization helpers, and response envelope. The proxy
now receives chainId + tokenAddress, injects the Alchemy API key,
forwards to the correct endpoint, normalizes the Solana response, and
returns a flat { chainId, tokenAddress, name, symbol, decimals, logo }.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warn instead of crashing when ALCHEMY_API_KEY is missing, returning 503
from the handler. Add Cache-Control: public, max-age=86400 to successful
responses since token metadata is essentially immutable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Solana URL follows the same ALCHEMY_API_KEY pattern as EVM chains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant