Overview
x402 extends HTTP with a payment layer:- Agent makes request without payment
- Server returns HTTP 402 with payment requirements
- Agent constructs EIP-3009
TransferWithAuthorizationsignature - Agent retries with
X-PAYMENTheader - Server verifies signature, processes request
Supported Networks
Step 1: Discover Payment Requirements
accepts entry is a standard x402 PaymentRequirements object, so official
clients (npm x402-fetch) can consume it directly. Notes on individual fields:
Every 402 also carries additive recovery hints under
extensions.bazaar.info, so an agent without a wallet can still get started (full reference: docs/errors.md):
The same body is mirrored base64-encoded in the
PAYMENT-REQUIRED response header.
Step 2: Build EVM Payment (Base/EIP-3009)
Step 3: Make Paid Request
Response Headers
Replay Prevention
Each nonce can only be used once (Redis, 10-minute TTL — tied to the quote’smaxTimeoutSeconds). Reusing a nonce returns HTTP 402 with “Nonce already used (replay detected)”. Settled-transfer tx hashes (see below) use a longer 24-hour replay window instead.
Settled Transfer Fallback (EVM)
If your agent settles by sending USDC directly to thepayTo address from the 402
response, retry the request with the transaction hash instead of a signature. The
retry must also carry the X-Payment-Challenge token from the same 402 response you
were quoted with (also in the settled-transfer accepts entry as extra.challengeId):
X-PAYMENT or PAYMENT-SIGNATURE): {"chainId": "eip155:8453", "txHash": "0x…"}.
Every 402 advertises this path as the accepts entry with scheme: "settled-transfer", whose
extra carries the header name and these rules. The official x402 clients skip it — they only
select schemes they have registered — so it costs them nothing.
Limitguard verifies on-chain that the transaction succeeded, emitted a USDC Transfer of at
least the endpoint price to payTo, and was mined within the last ~150 blocks (~5 minutes).
Each hash is accepted once (24-hour replay window). If our RPC node cannot see the
transaction yet (not found, or its view of the chain head is still behind the transaction’s
block, which a load-balanced RPC often is right after mining), the server re-reads for a few
seconds before answering. If it still cannot see it, you get 402 settlement_not_yet_visible
with a Retry-After header. The hash has not been consumed and stays valid for the rest of
the 150-block window, so send the same request again (same hash, same X-Payment-Challenge).
Solana uses txSignature (see above).
When a proof fails verification its replay claim is released so you can retry once the
transaction confirms. That release is retried a bounded number of times (3 attempts); if it
still fails, the claim is marked stuck rather than left indistinguishable from a real
duplicate, and your next attempt with the same proof gets 402 settlement_claim_stuck
(no automatic retry — the same response repeats for up to 24h) instead of a misleading
duplicate_settlement. REST and MCP share this code path, so both transports give the same
retry budget and the same message.
Send the amount for a single call. A transfer is redeemed exactly once, so batching several
calls’ USDC into one gas-efficient transfer buys one call and forfeits the surplus — the
excess is credited to the ledger and reported in X-Payment-Amount, but it is not refunded
and cannot be spent on a later request.
Why the challenge token (issue #205)
A settled-transfer proof is just a public tx hash, with no signature binding it to whoever presents it. Before the challenge existed, a third party watching thepayTo address
on-chain could submit your hash before you did and be served your result. The challenge
token is delivered only in the HTTP 402 you received — never on-chain — so a proof is now
redeemable only together with the token minted for that resource, and the token is
checked before the replay guard and before any on-chain lookup: a submission without it
is refused with 402 settlement_challenge_required, takes no claim on the hash, and
leaves the hash redeemable by you.
Rules:
- The token is bound to the resource path it was quoted for. A token for
/v1/risk/scoredoes not redeem a transfer for/v1/entity/check. - It expires with the quote (the same ~5-minute window a transfer must confirm within). If it has expired, request a fresh quote (a bare request → 402) and retry with the new token: your hash is still unclaimed and still yours.
- It is single-use: once a proof has verified with it, it is spent.
- The same header applies to a Solana
txSignaturefor an already-confirmed transaction. - There is no grace period after which an unrelated submission is served anyway.
PAYMENT-SIGNATURE) path
and the Solana partially-signed-transaction path are delivered to us privately, are not
exposed to this race at all, and need neither the token nor the signature.
Proving you own the sending wallet (issue #365)
Send the sending wallet’s signature over the challenge and a copied hash becomes worthless to anyone else:
Signature schemes:
- EVM (Base) — EIP-191
personal_signover that message, the ordinarypersonal_sign/eth_signany wallet exposes. Send it as hex, with or without the0xprefix. The server recovers the address and compares it to thefromof the USDC Transfer log in your transaction’s receipt. - Solana — a raw ed25519 signature over the UTF-8 bytes of that message, base58 encoded. The server verifies it against the transfer authority read from the confirmed transaction.
402
settlement_signature_invalid — and, importantly, your proof is not consumed: the
deduplication claim is released, so the genuine payer can still redeem the same hash. A
wrong signature therefore cannot be used to burn someone else’s transfer.
All three redemption paths enforce this identically: the protected REST endpoints, the
MCP tool calls, and POST /x402/verify.
Verifying a settled-transfer proof via /x402/verify
The same tx hash and challenge token can be redeemed against POST /x402/verify
directly, without retrying the protected endpoint — useful for pre-validating a
proof before spending it. Because /x402/verify has no request URL of its own to
infer the resource from, you must say which resource the proof was quoted for, via
the v2 envelope’s resource.url object or a plain resourcePath string:
settlement_challenge_required even for an otherwise-valid proof and token — see
Why the challenge token. resource must be an
object with a url field (this endpoint’s own convention, not the x402 spec’s
accepts[].resource, which is a plain string); a bare string there is not
recognized and falls back to requiring resourcePath instead. /x402/verify never
consumes the challenge token — only redeeming the proof against the protected
endpoint does, so calling /verify first does not burn your one-time token. The
same rules apply to a Solana txSignature settled-transfer proof.
Quality Tiers with x402
Control cost viaX-Response-Quality header:
X-PAYMENT must match or exceed the price for the selected tier:
enhanced was retired on /v1/entity/check 2026-09-24 (#405): it took the identical
fan-out as fresh, so X-Response-Quality: enhanced is now quoted, charged and
served at the fresh amount above (850,000), not the old 1,500,000 — the 402 body’s
info.note says so. The same retirement applies to /v1/risk/score,
/v1/reputation/score and /v1/kyb/check.
Not every tiered endpoint sells every tier. POST /v1/entity/deep-check sells fresh
(1.50, 1,500,000,
adds adverse media) always — it is unaffected by the #405 retirement above, since its
enhanced tier genuinely reaches a source fresh does not — plus a third, separately
opt-in credit tier ($2.25, 2,250,000, adds an Italian credit report) only while
OPENAPI_CREDIT_API_KEY is configured — X-Response-Quality: credit is otherwise
quoted, charged and served as fresh, and even once configured a non-Italian request on
the credit tier is requoted down to the fresh amount before payment is ever required.
It has no cache-only branch, so X-Response-Quality: cached is quoted, charged and served
as fresh.
cached is served only from cache. If nothing is cached for the entity, the response is
404 with errorCode: "not_cached" and a retry block naming X-Response-Quality: fresh
and its price; no data source is called (see docs/errors.md) and the request is not
charged. The unpaid 402 quote for a cached request probes the cache first: on a
confirmed miss it is priced at the fresh tier and carries
extensions.bazaar.info.cachedTierAvailable: false, so a caller is not sold the cheap
tier for a call that would deliver nothing. When a quote carries that flag, retry with
X-Response-Quality: fresh or drop the header; the amount in the quote is already the
fresh price, so paying it as-is also works. If the probe cannot run (cache unconfigured
or unreachable) the cached price is quoted as before; a miss on the paid call is still
not charged, because settlement is gated on the route’s 2xx response. The probe’s
answer is subject to the per-entity limit: an anonymous caller gets at most 5 quotes
per entity per hour on these four endpoints (the same counter that meters the checks
themselves, see docs/rate-limits.md), then 429, so cache state cannot be used to
enumerate which entities have been checked.
Fresh calls fill the cache, so “cached when available” means: after a fresh check of
the same request (entity, country and the same identifiers — a different KVK number is a
different cache entry). A fresh call itself never reads a cache: it always runs the live check. The refill TTL differs by endpoint: 1 hour for
/v1/entity/check and /v1/kyb/check (the trust-response cache), 7 days by default for
/v1/risk/score and /v1/reputation/score (the sanctions cache). Every endpoint warms the
cache its own cached tier reads, so fresh then cached on one endpoint works on its own;
a fresh /v1/entity/check or /v1/kyb/check additionally warms the sanctions cache that
/v1/risk/score and /v1/reputation/score read, but not the reverse.
Both cache keys are case-insensitive and ignore surrounding whitespace, so a fresh check of
"Acme BV " is hit by a later cached call for "ACME BV".
Official x402 Client Payloads
The official x402 clients (@x402/fetch and x402-fetch on npm, x402 on PyPI) send a
payload shaped differently from the flat one built in Step 2. Both of their shapes are
accepted as-is — no adapter needed:
amount and sender in payload; if
present they are used for the price check, and the transaction is still verified.
A partially signed transaction is the default the Python SDK’s SolanaWallet sends. It
compiles the transfer with the address advertised as accepts[].extra.feePayer at account
index 0 and leaves that signature slot empty, signing only as the transfer authority; the
server co-signs as fee payer and broadcasts. A paying wallet therefore needs USDC and no
SOL. Because no money has moved when the payload is presented, it is not a client-broadcast
proof and is not gated on the X-Payment-Challenge token that a txSignature or
txHash proof must be redeemed with. To pay the network fee yourself and send a
txSignature instead, construct the wallet with broadcast=True.
These payloads work against X-PAYMENT on any paid endpoint and against POST /x402/verify
and POST /x402/settle.
V1 Backward Compatibility
The olderX-PAYMENT header (V1 format with maxAmountRequired, from, to fields) is still accepted for backward compatibility. PAYMENT-SIGNATURE is the V2 spec header and is recommended for new integrations; it takes priority when both are sent.
Circuit Breaker Fallback
If the payment facilitator is unavailable (circuit breaker open), Limitguard falls back to a verified wallet cache. Wallets that have previously completed verified payments are cached. The response includesX-Payment-Fallback: true when this path is used.
Routes settled through the Coinbase CDP facilitator (see CDP_FACILITATOR_ROUTES in
Configuration) are the exception: a CDP outage fails closed with
402 server_error instead of falling back to the wallet cache, so a false “verified”
never gets served on the same authorization the CDP outage might still land later.