Why your x402 endpoint is unpayable by half the clients
Two wire dialects, three lockouts, and the fix — measured against the real shipped SDKs, not the spec.
The short version. The x402 ecosystem ships in two wire dialects and most sellers serve only one. The npm x402-fetch line (v1) parses the 402 body and needs friendly network names and maxAmountRequired; the @x402/* line (v2) reads the PAYMENT-REQUIRED header and speaks CAIP-2. Serve one and the other half of the ecosystem cannot pay you — and you will never see it, because a client that cannot parse your 402 never sends a request you can log.
How we found it: our own test clients shared our own blind spot
We ran a paid x402 API for weeks with almost no conversions. Every check we had said the pipe was healthy: the 402 validated against the facilitator, the catalog listed us, our own probes paid successfully end to end. The probes were the problem. We had written them against our own server, so they inherited every assumption our server made. The one client shape that could have revealed the bug was the one we never ran — somebody else's.
The fix was to install the actual published packages and point them at production. Three separate lockouts surfaced within minutes. All three are invisible from the seller side.
Lockout 1 — the payload echo that no SDK sends
Our matcher required the payment header to contain an accepted field echoing the chosen requirements back to us. That felt reasonable, and it is what the v2 reference payload carries. But the widely-installed v1 clients send:
{ "x402Version": 1, "scheme": "exact", "network": "base", "payload": { … } }
No echo. Our server answered payment_requirements_mismatch — an error that reads like the buyer made a mistake. They had signed a real transfer, been refused, and moved on. Name the rail from the top-level scheme + network pair when the echo is absent, and only reject an echo that names terms you did not offer.
Lockout 2 — the network dialect (this is the big one)
The v1 client zod-parses the 402 body against a schema whose network enum contains "base", "base-sepolia", "solana" and friends. It does not contain "eip155:8453". If your body carries CAIP-2, the client throws invalid_enum_value before constructing a payment. It also reads maxAmountRequired, not amount.
The v2 line ignores the body entirely when a PAYMENT-REQUIRED header is present, and that header is CAIP-2. So the two generations read two different documents. The fix is to serve both, and to be deliberate about which is canonical:
// BODY accepts[0] — readable by BOTH generations (v1's schema is
// non-strict, so the extra v2 keys pass straight through):
{
"scheme": "exact",
"network": "base", // v1 needs the friendly name
"networkCaip2": "eip155:8453", // v2 information, harmless to v1
"maxAmountRequired": "5000", // v1 reads this
"amount": "5000", // v2 reads this
"resource": "https://example.com/api/thing",
"description": "…", "mimeType": "application/json",
"payTo": "0x…", "asset": "0x…", "maxTimeoutSeconds": 120
}
// HEADER (PAYMENT-REQUIRED, base64): canonical v2, CAIP-2, byte-stable.
// Catalogs index this and clients sign against it — never reshape it casually.
The trap inside the fix. If you run multiple rails, do not put the non-EVM ones in the body. The v1 client parses every entry in accepts[], so a single Algorand entry throws the whole array and re-creates the lockout you just fixed. Extra rails belong in the v2 header.
Lockout 3 — CAIP-2 truncation on Algorand
CAIP-2 truncates a chain reference to 32 characters. Algorand mainnet's genesis hash is longer, so the spec form is algorand: plus the first 32 characters. The TypeScript @x402/avm SDK matches that truncated form and compares strings exactly. The Python x402-avm package hard-codes the full padded hash and raises Unsupported CAIP-2 network on the truncated one. Facilitators may advertise the long form. Serving either spelling alone locks out an entire language's worth of buyers; serve both as separate accepts entries and normalise both on the way in.
Two more that kill quietly
- The 500-character description cap. A resource description longer than 500 characters makes the CDP facilitator reject verify and settle — your endpoint keeps serving 402s that nobody can pay, and the 402 itself looks perfect.
- The 30-day delist clock. A catalog listing with no settlement for 30 days is removed from the catalog and from search. There is no re-register API: only a real settled payment refreshes it. If your traffic is all crawlers, your listing has a countdown on it.
How to know, instead of hoping
Every one of these is invisible to a seller testing with a seller's client. The only check that works is to pay your own endpoint with the real published packages — and you can do it for free, because signing costs nothing and an empty wallet fails at the right stage:
// canary.mjs — an empty throwaway wallet, no funds move.
// PASS = the failure comes from the FACILITATOR (insufficient funds).
// FAIL = the failure comes from YOUR server (mismatch, parse error).
import { wrapFetchWithPayment, createSigner } from "x402-fetch"; // v1 line
import { generatePrivateKey } from "viem/accounts";
const signer = await createSigner("base", generatePrivateKey());
const f = wrapFetchWithPayment(fetch, signer, BigInt(1000000));
try { await f("https://your-endpoint.example/api/thing"); }
catch (e) {
const m = String(e.message);
if (/mismatch|invalid_enum_value/.test(m)) console.log("RED — your server:", m);
else console.log("GREEN — reached the facilitator:", m.slice(0, 120));
}
Run it against both SDK generations, and against every rail you advertise, on a schedule. We run ours daily; it has caught a regression that a full deploy-gate suite did not, because the gates test what we believe and the canary tests what the ecosystem actually does.
Why we are giving this away. We sell a scored market board to agents over x402 — not developer tooling. These lockouts cost us weeks of silent zero-conversion, and there is nothing written about them anywhere. If you sell over x402, an afternoon with the real clients is the highest-return afternoon available to you. If you want to see the shape in production, our own 402 is public and free to inspect: coil.trade/api/board/regime — the body is the v1 dialect, the header is canonical v2, and both rails are in the header.
What Coil is, briefly
Coil is a long-only trading system and a scored market board that AI agents buy per read over x402 — opportunity, entry-window and hold-strength scores across the S&P 500, the Nasdaq-100 and a macro book, plus regime verdicts, from $0.001 a call with no account and no subscription. Scores and states only; never stop prices, never targets, never advice. See the agent docs, poll the free change-detection hash, or check the forward-return audit before paying us anything.