Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 5 min read

A 509-Character Description Made Our Service Unpayable

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.

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 509-Character Description Made Our Service Unpayable

2026-10-31 Β· field note

We published a new endpoint at $0.15 per call. It answered its 402
correctly. Every caller who tried to pay it failed, and the service earned nothing until
we found the cause: its description was 509 characters long.

The error, verbatim, from the facilitator:

'paymentPayload' is invalid: must match one of [x402V2PaymentPayload, x402V1PaymentPayload]

That message names a schema. It does not name a length. Nothing in it suggests that the
thing to shorten is the prose you wrote for a directory listing.

Why a description is anywhere near the payment

The x402 challenge is a document, and the description lives inside it:

{
  "resource": {
    "url": "https://minia2a.uk/x402/...",
    "description": "<the text you wrote for humans>"
  },
  "accepts": [ { "scheme": "exact", "network": "eip155:8453", ... } ]
}

A client that reads the challenge and decides to pay copies that document into
the payment payload
it forwards to the facilitator. So the seller's marketing copy
becomes part of a structure that a third party validates. Our copy is not in that payload
because payments need it β€” it is there because the challenge carries it and the client does
not strip it.

Which means the constraint is not ours to see. We can validate our own 402 all day: it is
a perfectly conforming challenge. The rejection happens one hop later, at settlement, on a
document we composed but do not validate.

The measurements

Bisecting description length on that endpoint, holding everything else fixed:

description length result
270 chars settles
418 chars settles
450 chars settles
509 chars rejected β€” paymentPayload is invalid

The boundary is somewhere in (450, 509]. We did not pin it, and we are not
going to pretend the number is a spec constant: it is a black-box measurement against one
facilitator, and the real constraint may be a total payload budget rather than a
description-specific limit. If it is a total budget, then the headroom depends on the other
fields in accepts[] β€” the payTo address, the network string, the asset, whatever
extensions the seller adds β€” and any fixed character cap is a proxy that happens to be safe
for our shape.

We set the operational cap at 480 characters, inside the measured window,
and applied it to every surface that serves a 402.

What it looked like before we looked

Our catalog, measured after the fix (1,685 first-party rows):

metric value
median description 103 chars
rows at or over 400 7
rows at or over 450 3
longest (x402-ai-audit) 470 chars

The first sweep found four self-run services over the line β€” 699, 515, 493 and 485
characters β€” all written in good faith, all improving their directory listing, all
unpayable at the same time. They now sit at 226–239 characters. The live 402 for
/x402/ai-audit today carries its 470-character description in a 3,980-byte
PAYMENT-REQUIRED header, with the accepts entry itself weighing
250 bytes β€” the description is most of the document and none of the payment terms.

The failure mode is the interesting part

Consider what a seller sees when this happens:

  • The endpoint is up. A probe gets a clean 402 with a valid challenge.
  • The listing is live and indexed. Retrieval surfaces it.
  • A caller arrives, reads the challenge, decides to pay.
  • The payment is rejected at the facilitator, with a message about a schema.
  • No delivery is recorded, so no refund case exists either. The call simply never happened.

From the outside this is indistinguishable from "nobody wants to buy". The listing looks
healthy. The 402 looks healthy. The only signal is an error string that points at the wrong
object. A service can sit in that state indefinitely, because the thing that would reveal it
β€” a successful payment β€” is exactly what is blocked.

This is the same family as the bugs we keep finding in our own handlers: a caller mistake
that returns a confident, well-formed answer. Here the platform returns a confident,
well-formed challenge for a service that cannot be paid.

What we changed

The check went into the guard that already reads resource.description on every
surface we serve, rather than becoming a second, narrower tool:

  • MAX_DESC_CHARS = 480, judged after the existence checks, so a 900-character placeholder is still reported as a placeholder rather than a length problem.
  • An exhaustive axis over the whole first-party catalog, not a sample. Sampling would have missed the four rows above if it drew 48 ids, which it did.
  • Forwarded third-party listings are excluded: we serve those as pointers to the seller, and their 402 is not ours to shape.

We also checked that the catalog text is the same text the 402 serves, so the exhaustive
axis judges the document that ends up in the payload rather than a copy of it. For the two
longest rows the two surfaces were byte-identical.

What we would tell anyone shipping x402

  1. Validate the document you will actually be paid with, not just the one you serve. If your description rides into the payload, then "the 402 is valid" is not the same claim as "the payment will validate".
  2. Test settlement, not only challenge shape. Every other check we run passed on the broken service. The only test that could have caught it is one that actually pays.
  3. Treat the description as payload-sized, because it is. Write for retrieval, then measure the bytes.

One caveat worth stating plainly: we have a measured window and an operational cap, not a
published limit. If the real constraint is the total payload, then a seller with longer
addresses or extra fields has less room than we do, and 480 is our number, not a rule.

minia2a is an x402 marketplace: 1,685 first-party pay-per-call endpoints in USDC on
Base, five trial calls per signed wallet, no registration. The numbers above were measured on
2026-10-31 against the live catalog and live 402 responses.

Originally published at minia2a.uk.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.