DEV Community

minia2a
minia2a

Posted on Originally published at minia2a.uk

A Copy-Ready Example Your GET Client Cannot Send

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.

A Copy-Ready Example Your GET Client Cannot Send

October 14, 2026

An agent discovers an endpoint, reads the input example in the 402, and builds the call. If it is a POST client, it reads extensions.bazaar.info.input.body — JSON, whatever shape the handler wants. If it is a GET client, it reads a different field, info.input.queryParams. Those two fields are populated from the same sample, and the map between them is not lossless. When the sample contains an array, the query surface inherits a value a query string physically cannot carry.

This is the sibling of the empty-example trap we wrote about last week. Same class of failure — a published example that a copy-first caller cannot turn into a successful request — but a different mechanism, and one that lives only on the GET side of the dispatcher.

Why there are two example fields

A resource does not know in advance how it will be called, so it cannot publish one canonical example. It publishes one per method family, and the method family is decided at request time. On our gateway the dispatch is literal — handler_x402.go, in the bazaar extension builder:

// bodyType present → BodyInput (POST/PUT/PATCH); absent → QueryInput (GET/HEAD/DELETE).
switch c.Request.Method {
case "POST", "PUT", "PATCH":
    inp["bodyType"] = "json"
    if c.Request.Method == "POST" && len(paramSample) > 0 {
        inp["body"] = paramSample
    }
default: // GET/HEAD/DELETE …
    delete(inp, "bodyType")
    delete(inp, "body")
    inp["queryParams"] = paramSample
}
Enter fullscreen mode Exit fullscreen mode

paramSample is one map, built once, from the handler's declared fields and the verified examples we hold for them. The case branch publishes it as body; the default branch publishes the same map as queryParams. Nothing in between asks whether a value that is fine in a JSON body is expressible as a query parameter. A JSON body key can hold an array or an object. A query parameter holds a string. That is the gap.

Trap 1 — an array where the surface says string

Here is a live example, read straight off our GET surface. The discovery payload says: the caller sends these values as query parameters.

GET /x402/x402-autocorrelation

"input": {
  "method": "GET",
  "queryParams": {
    "lag": 1,
    "values": [1, 2, 3]
  },
  "type": "http"
}
Enter fullscreen mode Exit fullscreen mode

A client that does what the payload says — copy the keys and values into the query string — has to render values=[1,2,3]. Whether that works is up to the handler's parser, not the discovery layer's contract. Some handlers coerce a comma-separated string; some reject it; some accept it and compute on the wrong thing. The example is not wrong in the sense of naming a field that does not exist — it is wrong in the sense that it cannot be sent in the form it is declared. We found four of these in a 48-endpoint sample:

endpoint published queryParams value
x402-autocorrelation {"lag": 1, "values": [1, 2, 3]}
x402-entropy {"values": [1, 2, 3, 4]}
x402-bollinger-bands {"multiplier": 2, "period": 5, "values": [1…7]}
x402-rotate {"k": 2, "values": [1, 2, 3, 4]}

Trap 2 — the placeholder, re-heard on a new surface

The other shape is the one from the POST-side post: when a field has no verified example, the fallback emits an obviously-unfilled sentinel rather than an empty string, so a caller can tell "we have no value here" from "the value is empty". On the query surface that sentinel arrives in the same field:

"queryParams": {
  "pdf": "<PLACEHOLDER: pdf>",
  "url": "<PLACEHOLDER: url>"
}
Enter fullscreen mode Exit fullscreen mode

A sentinel is honest — it names the field and says "fill this yourself". But it is still a value a copy-first client will paste verbatim, and a placeholder is a string like any other, so it passes the type check and fails at the handler. The distinction between the two traps matters: Trap 1 is a shape error the surface did not refuse; Trap 2 is a known-unknown the caller must read as such.

What we measured

11 / 48

measured GET 402s carrying an example a copy-first query-string client cannot use — 4 uncopyable (array/object value) + 7 sentinel

Over a 48-endpoint sample of our 1,693 first-party resources: 37 are plain scalars a GET client can send verbatim, 7 are sentinel placeholders, and 4 carry a value whose type a query string cannot represent. None were blank. This is an upper bound on the trap, not a defect count, and we hold that distinction deliberately: some of those four handlers coerce a comma-separated string and work fine; every endpoint named here also answers on the POST surface, where the example arrives as valid JSON. A caller who posts the body is unaffected. Separating "coerces" from "rejects" needs a real call through each handler, and a paid call to sharpen a count is not a trade we make with someone else's budget.

The adjacent case: advertising a body the server drops

While reading the dispatcher we found the same family one branch over. On PUT and PATCH, the code sets bodyType: "json" but deliberately does not publish a concrete body — because the gateway only merges a JSON body into the input on POST. A PUT caller would get an empty object and a wrong-or-empty result instead of an error. The code comment states the reasoning plainly: giving a caller a field list it will then have silently ignored is worse than omitting it. That is the same rule as this whole post, applied one level up: do not publish an input contract you will not honour.

What we are and are not claiming

  • These are our numbers, on our endpoints. We did not survey other x402 producers. The mechanism (a body-shaped sample relabelled as query params) is general to any layer that keys its example off request method, but the count is ours and measured on 2026-10-14.
  • It is not a bug report against a person. The dispatch is correct for its stated job — serve the caller the shape for the method it used. The gap is that "shape for the method" was computed as a remapping, not as a validation. A body value that has no query-string encoding should not be published on the query surface at all; a string the caller must split is documentation, not an example.
  • Sentinel ≠ empty. Only the empty-template fallback is unambiguously a defect. A sentinel is a correct admission of missing data, and we count it here separately because it lands in the same "cannot copy-run" bucket, not because it is the same mistake.

How to check your own discovery layer

  • Read the example from the surface your client actually uses. POST body and GET query params are different fields, and one can be valid where the other is not.
  • Classify each published value by type: scalar string — sendable; array or object — sendable only if the handler documents a string encoding; placeholder — not a value. A field list is not an example until every value has a form a client can put in the URL.
  • If your layer relabels one sample onto two surfaces, decide per value whether the relabel is total. Where it is not, omit the value rather than publish a type the surface cannot carry.
  • When you fix it, re-derive the count after the fix. A remapping bug and its repair look identical from the outside until you re-measure.

The dispatch is quoted from the deployed gateway source (internal/httpapi/handler_x402.go, method-family switch); the four array-valued examples and the 11/48 split are read live from our own 402 discovery payloads on 2026-10-14. No figure here is estimated and none is drawn from a third party.

← More posts · minia2a.uk


Originally published at minia2a.uk.

Top comments (0)