Repository navigation
x402Serve() has no replay protection or rate limiting — document as a known limitation #91
Description
Context
While verifying a third party's ("Built with Nirium" on nirium.xyz/stats) claim about their production x402 setup, we confirmed that x402Serve() has no protection against a reused X-PAYMENT header and no rate limiting. Both have to be built by the integrator on top of it.
Evidence
Downloaded the real published tarball for the version pinned in that integrator's package.json (nirium@0.11.0) and grepped the full dist/index.js for replay, nonce, rate limit, dedupe, idempot, single-use — zero matches. Read the x402Serve() function body directly (dist/index.js lines 565-702 in that tarball): it validates payTo/routes/facilitatorApiKey, builds the route map, and wraps paymentMiddlewareFromConfig from @x402/express. No claim tracking, no store, no window counter anywhere in the function.
The integrator (Edgadafi/remesa-liquidez, commit 1e0cbd59, confirmed via the GitHub Deployments API as the live "Production" deployment) built both protections themselves, mounted before x402Serve():
- Single-use
X-PAYMENT:backend/src/middleware/paymentGuard.ts— hashesX-PAYMENT+ method + URL, claims it once viaSET NXon Upstash, releases the claim if the request doesn't end in 2xx (so a legitimate retry after a facilitator failure isn't blocked), and fails closed (503) in production without a durable store. - Rate limiting:
backend/src/middleware/rateLimit.ts— sliding window per IP on Upstash, mounted per-route-group inapp.ts, fails open on store failure.
Why it matters
x402Serve() doesn't itself claim to do either of these, and neither does the README. But someone who copies our own examples/deploy-x402-vercel or any of the SDK's other x402 examples and doesn't independently build what this integrator built ends up with an endpoint where the same signed payment proof can be replayed as many times as it's resent before the facilitator's own settlement window closes, and with no defense against a request flood.
Proposed directions (not asking for either in this issue, just registering the gap)
- Document it as a known limitation in the README and in
/developers— "x402Serve()verifies and settles the payment; it does not deduplicate a proof across requests or rate-limit callers. Add your own guard (see [example link]) if you need either." - Evaluate, separately, whether an opt-in
replayGuard/rateLimitconfig onx402Serve()itself (backed by a pluggable store, matching the KV-store pattern this integrator used) is worth adding to the SDK later.
Verification
Confirmed by downloading the real npm tarball for nirium@0.11.0 (not reading the repo source) and grepping the actual dist/index.js that ships to consumers, cross-checked against the integrator's own commit history via the GitHub API (not just their word).
🤖 Generated with Claude Code
Activity
docs: note x402Serve() has no built-in replay/rate-limit protection (#92
Eras256 commentedon Sep 24, 2026
Resolved in two merged PRs:
guardconfig (off by default, zero change to existing consumers) that turns on single-use replay protection for the payment proof and a sliding-window rate limit, backed by a pluggable store. Ships withcreateUpstashX402GuardStore()as a reference adapter, andexamples/deploy-x402-vercelnow runs with it turned ON by default instead of only showing the disabled state.The design (the store interface, releasing a claim only on a non-2xx response, fail-closed for replay / fail-open for rate limiting) is generalized from, and credited in the code and CHANGELOG.md to, the real production integrator this issue was originally filed about, Edgadafi/remesa-liquidez.
Tests reproducing the exact scenarios discussed here (reused proof -> 409, released on failure -> allowed retry, store down -> 503 not silent pass-through, rate limit -> 429, store down -> fail-open) pass in CI: https://github.com/nirium-protocol/nirium/actions/runs/36051437859
Closing.