Skip to content

feat: add rate limiting to public API#11986

Open
0xApotheosis wants to merge 5 commits intodevelopfrom
feat/public-api-rate-limiting
Open

feat: add rate limiting to public API#11986
0xApotheosis wants to merge 5 commits intodevelopfrom
feat/public-api-rate-limiting

Conversation

@0xApotheosis
Copy link
Member

@0xApotheosis 0xApotheosis commented Feb 23, 2026

Description

Add tiered rate limiting to the public API using express-rate-limit to protect expensive swap endpoints from abuse. A single client hitting /v1/swap/rates fans out to 10+ external swapper protocols — without rate limiting, one abusive IP can exhaust external API quotas and degrade service for everyone.

Four tiers (all configurable via env vars):

Tier Endpoints Default Limit Env Var
Global All routes 300/min per IP RATE_LIMIT_GLOBAL_MAX
Data /v1/chains/*, /v1/assets/* 120/min per IP RATE_LIMIT_DATA_MAX
Swap Rates GET /v1/swap/rates 60/min per IP RATE_LIMIT_SWAP_RATES_MAX
Swap Quote POST /v1/swap/quote 45/min per IP RATE_LIMIT_SWAP_QUOTE_MAX

Widget compatibility: The swap widget polls rates every 15s (4 req/min), well under the 60/min limit. 429 responses return immediately so the next poll cycle succeeds normally.

Key details:

  • trust proxy set to 1 for correct IP detection behind Railway's reverse proxy
  • draft-7 standard headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) on every response
  • 429 responses return JSON matching the existing ErrorResponse format with code RATE_LIMIT_EXCEEDED
  • OpenAPI docs updated with 429 response schemas for all rate-limited endpoints

Bonus fix — resilient asset loading: getBaseAsset() throws via assertUnreachable for any chain not yet in KnownChainIds. When new chains are added to the asset generation pipeline before being added to KnownChainIds, this crashed the entire server on startup. Now assets.ts catches the error and includes unknown-chain assets with their existing data (without enrichment), so new chains no longer break the public API.

Issue (if applicable)

Closes #11676

Risk

Low risk. This only affects the public-api package (deployed independently on Railway). No changes to the main web app, swap widget, or any on-chain transaction logic. Rate limits use sensible defaults with generous headroom and are fully configurable via env vars without redeployment of code.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

None. This is server-side middleware only.

Testing

Engineering

  1. cd packages/public-api && yarn build:bundle && yarn start:prod
  2. Verify server starts normally
  3. Rapid-fire requests to /v1/swap/rates — confirm 429 after 60 requests within 1 minute:
    for i in $(seq 1 65); do curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3001/v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000"; done
  4. Verify 429 response body: { "error": "...", "code": "RATE_LIMIT_EXCEEDED" }
  5. Verify RateLimit-* headers on normal 200 responses:
    curl -i "http://localhost:3001/v1/chains"
  6. Verify env var override works: RATE_LIMIT_SWAP_RATES_MAX=5 yarn start:prod

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

No user-facing UI changes. Rate limiting is transparent to normal usage patterns. Abusive clients will receive a 429 JSON response with a Retry-After header.

Screenshots (if applicable)

N/A

Other

Also adds a gh pr edit --body workaround to CLAUDE.md — the GraphQL command fails on this repo due to deprecated Projects Classic fields, so we document the REST API alternative (gh api repos/.../pulls/<number> -X PATCH -F "body=@file").

Summary by CodeRabbit

  • New Features

    • API rate limiting enforced across endpoints with per-minute limits; rate-limited requests return 429 with Retry-After and rate-limit headers
    • Trust-proxy toggle added via environment variable and example env vars included to tune rate limits
  • Bug Fixes

    • Asset enrichment now handles errors gracefully to avoid disrupting data processing

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent 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 58827b3 and 0db7011.

📒 Files selected for processing (6)
  • CLAUDE.md
  • packages/public-api/.env.example
  • packages/public-api/src/assets.ts
  • packages/public-api/src/docs/openapi.ts
  • packages/public-api/src/index.ts
  • packages/public-api/src/middleware/rateLimit.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/public-api/src/index.ts
  • packages/public-api/src/middleware/rateLimit.ts

📝 Walkthrough

Walkthrough

Adds express-rate-limit, new rate-limiting middleware with four configurable limiters, wires those limiters into public API routes and OpenAPI 429 responses, enables trust proxy, updates .env example, and wraps asset enrichment in a try/catch to avoid failures during base asset lookup.

Changes

Cohort / File(s) Summary
Rate Limiting Middleware
packages/public-api/src/middleware/rateLimit.ts
New module creating four express-rate-limit instances (globalLimiter, dataLimiter, swapRatesLimiter, swapQuoteLimiter), env-configurable maxima, shared 429 handler, and exported RateLimitErrorCode.
API Integration
packages/public-api/src/index.ts
Imports and applies globalLimiter app-wide; applies dataLimiter to chains/assets endpoints and swapRatesLimiter/swapQuoteLimiter to swap endpoints; sets app trust proxy based on TRUST_PROXY env.
OpenAPI docs
packages/public-api/src/docs/openapi.ts
Adds RateLimitErrorSchema and shared 429 rateLimitResponse; attaches 429 responses to chains, assets, and swap endpoints.
Configuration & Dependencies
packages/public-api/.env.example, packages/public-api/package.json
Adds TRUST_PROXY=1 example and commented RATE_LIMIT_* env examples; adds dependency express-rate-limit@^7.5.0.
Asset Enrichment Error Handling
packages/public-api/src/assets.ts
Wraps getBaseAsset(...) usage in try/catch; on error preserves original asset and logs a warning.
Docs / Misc
CLAUDE.md
Minor PR workflow doc note added.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • premiumjibles
  • kaladinlight

Poem

🐇 I nibble logs and watch the tide,
Four gentle gates keep traffic wide,
A quiet hop, a throttled beat,
The API breathes calm and neat,
Carrots saved for code to glide.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The documentation update in CLAUDE.md regarding GraphQL deprecated errors is tangential but unrelated to the primary rate-limiting objective in #11676. Consider moving the CLAUDE.md documentation update to a separate PR to keep this PR focused on rate limiting implementation for #11676.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add rate limiting to public API' directly and clearly summarizes the main change: implementing rate limiting functionality in the public API service.
Linked Issues check ✅ Passed The PR implements tiered rate limiting across multiple endpoints and fixes asset handling for unknown chains, addressing the core requirement to mitigate endpoint abuse stated in #11676.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/public-api-rate-limiting

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.

Protect expensive swap endpoints from abuse with tiered rate limits:
- Global: 300 req/min per IP (all routes)
- Data: 120 req/min per IP (chains/assets)
- Swap Rates: 60 req/min per IP (GET /v1/swap/rates)
- Swap Quote: 45 req/min per IP (POST /v1/swap/quote)

All limits are configurable via environment variables and compatible
with the swap widget's 15-second polling interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@0xApotheosis 0xApotheosis force-pushed the feat/public-api-rate-limiting branch from e14eb17 to f9c3149 Compare February 23, 2026 08:09
@0xApotheosis 0xApotheosis force-pushed the feat/public-api-rate-limiting branch from 35759c7 to 58827b3 Compare February 24, 2026 05:03
@0xApotheosis 0xApotheosis marked this pull request as ready for review February 24, 2026 05:06
@0xApotheosis 0xApotheosis requested a review from a team as a code owner February 24, 2026 05:06
gh pr edit --body fails on this repo due to deprecated Projects Classic
GraphQL fields. Document the REST API alternative.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@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: 5

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

Inline comments:
In `@packages/public-api/src/assets.ts`:
- Around line 50-60: In the catch block that wraps the
getBaseAsset(asset.chainId) call (the try surrounding getBaseAsset,
enrichedAssetsById, assetId and asset), replace the silent swallow with a
structured log entry that records the caught error plus context (at minimum
assetId and asset.chainId and a short message like "failed to enrich asset with
base data"), then continue to set enrichedAssetsById[assetId] = asset as the
fallback; ensure you use the module's standard logger (e.g., processLogger or
logger) and include the error object/stack in the log metadata for debugging.

In `@packages/public-api/src/docs/openapi.ts`:
- Around line 209-210: Remove the newly added comment block containing the line
"// --- Shared Response Schemas ---" from the file; locate the standalone
comment token matching that exact text and delete it so the codebase conforms to
the "no code comments" guideline.

In `@packages/public-api/src/index.ts`:
- Line 23: The line app.set('trust proxy', 1) forces Express to trust
X-Forwarded-* headers; make this configurable via an environment variable (e.g.,
TRUST_PROXY) so non-proxy environments don't accept spoofed headers—read
process.env.TRUST_PROXY (treat empty/undefined as false), parse values like "1",
"true", or a numeric value accordingly, and call app.set('trust proxy',
parsedValue) instead of the hardcoded 1; update the initialization around
app.set('trust proxy', 1) to use this parsed env value so req.ip/rate-limiter
behavior is correct per deployment.

In `@packages/public-api/src/middleware/rateLimit.ts`:
- Around line 13-17: Create a string enum (e.g., export enum RateLimitErrorCode
{ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED' }) and replace the hardcoded
'RATE_LIMIT_EXCEEDED' in the rateLimitHandler (const rateLimitHandler:
Options['handler']) with the enum value
(RateLimitErrorCode.RATE_LIMIT_EXCEEDED); also update the OpenAPI/schema usage
to reference the same enum (via z.nativeEnum(RateLimitErrorCode)) so both the
handler and the schema share the single enum constant.
- Around line 20-27: The createLimiter function is missing an explicit return
type; change its signature to return the RateLimitRequestHandler type (the type
returned by rateLimit()), e.g. declare createLimiter(envKey: string, defaultMax:
number): RateLimitRequestHandler => ..., and ensure you import
RateLimitRequestHandler from the rate-limit package (the same module that
provides rateLimit) so the returned value from rateLimit(...) is correctly
typed; keep the body using rateLimit({ windowMs: WINDOW_MS, max:
parseEnvInt(envKey, defaultMax), standardHeaders: 'draft-7', legacyHeaders:
false, handler: rateLimitHandler }) unchanged.

ℹ️ 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 671b4fa and 58827b3.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • packages/public-api/.env.example
  • packages/public-api/package.json
  • packages/public-api/src/assets.ts
  • packages/public-api/src/docs/openapi.ts
  • packages/public-api/src/index.ts
  • packages/public-api/src/middleware/rateLimit.ts

0xApotheosis and others added 2 commits February 25, 2026 10:56
- Log getBaseAsset failures with structured context instead of silently swallowing
- Make trust proxy configurable via TRUST_PROXY env var to prevent IP spoofing
- Centralize RATE_LIMIT_EXCEEDED in a string enum shared by handler and OpenAPI schema
- Add explicit RateLimitRequestHandler return type to createLimiter
- Remove section comment violating no-comments guideline

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.

Add rate limiting etc to mitigate endpoint abuse

1 participant