None of this showed up as an outage. Every one of these records returned HTTP 200, well-formed JSON, on time. That is the whole problem with the discovery layer: a record that lies is indistinguishable from a record that works, right up until a buyer acts on it.
The audit started with a shape in the log that made no sense. In one day, one IP requested /n 116 times, /s 87 times, /i 87 times, /e 87 times, then a long tail of single-character paths at 58 and 29 hits each. 1,189 requests, all 404, all one character long.
The counts are the tell. They're all multiples of 29: 4×29, 3×29, 2×29, 1×29. That's a character-frequency histogram — some string, iterated 29 times. Solving for a string with four n, three each of s/i/e, two each of t/o/l/k/a/w/2/-, and one each of x/v/u/r/p/m/j/h/c/4/0 gives exactly one answer:
https://minia2a.uk/.well-known/x402-service.json
(The / and . characters are missing from the histogram because those requests normalize or redirect rather than 404 — which is itself a confirmation.)
A client was iterating that URL as if it were a list. And it had a good reason to. We publish the field name endpoints in two sibling records:
| Record | endpoints is… |
|---|---|
| /.well-known/x402-service.json | an array of 82 endpoint objects |
| /.well-known/x402 | a bare URL string |
Any consumer that learned the field from the first record and then read the second one gets a string where it expected a collection. In Python or JavaScript, iterating a string doesn't throw — it quietly yields characters. So the crawler dutifully fetched /h, /t, /t, /p, /s, and never once reached our catalog.
We'd been reading that as crawler noise. It was our schema.
The fix meant regenerating one record from the other, which meant actually reading the catalog — and that surfaced something worse. Our published prices didn't match what the gateway charges.
Our flagship gas-price endpoint was published at 0.005 USDC. Its actual 402 says:
"accepts": [{
"amount": "500000", // 6-decimal USDC = 0.5, not 0.005
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xf16F0882...",
"scheme": "exact"
}]
A hundred times the advertised number. Across the catalog the error ratios clustered at 100× (20 entries), 500× (6), 50× (2), and one each at 300×, 10×, and 5×. Under-pricing sounds like the harmless direction to be wrong in. It isn't: an agent that budgets from your catalog and then meets a paywall demanding 100× more doesn't negotiate. It fails the call, or it silently overpays. Either way the number you published was not a price, it was a guess someone made once and nobody re-checked.
The same audit found the catalog's payment.address pointing at a wallet the gateway no longer settles to. All 82 live 402s name the same current address; the published record named a different one. USDC sent to the address we advertised would not have been credited.
Then the OpenAPI spec, which is the surface external indexers actually pull — one client fetches ours 266 times a day.
Most of our services are proxied by the gateway, so their published path is ours and the 402 happens on our edge. But a handful are seller-hosted: the seller runs the API on their own infrastructure and registers it with us. For those, the generator was publishing the seller's own upstream path, with an operation-level servers override pointing at the seller's host. That's legal OpenAPI. It was also wrong twice over.
We were, in our own machine-readable catalog, advertising the bypass. Every one of those services answers a proper 402 at the gateway's proxy route, so that's what we publish now — no server override, one payable path per service.
Smaller, but the most embarrassing. Four records described agent registration as Ed25519 keypair signing. The gateway verifies EIP-191 personal_sign over an exact message string. One of those records is aimed specifically at LLM and catalog ingestion, and its example query read "register my agent using my Ed25519 key" — we were feeding a nonexistent integration path to the systems most likely to repeat it verbatim.
Another named a registration endpoint that only answers GET. It returns a pointer to the real endpoint, which is sensible, but the record called the pointer the endpoint — so anything that POSTed where we said to POST got a 404.
Every defect here has the same shape: a claim about the system, stored outside the system, that nothing re-derives. Prices were copied into a JSON file when they were true. The payout address was correct when it was written. The Ed25519 description matched a design that changed. The type mismatch was two developers reasonably naming two different things endpoints.
Static files don't rot on their own — they rot because the thing they describe moves. So the fix isn't "be more careful when editing JSON." It's to stop storing the claim:
All of it now runs as a daily check that fails loudly on drift. But a check you've only ever watched pass proves nothing — a regex that matches nothing also reports zero mismatches. So we planted the old wrong price back into the input and confirmed the checker reported it:
handbook prices: x402-gas says 1c, charges 50c → FAIL
Then reverted. That takes two minutes and is the difference between a guard and a decoration.
One last thing worth internalizing, because it changes how much the fix is worth. A separate client has been calling five endpoint IDs that no longer exist, roughly 720 times a day, for a week — while re-fetching our (now corrected) spec hundreds of times a day. The dead IDs are gone from everything we publish. It's still calling them.
Downstream consumers cache, merge, and hand-copy your records into their configs. Fixing the record fixes the next reader, not the current ones. Which is the real argument for validating discovery records continuously rather than carefully: every day a wrong record is live, some fraction of its readers commit that error to a config that outlives your fix.
Figures measured against the live gateway on August 18–19, 2026: 1,189 single-character requests and the 404 counts come from the gateway's own request log; prices and payout addresses were read from live 402 challenge responses at the time of the audit. Platform totals are snapshots — current values are at /api/stats. The seller-hosted upstream that serves without a paywall is deliberately not named here.