← Meterdoor · Article · As of October 2026
Building the x402 402 response: the most common mistakes
The server sends 402, but no agent pays. Usually a single field in the payment requirements is to blame.
Ten sources of error that our checker tests for – derived from the specification and from mistakes we made while building our own
middleware, for x402 version 1 and 2.
With x402, a server answers an unpaid request with 402 Payment Required and describes in the response what it
asks for: network, token, amount, recipient. The client reads these requirements, signs a matching USDC authorization and repeats
the request. The client does not check what you meant, only what is there. An amount in the wrong format or a wrong token name does
not produce a helpful error message; it means nobody pays, or the signature is rejected later.
First, the versions: in version 1 the requirements are JSON in the body [1]. In version 2
they are Base64-encoded in the PAYMENT-REQUIRED header; the specification names the header as the authoritative place,
the body is up to the server [2][3]. Many servers send both today. This is what correct requirements look like in version 2
(decoded header, Base Sepolia testnet, 0.01 USDC):
{
"x402Version": 2,
"error": "Payment required",
"resource": { "url": "https://api.example.com/api/report",
"mimeType": "application/json" },
"accepts": [{
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0xYourReceivingAddress…",
"maxTimeoutSeconds": 60,
"extra": { "name": "USDC", "version": "2" }
}]
}
1. Something other than 402 comes back
The most common case is mundane: an auth layer, a proxy or the order of your middleware means the client gets 401,
403, a redirect to a login page or even 200 with content. Without 402, no x402 client starts
paying. Test the endpoint the way a stranger’s client sees it: no cookies, no token, with curl -i.
2. Amount as a decimal
amount (version 2) and maxAmountRequired (version 1) are strings containing an integer in
the smallest unit [1][3]. USDC has six decimals, so 0.01 USDC is "10000". Wrong: "0.01",
0.01 as a number, and 10000 without quotes. An amount of "0" is also an error.
3. Network format of the wrong version
Version 1 names the network with strings like "base" or "base-sepolia" [1]. Version 2 requires CAIP-2
identifiers: "eip155:8453" for Base, "eip155:84532" for Base Sepolia [3]. If you move to version 2 by only
adding the header and copying the version 1 entries, you send "base-sepolia" in version 2 – or, the other way round,
"eip155:84532" in version 1. Neither matches any client.
4. Token address does not match the network
asset is the USDC contract address on exactly this network. On Base it is 0x8335…2913, on
Base Sepolia 0x036C…CF7e [6]. A typical mistake when moving to mainnet: the network is switched, the testnet address
stays. The client then signs for a contract that does not exist on that network or is not USDC.
5. Wrong EIP-712 name in extra
For the exact scheme with EIP-3009, the client needs the EIP-712 name and version of the token contract, otherwise
it cannot sign correctly [4][5]. The catch: the names differ per network. The USDC contract on Base reports "USD Coin",
the one on Base Sepolia "USDC", both with version "2" (our own call of name() and
version() on both contracts, 26 October 2026). Almost all examples in documentation use the testnet and show
"USDC". Copy that to mainnet and you get signatures the contract rejects – only at settlement, after the client has
already “paid”.
6. Recipient invalid or a placeholder
payTo must be an address with 0x and 40 hex characters. Typical leftovers from templates are
placeholders like the one in the example above, shortened addresses and the zero address 0x000…000. A payment to the
zero address would be lost. If the recipient comes from an environment variable, check that it is really set on the server and does
not flow into the response empty.
7. maxTimeoutSeconds missing or too short
The field is required [1][3]. A very small value (a few seconds) makes payments fail that take a little longer on chain. The examples in the specification use 60 seconds.
8. PAYMENT-REQUIRED header missing, not Base64, or invisible in the browser
Version 2 clients read the requirements from the header [2]; we reproduced this ourselves on testnet with
@x402/fetch 2.28. Three variants of this mistake:
- The header is missing entirely – the endpoint only speaks version 1.
- The header contains plain JSON instead of Base64, or Base64 of something other than UTF-8 JSON.
- The header is there, but browser clients on other origins are not allowed to read it. Browsers expose non-standard response
headers only if the server lists them in
Access-Control-Expose-Headers[7]. The same applies toPAYMENT-RESPONSE. And because the client sends thePAYMENT-SIGNATUREheader on the second attempt, anOPTIONSpreflight comes first, and it must not end up behind the paywall.
// mount before the paywall
app.use("/api", (req, res, next) => {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", "Content-Type, PAYMENT-SIGNATURE, X-PAYMENT");
res.set("Access-Control-Expose-Headers", "PAYMENT-REQUIRED, PAYMENT-RESPONSE, X-PAYMENT-RESPONSE");
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
Whether you want to allow other origins at all is a separate decision; for agents that do not run in a browser, CORS does not matter.
9. Version 1 and version 2 ask for different things
If you offer both versions, the requirements are often maintained in two places. After a price change the amount is then right in only one version, or the header names a different receiving address than the body. Generate both from the same configuration and compare recipient, amount, token and network.
10. Resource missing or relative
Version 1 requires resource in every entry, version 2 requires resource.url once [1][3]. In both cases
this is the absolute URL of the resource. A relative path like /api/report or, behind a proxy, the internal address
(http://localhost:3000/…) are common mistakes.
Bonus: testnet in production
Not a format error, but costly: the endpoint is live but asks for USDC on Base Sepolia. Payments work, they are just worthless.
When moving from test to mainnet, change three things together: network, asset and
extra.name (see mistakes 4 and 5). Mainnet also needs a different facilitator: the public one at
x402.org/facilitator supports testnets only [8].
What a correct response does not guarantee
A flawless 402 response only means that a client can pay. Whether your server verifies the payment correctly, charges
only on success and does not deliver twice when a connection drops cannot be seen in the response. Only a real test run on testnet
helps, for example with @x402/fetch and a test wallet. Other schemes (such as exact with Permit2, or
networks other than Base) follow their own rules [4].
Check it yourself
Our free x402 checker tests these ten points: enter a URL or paste the output of
curl -i, and the result shows OK, note or error for each field. It runs in your browser, does not pay, does not connect
a wallet and does not send your URL to us. In detail it checks USDC on Base and Base Sepolia with the exact scheme; it
does not detect the OPTIONS preflight or an internal address like localhost in the resource, checks CORS
only when fetching directly, and a passed check does not guarantee a successful payment.
If you would rather not build the requirements by hand: the Meterdoor core generates version 1 and 2 from one configuration and, at the same endpoint, also serves people paying in euros via Stripe Checkout. It is open source (Apache 2.0), a prototype, and so far tested only on testnet and in Stripe test mode. Source code on GitHub · Product page
This article is general technical information, not legal or tax advice.
Sources (retrieved 26 October 2026)
- x402 specification, version 1
- x402 specification: HTTP transport, version 2
- x402 specification, version 2
- x402 specification: exact scheme on EVM
- EIP-3009: Transfer With Authorization
- Circle: USDC contract addresses
- MDN: Access-Control-Expose-Headers
- x402-foundation/x402: README @x402/express (“The default https://x402.org/facilitator supports testnets only”)
Meterdoor is not an official product of x402, Coinbase or Stripe. x402, Stripe and Base are trademarks of their respective owners.