DEV Community

Low Key Builds
Low Key Builds

Posted on

I sent an empty-wallet payment to 4,256 x402 routes. 38 sellers told me their facilitator was down.

Answering HTTP 402 is not the same as being payable. On 2026-10-14 I took every Base seller in the Coinbase CDP Bazaar with at least two distinct payers in the last 30 days and sent each of their routes one payment that cannot succeed: an EIP-3009 authorization signed by a brand-new wallet holding zero USDC, through the reference @x402/fetch client. No money can move. A healthy route rejects it for balance and says so. Anything else tells you how that seller treats every buyer whose payment fails.

Disclosure: I built the checker used here (ContractLens). The free single-URL check is linked at the end; it has paid options too.

The sample

  • CDP Bazaar crawl: 34,672 listed resources from 2,332 origins.
  • Probed: 4,256 routes from 1,735 origins (up to 5 routes for the 442 origins with 5+ payers and 2+ routes, up to 3 for the rest). GET routes got the listing's example query, POST routes the listing's example body.
Result Routes Origins
Payable (rejected only for balance) 2,262 935
Rejected with an opaque reason ("payment_required", "invalid_payload", empty) 916 370
Connection failed during the run (see correction below) 455 181
Other non-balance rejection (mostly input validation) 306 165
HTTP 5xx when the payment fails 91 41
Rejected payment reported as a facilitator outage 77 38
Listed route answers 404, or 5xx before quoting a price 32 20
Description over ~500 characters 16 10
Price above the $1 default client spend cap 17 14
Served the paid response anyway 5 3

Correction (2026-10-14, 10:55 UTC): the 455 "connection failed" routes are mostly not seller outages. I re-checked their 181 hosts about 90 minutes later through public DNS resolvers (1.1.1.1, 8.8.8.8): 133 responded normally, so most failures were transient or my own resolver failing under parallel load. 44 hosts fail DNS at public resolvers (SERVFAIL), which does mean their Bazaar listings point at dead names. The other rows come from responses the sellers actually sent and are unaffected.

Opaque rejections are not defects by themselves: an empty wallet and a broken payment path look identical from outside, so I don't count them as broken. The bold rows are the ones that are wrong regardless of who is paying.

1. "Facilitator unavailable" when the buyer is simply broke (38 origins)

This was the surprise. When the CDP facilitator rejects a payment, /verify answers HTTP 400 with a body like {"isValid": false, "invalidReason": "..."}. The reference SDK handles that: HTTPFacilitatorClient.verify in @x402/core throws a VerifyError carrying the reason, and the middleware returns 402 with it. My own routes answer the empty-wallet test with 402 invalid_payload: ... execution reverted.

Hand-rolled integrations often do this instead:

const r = await fetch(`${FACILITATOR}/verify`, { method: "POST", headers, body });
if (!r.ok) return res.status(503).json({ error: "facilitator unavailable" }); // wrong for 400
Enter fullscreen mode Exit fullscreen mode

So 38 sellers answer a rejected payment with 502/503 "verification temporarily unavailable", "payments_unavailable", "broker_unavailable", "retry in 60 seconds" and similar. Two of them literally put cdp_verify_http_400 and facilitator_status_400 in the message. Funded buyers are fine. Everyone else (low balance, expired authorization, reused nonce, wrong amount) is told to retry later, and agent SDKs do retry 5xx responses. The fix:

const r = await fetch(`${FACILITATOR}/verify`, { method: "POST", headers, body });
const data = await r.json().catch(() => null);
if (data && "isValid" in data && !data.isValid)
  return res.status(402).json({ x402Version: 2, error: data.invalidReason, accepts });
if (!r.ok) return res.status(502).json({ error: "facilitator_error" }); // real outage only
Enter fullscreen mode Exit fullscreen mode

2. HTTP 500 on a failed payment (41 origins)

Same symptom, cruder cause: the verify failure throws and lands in a generic error handler, sometimes rendering an HTML error page. In an earlier run of this test, one seller with 41 buyers fixed this across a whole fleet of subdomains within hours of hearing about it, which suggests it is usually an oversight rather than a design choice.

3. Listings that cannot be called (20 origins)

Twelve origins have Bazaar listings whose listed URL answers 404. In the ones I checked by hand, the URL contained the route template, like /v1/:chainId/quote or /stock/:symbol, or an example id that does not exist. Agents that pick these from the Bazaar get 404 before they ever see a price, while the listing still shows payers from earlier. Declare a concrete, working example URL, or describe path parameters in the Bazaar input schema.

Related: one seller's Bazaar example query used the parameter descriptions as values (?limit=How many articles to return.), which returns 400.

4. Descriptions over ~500 characters (10 origins)

The CDP facilitator rejects payment payloads whose resource.description is longer than about 500 characters (paymentPayload is invalid), before balance is checked. It is tracked upstream (x402#2284, #2993). The clearest case: one origin's routes with 156-character descriptions behaved normally while its 731-character route returned "facilitator rejected (400)". Keep the long text in the Bazaar extension or your docs.

5. Served without payment (3 origins)

Three origins returned a paid result or accepted a paid job (HTTP 200 or 202) for the zero-balance signature; two of them did so in two separate runs. Either verification ran after the handler, or a failed settlement did not stop the response. One also called a paid upstream cloud API before verifying, so every unpaid attempt cost the seller money. I emailed the two with a public contact privately and am not naming any of them.

What I did with this

I re-verified each hard defect, then emailed or opened an issue with 19 sellers who had a public contact, one specific finding each, nothing else attached. In the previous round of this test, three sellers fixed what was reported (one opened an upstream PR, x402#3795, to surface rejection reasons).

Check your own routes

  • Free, one URL at a time (GET or POST): https://contractlens.appin60.com/x402-payability
  • Whole origin, every route we can find in your /.well-known/x402, /openapi.json and your own Bazaar entries, with a fix hint per route: $0.25 in USDC via x402 (GET https://contractlens-api.appin60.com/audit?origin=https://your-api) or with a browser wallet on the page above.
  • A $0.99 monitor re-checks one route hourly for 30 days and emails you when it changes.

Method caveats: one test payment per route, from one location, on one day. Opaque rejections are counted as unknown, not broken. Classifications and raw results are per route; the checker shows the exact response for any URL you give it.

Top comments (2)

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

The 400-to-503 mapping is the finding that generalizes beyond x402. Treating a declined buyer as an infrastructure outage means you can never tell the difference between a seller having a bad day and a buyer failing a check, which poisons retry logic on both sides. Card rails made this exact mistake for years, translating every declined authorization into a generic gateway error the buyer's own system could not act on, and the result was the same: agents retry blindly, sellers blame their providers, and nobody gets a signal they can actually route on.

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

Good test. Saying the facilitator is down when the wallet is empty just sends agents into retries. A specific error with what to do next lets the agent top up or give up.