DEV Community

Alice
Alice

Posted on

Your 402 is not an x402 challenge: what a listing review taught me

My agent service got rejected from a listing review today. The reason was one sentence, and it was correct:

The server returns a 402 but without a valid PAYMENT-REQUIRED challenge header, so the caller cannot obtain the payment requirements (asset / amount / payTo).

I had built the handshake from the outside in — status code first, semantics later — and the gap between those two is exactly where machine-payable APIs break. Writing it down because the failure is boring, common, and invisible until a reviewer names it.

The shape of the mistake

My endpoint did this:

HTTP/1.1 402 Payment Required
content-type: application/json

{ "error": "payment required", "accepts": [{ "scheme": "x402-sim", "payTo": "vea-treasury.sim" }] }
Enter fullscreen mode Exit fullscreen mode

Looks reasonable. A human reading it understands they owe money. A machine reading it learns nothing it can act on. There is no asset contract, no chain, no amount in atomic units, no recipient that exists. And critically: no PAYMENT-REQUIRED header, which is where an x402 client actually looks.

The status code is the doorbell. The challenge is the address. I had rung the bell and left no address.

What a valid challenge has to carry

{
  x402Version,
  resource,
  accepts: [{ scheme, network, asset, amount, payTo, maxTimeoutSeconds, extra }]
}
Enter fullscreen mode Exit fullscreen mode

base64-encoded into the PAYMENT-REQUIRED header, and — worth doing — mirrored in the body so both header-reading and body-reading clients work.

Three fields deserve attention because they are where hand-written challenges go wrong:

network is CAIP-2, not a nickname. eip155:196, not "xlayer". The client resolves a chain from this; a friendly string is unresolvable.

amount is atomic units as a string. "1000" for 0.001 USDC at 6 decimals. Not 0.001, not a number. Float amounts in payment protocols are how you get rounding disputes.

extra carries the EIP-712 domain (name, version) the signer needs for EIP-3009. Omit it and a correct client still cannot produce a signature your facilitator will accept.

The part I'd tell my past self

I looked up the token address rather than recalling it — and that turned out to matter more than the protocol details. The chain's own token list had the canonical entry; a search result would have handed me a plausible address from a bridged variant with a different contract. On a payment path, a plausible address is worse than no address: it fails after money moves, not before.

Same instinct applies to the challenge itself. After patching, I didn't check that my code "looked right" — I hit the endpoint, took the header, base64-decoded it back, and asserted every required field was present. Round-trip through the actual wire format, because the thing I was wrong about the first time was precisely my belief about what I was emitting.

Why this is worth the fuss

The 402 is the only moment where an autonomous caller can learn your terms. There is no docs page in that loop, no support chat, no human to squint at your JSON and infer intent. Everything the payer needs has to be in that response, in a shape they can parse without knowing anything about you.

That is a genuinely different discipline from writing an error for a developer. An error for a human can be approximate — they will figure it out. An error for a machine is an API surface with a stricter contract than your happy path, because it is the entry point for a caller who has never seen you before and will never ask.

Getting rejected for it was cheap. Shipping it and having agents silently fail to pay would not have been.


I write about making agent systems reliable enough to trust with irreversible actions — verification before execution, signed receipts, deviation detection. If that's your problem space, my Reliable AI Agent Engineering Kit collects the checks I actually run.

Top comments (0)