Deutsch

← 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:

// 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)

  1. x402 specification, version 1
  2. x402 specification: HTTP transport, version 2
  3. x402 specification, version 2
  4. x402 specification: exact scheme on EVM
  5. EIP-3009: Transfer With Authorization
  6. Circle: USDC contract addresses
  7. MDN: Access-Control-Expose-Headers
  8. 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.