Most x402 bugs are not mysterious blockchain bugs. They are usually one of a handful of mismatches: the server quoted one thing and verified another, the client paid on the wrong network, the manifest advertised stale data, or the payment proof expired before the retry.
Start with the unpaid response
Before testing wallets, facilitators, or settlement, confirm your endpoint returns a clean 402 with JSON an agent can parse:
curl -i https://api.example.com/paid-resource
You should see HTTP/1.1 402 Payment Required, content-type: application/json, x402Version, and at least one valid accepts[] entry. If that part is wrong, payment cannot work reliably no matter how good the client is.
Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Client never sees a payable option | accepts[] is missing, empty, or uses informal network names | Return a real array with exact chain ids such as eip155:8453. |
| Payment succeeds on-chain, but retry still returns 402 | Server verifies against a different payTo, asset, amount, or request reference than the quote | Log the quoted requirements and the verification requirements side by side; they must match exactly. |
| Amount looks wildly too high or too low | Decimal confusion | Use base units as strings. For 6-decimal USDC, 0.01 USDC is "10000". |
| Works locally, fails from browser clients | CORS does not allow payment headers | Allow the header your client uses, commonly PAYMENT-SIGNATURE or PAYMENT, and expose useful response headers. |
| Works once, then replay attempts fail | Correct behavior: proof is single-use | Do not reuse a proof across requests. Pay again or use an explicit batching scheme if your protocol supports it. |
| Quote worked a minute ago, then fails | maxTimeoutSeconds expired | Re-fetch the 402 immediately before paying, especially in agents that deliberate across multiple resources. |
| Agent Bazaar listing does not update | Manifest was changed but not re-submitted, or the URL is not publicly fetchable | Fetch the manifest with plain curl, then re-submit with POST /submit. |
Debug the retry as a separate request
Treat the paid retry as its own HTTP request with its own logs. For each failed retry, log:
- Request URL and method.
- The payment header name that was present, without logging the full secret/proof in public logs.
- The quoted
network,asset,payTo, andamount. - The verified
network,asset,payTo, andamountextracted from the proof or facilitator response. - Whether the quote/reference was expired or already used.
That single comparison usually points straight at the mismatch.
CORS checklist for browser-based clients
Server-to-server agents rarely care about CORS, but browser clients do. If you expect web wallets or browser demos to call your endpoint, include the payment header in preflight handling:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: content-type, payment, payment-signature Access-Control-Expose-Headers: x-payment-response
Use the exact header names your client library sends. Header names are case-insensitive on the wire, but your framework's config may not feel that forgiving.
Facilitator vs self-verification bugs
If you use a facilitator, isolate whether the failure happens before or after facilitator verification:
- If the facilitator rejects the payment, inspect the client-side payment construction and wallet/network choice.
- If the facilitator accepts it but your server rejects it, inspect your local expected values and replay/expiry storage.
- If both accept it but the client still receives 402, inspect whether the retry is actually carrying the returned proof header.
If you self-verify, add a confirmation-depth policy and be explicit about it. "Transaction submitted" is not the same as "server is willing to serve the paid resource."
Manifest listing issues
If POST /submit fails, test the manifest from the outside world, not your local machine with special access:
curl -i https://yourapi.com/.well-known/x402.json
curl -X POST https://bazaar.saylorinnovations.com/submit \
-H "content-type: application/json" \
-d '{"manifestUrl":"https://yourapi.com/.well-known/x402.json"}'Typical listing blockers are private hosts, localhost URLs, invalid JSON, no paid resources, non-HTTPS URLs, and malformed accepts[] entries. The fix is almost always in the manifest, not in Agent Bazaar.
The best sanity test
Publish one tiny endpoint priced at a very small amount, test it end to end, then copy that exact payment path into more expensive resources. Once one route works, adding more x402 endpoints is mostly a manifest and routing exercise.