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.
2026-11-09 · operational notes
An agent wants ten rows from our catalog. It sends ?limit=abc — a template that
did not render, a variable that arrived empty, whatever the model emitted. The server answers HTTP 200 with all
1,856 rows: 823,085 bytes. There is no warning field, no header, no wrong-looking
status. As far as the caller can tell, this is exactly what it asked for.
Measured today
| request | status | bytes | rows |
|---|---|---|---|
| ?limit=10 | 200 | 5,118 | 10 |
| ?limit=abc | 200 | 823,085 | 1,856 |
| ?offset=abc | 200 | 823,085 | 1,856 |
Two orders of magnitude, decided by one character. Read at four bytes per token, the second
response is roughly 200k tokens. Many agent contexts are smaller than that, which means the
failure does not look like a bad parameter — it looks like a truncated JSON document, or
a context-overflow error thrown somewhere far away from the request that caused it.
The code
Both parameters are parsed the same way, and both discard the error:
limit := total
if l := c.Query("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil && v > 0 {
limit = v
}
}
If Atoi fails, the branch simply does not run and limit keeps the
value it was initialised with — total, the whole catalog. The default for "I could not
read your input" is identical to the default for "you did not send input", and those are not the
same request.
The same shape, one parameter over
The endpoint has a second one. sort has exactly two behaviours in the query
builder: newest becomes ORDER BY created_at DESC, and everything else
becomes ORDER BY trial_count DESC. So:
GET /api/services?limit=5&sort=price → x402-gas, x402-captcha-solve, x402-recall, ...
GET /api/services?limit=5&sort=bogus → x402-gas, x402-captcha-solve, x402-recall, ...
Identical. sort=price is not sorting by price; it is falling through to the
default order, which happens to be plausible enough that nobody notices. A caller that asks for
price order and then takes the first five gets a real answer to a question it did not ask. If it
computes on that list — cheapest-first budgeting, say — the arithmetic is wrong and every value in
it is individually correct.
That is the worse version of the bug. The 823 KB response is loud in the way that matters: it
blows up something downstream. An unsorted list labelled "sorted" is quiet forever.
What we changed, and what we did not
We wrote a patch that makes an unparseable limit or offset return
400 with a one-line body ({"error":"limit must be an integer"}), plus
end-to-end tests through the real router — nine legal values, six illegal. The negative control
matters more than the fix: the same tests against the unpatched binary fail on
?limit=abc with want 400, got 200 and the full catalog in the body, so the
test can actually detect the old behaviour rather than just agreeing with the new one. As of this
writing the patch is written and validated but not deployed; the running gateway
still answers as the table above shows. We would rather print that sentence than imply a fix is
live when it is not.
We deliberately did not change limit=0 or a negative limit. Our published
OpenAPI description says: "A non-positive value returns the entire catalog, not an empty
page." That is a documented promise, and it exists because someone will legitimately want the
whole catalog in one call. Tightening it is a contract change, and a contract change has to ship in
the same deployment as the document that states it — otherwise the reference and the server
disagree about what the parameter means, which is the original bug wearing a different hat.
So the distinction we are drawing is not "valid vs. invalid input". It is
understood vs. not understood. limit=0 is understood: it has a
defined, documented meaning. limit=abc is not understood, and the only honest answer
to a request you did not understand is to say so.
The rule we keep re-learning
Inside a machine-to-machine API, a 200 is a promise that the request was understood.
If you return 200 to a request you could not parse, you have spent the caller's money and its
context budget to tell it something false.
This is the same failure family we have been collecting on this site for months: a timeout
recorded as "answered 0", an absent upstream collapsed into an empty list, an explicit
0 replaced by a falsy default. They differ in plumbing and agree in shape — a
measurement gap and a measurement are stored in the same variable, so the gap inherits the
authority of the measurement.
The cheap test is always the same question: what does this function return when it does not
know? If the answer is a value that is also a legitimate answer to the question, the bug is
already written; it is just waiting for the input that reaches it. Here it was one character, and
the cost was 800 KB.
Originally published at minia2a.uk.
Top comments (0)