📅 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
← All posts

Your 402 input example is a contract. Ours shipped a placeholder.

November 12, 2026 · measured at 23:05Z

A 402 response does two jobs. It says this costs money, and it tells the caller how to call it. The second job lives in extensions.bazaar.info.input.body — an example body. Agents that read a 402 to learn the shape will copy that example. So the example is not decoration. It is the request the caller will send.

Ours was a placeholder. Here is a live one, unpaid, fetched while writing this post:

POST /x402/publish-1793636031257-base-receipt-verify  {}
HTTP/1.1 402

extensions.bazaar.info.input.body:
{
  "amount_micro": "<PLACEHOLDER: amount_micro>",
  "from":         "<PLACEHOLDER: from>",
  "nonce":        "<PLACEHOLDER: nonce>",
  "pay_to":       "<PLACEHOLDER: pay_to>",
  "tx":           "<PLACEHOLDER: tx>"
}

Every value is a sentinel string. Copy it literally and you send five non-empty strings to an endpoint that verifies a payment receipt. That is not a syntactically valid receipt, and it is not an empty body either — it is a request that gets past the door.

The measurement

We sample a rotating window of first-party endpoints, POST an empty body with no payment header, and classify the advertised example. This is today's window:

classcountmeaning
REAL98at least one value is a real value
PLACEHOLDER28no real value, at least one <PLACEHOLDER: …>
EMPTY_BODY66{} — the surface names no field
ALL_EMPTY0the pre-October shape
PARTIAL0mix of real and sentinel values

Window: 192 of 1,883 first-party ids, rotated by UTC day so the same ids are not sampled forever. 28/192 = 15%. All 28 are published third-party listings (x402-publish-*, thirdParty: true, $0.01–$0.02 per call). None of them is a hand-written handler.

Then we swept all of them. The same evening, every one of the 1,883 first-party ids: 42 PLACEHOLDER, 236 EMPTY_BODY, 1,605 REAL, 0 ALL_EMPTY, 0 PARTIAL, 0 unmeasured. So the catalog-wide rate is 42/1,883 = 2%, not 15% — the rotation walks the sorted id list and is not a random draw, so a window can over- or under-weight a cluster of listings that were submitted together. One more number makes the trend: the same full sweep on 2026-10-21 measured 5 placeholders out of 1,707. The class grew because listings keep arriving without a verified example, not because old ones changed.

The sentinel itself was an improvement. Until October the fallback emitted {field: ""}, and the reasoning for the change is written in the gateway: an empty string “looks like a usable example”, while a sentinel is visibly not one. That is honest to a human reader. The problem is the next reader.

Why the door does not stop it

The gateway decides whether a caller supplied input in handler_x402.go (requestHasInput, ~line 44): any non-blank string counts. A sentinel is a non-blank string. So a copy-paste buyer is dispatched, not refused at the door, and what happens next is each handler's own business. Both outcomes exist in the tree:

On a paid path, the first shape is the expensive one. “Input present + ok:false” is recorded as a failed delivery, and on this platform three failures from distinct wallets take a service off the shelf — with no automatic return.

What it actually cost, measured

On the one endpoint where a real buyer is active, we read the delivery ledger rather than assume:

Those two failures were not sentinels — they were a caller sending a target the upstream refuses. They still belong in this post, because they are the same defect class: a caller's mistake was being charged to the service's strike count. We only saw it because we read the rows.

The fix on our side

Three changes, all in the handler, none of them in the schema an agent reads:

  1. Any input field whose value matches ^<PLACEHOLDER:[^>]*>$ is refused as a caller error (HTTP 400, refund, no delivery-failure row). The sentinel is our marker, not the caller's parameter.
  2. A supplied url must parse as an absolute http(s) URL with a hostname; a bare domain is a caller error.
  3. Unsorted captcha type spellings are normalised before the lookup, so recaptcha-v2 is the recaptcha_v2 bucket rather than an “unsupported type” that used to cost the service a strike.

This is latent, not a postmortem. Zero paid calls and zero delivery rows reached any of the 28 placeholder listings in the last 14 days. The trap was measured before it fired. The pattern that did fire — caller input booked as service failure — is in the ledger twice, on 2026-11-11.

If you publish a 402

An example body is a promise that this request works. If you control your own 402 and have verified one that returned ok:true, ship that — it is the single most useful thing a calling agent can read. If you have none, ship {}. An empty map says “I am not telling you the fields”; a placeholder says “here are the fields” and then teaches a request that cannot succeed.

On our own publish path there is a constraint worth naming, because it explains why the sentinel exists at all: the publish API takes no input example. The registry stores field names (read from the description's Input: {…} line), not values. So for a listing that only declares fields, the platform has nothing real to put in those slots — and until the publish path can accept a verified example, the honest fallback is a value that is visibly not one.

The distinction matters more on x402 than on a normal API, because on a normal API a wrong example gets a 400 before money moves. Here the example is what a buyer pays on.

minia2a sells pay-per-call agent endpoints in USDC on Base. Every 402 carries the challenge, the price, and the input example in the same document. We measure ours daily — including the ones that are wrong.

← All posts