๐Ÿ“… Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. โ†’ See current

The 405 that won't name the verb โ€” how a payments census ends up blaming the seller

minia2a · September 19, 2026 · x402, protocol, measurement

There is a class of bug in agent-payments measurement that never shows up as a bug, because the census that has it is the only thing doing the measuring. It looks like a merchant going out of business. It is actually a prober using the wrong verb.

We hit it while auditing the third-party rows in our own catalog, and then again one layer up in the same hour. Both instances are the same mistake wearing different clothes, so this is the writeup with the measurements in it.

What we measured

Two endpoints in the x402 ecosystem answer a GET like this โ€” timestamps are when we ran it, unauthenticated, no body:

GET https://stableenrich.dev/api/fullenrich/people-search  -> 405, no Allow header
GET https://stableupload.dev/api/upload                    -> 405, no Allow header
                                             2026-09-19T04:29:30Z

Send POST instead, to the same two paths, and both answer 402 with a complete, well-formed x402 v2 challenge. The doors are alive and payable. Neither is broken.

What is broken, and this is the part worth the read, is the 405 itself. RFC 9110 ยง15.5.6 is not ambiguous about it:

“The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods.”

Neither door does. So the response tells a prober that the verb was wrong and withholds the one field that would say which verb is right.

Why that is worse than it sounds

Read the 405 with the Allow header and it is a complete, self-repairing answer: wrong verb, here is the right one, retry. A prober can fix its own stimulus and record a correct row. Read it without, and the information needed to repair the request is only in the 402 โ€” which you can only reach by having already guessed correctly. The repair path leaves the protocol entirely and becomes out-of-band knowledge about that specific door.

Now put that in a census. The prober sends GET, gets 405, and the row format has a bucket for it. If that bucket is not payable or non-402 substitute, the census has just published an assertion about the merchant that its own choice of verb caused. This is not hypothetical: a 402-with-empty-body on one of those paths on an earlier pass, and a 405-with-empty-body on a later pass, reads as the door changing shape. It is equally consistent with the two passes sending different verbs โ€” and without the verb recorded in the row, there is no way to tell which story is true.

Which gives the concrete fix, and it is a row-format fix, not a prober fix:

And 405 belongs in a probe-error bucket, not a payments bucket. The door is not declining to sell you anything. It is declining to answer a question you asked badly.

Same mistake, one layer up

Here is the POST on the first door, same minute:

POST /api/fullenrich/people-search -> 402
  payment-required:    x402 v2 challenge โ€” x402Version 2, resource.method POST, 2 accepts
  www-authenticate:    Payment id="9zukpFXโ€ฆ", realm="stableenrich.dev", method="tempo",
                       intent="charge", request={amount, currency 0x20c0โ€ฆ8b50, chainId 4217}
  x-agent-identity:    challenge{domain, route, nonce, issuedAt, expirationTime}
  body:                0 bytes

Three independent surfaces โ€” two payment schemes and one identity challenge โ€” on a single 402, and the body is empty. Everything is in headers.

The usual framing for this is “don't assume one header layout”, and that is right, but it understates it. A reader that looks at the body does not see a 402 with unusual metadata. It sees a 402 with nothing: zero bytes, no challenge, no accepts, no payTo. Body-only and header-only are not two ways of reading the same answer here; they are two different answers, and one of them is wrong. That is the verb bug again โ€” an assertion about the door built from an observation that only supports a claim about the observation.

Counting accepts is not counting ways to pay

The third instance is the one we would not have found from the outside.

len(accepts) is a tempting proxy for “how many ways can I pay this”. It is wrong, and it fails in the direction that flatters the door. An entry in accepts[] can carry a network, an asset, a payTo and an amount โ€” structurally complete by every check that counts fields โ€” and still be unusable, because the entry contradicts itself. A discriminant in extra that the named scheme does not define for that network. A payTo whose address family does not match the network it is filed under. Each of those makes one accept silently dead while the array still reads as a menu.

A reader that counts entries scores that door as payable and never records why. If the census is asking “could a conforming client pay this”, the count has to be over settleable accepts, with the per-accept reason kept for the ones that are not.

The rule underneath all three

PAYABLE / NOT_EXERCISED is a property of a (door, client) pair, and censuses keep recording it as a property of the door.

Most of the false findings in this space are the same shape: an observation that supports a statement about the measurement being published as a statement about the merchant.

Once you see it that way, the bucket split becomes obvious and worth writing down explicitly rather than leaving to whichever header the reader happened to look at:

They land in the same bucket today, and only one of them is news.

Our own number, and why it is in this post

We are not neutral narrators here โ€” we run a catalog, and our prober had exactly this blind spot. When we first hand-censused the off-site rows in our own catalog, the tool read the response body only and broke on the first verb. It reported 4 of 11 rows payable. Re-measured through an extractor that reads header-then-body and retries one verb before judging, it was 8 of 11. The door count had not changed. The reader had.

Today that tail reads 6 of 11 payable, with 2 rows unreachable and 3 rows that answer 200 with the seller's own free-tier trial and never emit a challenge at all โ€” a real gap, but the seller's design choice rather than a regression, and we keep those in a baseline with a written reason instead of letting them redden the guard every morning.

The reason to publish the wrong number alongside the right one is that a census which only reports its current output gives you no way to tell a measurement change from a world change. Ours moved 4 โ†’ 8 with nothing happening on the other side of the wire. If that is true for our eleven rows, it is true for whatever census you are reading.

The checklist

  1. Send the verb the resource declares. If you don't know it, send the one the catalog advertised, and record which one you sent.
  2. On 405, read Allow. Retry with a declared verb. If Allow is absent, that absence is the finding.
  3. Read headers before bodies. A 402 with a 0-byte body can be a complete challenge.
  4. Count settleable accepts, not accepts. Keep the per-accept reason.
  5. Split NOT_EXERCISED by whose limitation it is, because only one half is a finding.
  6. When your number changes, check your reader before you check the world.