Two weeks ago I published an audit of the x402 agent-payment economy with the headline finding that 76% of listed endpoints were dead or invalid.
That number was wrong. It was measuring my own parser.
The real figure is 38%. Not because the ecosystem recovered — because my prober had been reading the wrong half of the HTTP response. This post is the correction, the mechanism, and the four additional mistakes I made in the same family while fixing the first one. That last part is the useful bit.
Everything here is reproducible with curl. Commands are inline.
The mistake
x402 puts payment requirements in a 402 Payment Required response. In v1 they live in the JSON body:
{ "x402Version": 1, "accepts": [{ "scheme": "exact", "maxAmountRequired": "10000", ... }] }
In v2 they moved into a PAYMENT-REQUIRED response header (base64-encoded JSON), and the body became a server implementation detail. Many v2 services return a body with no accepts at all — sometimes just {}.
My prober only parsed the body. So a live v2 service looked like this: HTTP 402 received, no accepts found, verdict malformed, public advice "do not pay: dead or invalid."
Here is one of them:
curl -sD - -o /dev/null https://api.onesource.io/api/chain/block-number \
| grep -i '^payment-required' | sed 's/^[^:]*: //' | base64 -d
A complete, valid challenge: scheme: exact, network eip155:8453, amount: 1000 (0.001 USDC), a real payTo. The service works. It always worked. My oracle told agents it was dead, with a trust score of 8 out of 100 and 0% uptime.
1,869 endpoints across 166 operators were in that state. Among them, pro-api.coingecko.com.
I also owed a catalog an apology. I had been publishing that 402index overstates health — that only ~30% of what it lists as "healthy" really is. After the fix, the same comparison gives ~68% (live figure in catalogAudit.accuracyPct at /status.json — it moves with each crawl). The catalog was substantially more honest than I said it was. Mine was the unreliable measurement.
The fix, and what it cost
Fixing the parser was ten lines: read the body, read the header, prefer the header (that is what a v2 client pays against), record which one carried the challenge.
The expensive part was history. My uptime metric is the share of past observations with a healthy verdict, and for these services every past observation said malformed. That is not their downtime — it is my bug, recorded as their track record. I removed 3,920 malformed history entries from 1,869 endpoints that now return a valid header challenge, kept every unreachable and no-402 entry (my bug could not produce those), and stamped each repaired record with historyRepairedAt, historyRepairedEntries and a note. Those markers are in the free /verify response, so anyone reading a track record can see it was edited and why.
Which created a new problem: services with one observation and a "100% uptime" badge. So uptime is now null below three observations with an explicit insufficient confidence flag, and the trust score no longer grants full uptime credit on a thin history. A track record you cannot state is better than one you invent.
The daily time series had the same hazard. Live endpoints went 1,452 → 3,487 in one snapshot. Any reader — including future me — would see a market doubling overnight. Each snapshot now carries probeVersion, and day-over-day deltas across a version boundary are suppressed with a stated reason instead of rendered as green arrows.
Current numbers, checkable right now:
curl -s https://pulsefeed.dev/status.json | jq '.ecosystem'
Then I made four more mistakes of exactly the same kind
This is the part I would want to read.
Having just been burned by a parser that knew less than the spec, I added detectors for endpoints that return a valid 402 but still are not payable products. Within hours, three of them were wrong, in the same way, for the same reason: my code encoded my assumption about the protocol instead of the protocol.
1. I called a standard scheme unpayable
I decided the only settleable scheme was exact, flagged everything else as "no standard client can settle this — funds leave escrow, irreversibly," and published it at high severity.
The x402 spec has four schemes:
curl -s https://api.github.com/repos/x402-foundation/x402/contents/specs/schemes \
| jq -r '.[] | select(.type=="dir") | .name'
# auth-capture, batch-settlement, exact, upto
The service I flagged, api.bitrefill.com/x402/invoice/pay, uses upto — a normal scheme, in the spec, and by on-chain volume one of the largest receivers in the ecosystem. Three separate claims in my alert were false. I had reproduced my own bug against a different protocol field.
The real distinction is not payable versus unpayable:
- outside the spec → settlement does not go through a facilitator, funds are not in escrow — worth flagging;
-
in the spec but not
exact→ a defaultx402-fetchclient cannot pay it without work. That is a compatibility note, not an accusation.
One endpoint in my index is genuinely outside the spec: api.dynsuplabs.com advertises exact-prepay-proof, and its own extra.doc field instructs the client to broadcast the USDC transfer itself, outside any facilitator, stating that payment is final and non-refundable. That one deserved a flag. The other did not.
2. I read an authorization ceiling as a price
Same service, second false alarm. Its challenge carries amount: 1000000000 — 1,000 USDC. My price detector saw a thousand-dollar API call and flagged "absurd price."
In upto, amount is the maximum the service is authorized to draw, not the price of a call. It is a gift-card purchase flow; the actual charge is usage-based. Comparing a ceiling against other services' per-call prices means comparing two different quantities and calling the difference fraud.
Ceilings now live in a separate field with an explicit note — never in the price field, never in price comparisons.
3. I described a mechanism I never tested
Hundreds of listed URLs are documentation placeholders — /api/chain/tx/:hash, /v1/{id}, /example — that still return a valid 402. I wrote that the 402 is served "by middleware before the route is resolved," and that a paying agent "gets a 404."
Both were guesses. Requesting a genuinely nonexistent path on the same hosts:
curl -s -o /dev/null -w '%{http_code}\n' https://skills.onesource.io/api/chain/zzz-not-real-route # 308
curl -s -o /dev/null -w '%{http_code}\n' https://api.gocreativeai.com/definitely-not-a-real-zzz999 # 502
Not 402. So the route did match — the placeholder was accepted as a parameter value. And what a payer receives I do not know, because I never paid. The claim is now limited to what is observable: the URL is a string from documentation, the challenge is valid, and catalogs count it as a live service.
4. I attached intent to companies I had just proven myself wrong about
My public feed labeled named, identifiable businesses with "bait-and-switch pricing," "price gouging," "honeypot receiver," "possible hijack." In the US the first two carry statutory and FTC meaning; all four assert intent. I cannot demonstrate intent from an HTTP response — and I had just been wrong about the mechanism three times in one day.
Labels now describe the observation. "Advertised price differs from the amount actually requested." "Receiver address changed between observations," with the note that routine key rotation and a compromise are indistinguishable from outside. A high price now carries "may be entirely legitimate for what it returns — we measure the number, not its fairness."
What "live" actually means
Every one of these mistakes came from treating "returned a 402" as if it meant "is a payable product." It does not. Here is the checklist I run now, written so you can apply it to your own index. Copy it.
| # | Check | Why it exists |
|---|---|---|
| 1 | Parse the challenge from both the body and the PAYMENT-REQUIRED header, preferring the header |
v2 moved it; body-only readers see live services as broken |
| 2 | Compare the two when both are present | If they disagree, a v2 client pays what the header says — which is not what a body-only check displays |
| 3 | Check the scheme against specs/schemes/
|
Outside the spec means settlement bypasses the facilitator; in-spec-but-not-exact is a client compatibility matter |
| 4 | Know whether amount is a price or a ceiling |
upto and auth-capture state a maximum, not a per-call cost |
| 5 | Check that the network is mainnet | A testnet sandbox priced in dollars is not a product |
| 6 | Check the URL for unfilled path placeholders |
:id, {id}, /example are documentation strings |
| 7 | Know the token's decimals per chain | USDC is 6 decimals on most chains and 18 on BSC; assuming 6 everywhere is a 10¹² error |
| 8 | Count distinct operators, not endpoints | One host can list hundreds of routes |
| 9 | State the observation count next to any uptime figure | A percentage over one observation is not a track record |
| 10 | Version your measurement method and mark the break | Otherwise your own fixes read as market movement |
What my own numbers look like under that checklist
The same discipline, applied to me. My index currently reports 3,487 live endpoints. Inside that:
-
682 are documentation placeholders — and honesty requires the next sentence: 582 of them are a single host,
api.gocreativeai.com. Without it, 100 placeholders across 22 domains. Reporting "682 across 23 domains" and stopping there would be exactly the inflation I criticize. - 29 are on test networks.
- 1 advertises a scheme outside the spec.
- 1 has a body/header disagreement.
Honest remainder: 2,775 endpoints across 338 operators.
And those 3,487 endpoints resolve to only 350 distinct hosts, with the top three accounting for 1,575 of them. This is a far narrower ecosystem than any endpoint count suggests.
Other limits, stated plainly: my observations begin 2026-07-06, which is shallow. Other trackers have deeper history and broader coverage — x402.fuchss.app publishes on-chain data complete since 2025-05-09 across roughly 97k listed endpoints, and it separates reachability from envelope compliance, which is precisely the distinction I got wrong. Where our numbers differ, the longer series is more likely to be right about trends. I am confident only about what I can re-probe today.
Verify everything here yourself
# ecosystem totals, composition, catalog comparison
curl -s https://pulsefeed.dev/status.json | jq '.ecosystem, .catalogAudit'
# one endpoint, free
curl -s 'https://pulsefeed.dev/verify?endpoint=https://api.onesource.io/api/chain/block-number' | jq
# every anomaly currently observed, with on-chain references
curl -s https://pulsefeed.dev/incidents.json | jq '.byCategory'
# the v2 header, straight from the source
curl -sD - -o /dev/null https://api.onesource.io/api/chain/block-number \
| grep -i '^payment-required' | sed 's/^[^:]*: //' | base64 -d | jq
The dataset is CC-BY-4.0 on Hugging Face. The original article now carries a correction notice at the top, archived daily reports produced by the old prober carry a banner telling readers not to cite their liveness numbers, and /llms.txt carries a machine-readable correction beside the link.
If you run a catalog, a wallet, or an agent that decides whether to pay an endpoint: run check #1 today. It is ten lines, and in your own dashboard it is the difference between a live ecosystem and a mostly-dead one.
If you find an error in this post, tell me. That is now demonstrably the fastest way to improve this dataset.
Top comments (0)