2026-10-27 ยท engineering note
This is a note about two numbers that describe the same catalog and disagree by two orders of magnitude in usefulness, and about a third number that was used to argue against fixing it โ a number that was off by six times because it was measured in the wrong place.
The catalog is 1,769 pay-per-call endpoints across 25 categories. Two machine-readable surfaces describe it:
| Surface | What it declares | Coverage |
|---|---|---|
| OpenAPI document | 1,774 paths / 3,544 operations | 3,538 carry x-payment-info |
| DNS discovery manifest | resources | 81 |
The first is nearly complete. Each priced operation carries an extension object naming the
protocol, the network (eip155:8453), the asset, and the price in both USDC and cents.
A consumer that understands that extension can enumerate essentially the whole catalog from one
fetch.
The second is the path the DNS draft defines: a TXT record points at a manifest, and the
manifest's resources list is the only field that says "this URL is payment-gated".
Unknown root keys are ignored by design, for forward compatibility โ so a field you invent, or a
sibling document you point at from an unfamiliar key, is not read by anyone who implemented the
spec. That surface listed 81 URLs: 4.6% of the catalog.
Both statements are true at once, and the gap between them is not a listing bug โ it is what happens when one surface is generated and the other is curated by hand while the catalog keeps growing.
Before the size argument, we had a different argument: we believed the OpenAPI document carried no payment metadata at all. That belief came from a scan we ran ourselves, which reported zero matches across all 1,774 paths.
The scan was wrong, and the way it was wrong is worth writing down. It iterated the path map and
then inspected the keys of each path entry โ which are method names (get,
post), not the operation objects that hold the extensions. One level too shallow, zero
matches, and a confident conclusion built on top of it.
for path, entry in doc["paths"].items():
for key in entry: # 'get', 'post' โ method names
... # the extension lives one level deeper
A scan that reports zero has two possible readings: the thing is absent, or the scan is looking somewhere else. The second is the more common one, and it produces exactly the same output. We now require a positive control โ a key we know exists in the same corpus, found by the same walker โ before a zero from a scanner is allowed to become a claim.
With the OpenAPI path covered, the remaining question was whether to expand
resources to the whole eligible catalog. The objection was size: about
357.7 KB.
That is a real number. It is what json.dumps produced with default separators. It is
also not the cost of anything, because nobody transfers json.dumps output. They
transfer gzip:
1,683 entries / 25 categories plain JSON : 285,022 B gzip : 58,335 B
The document that was "too big" is 58 KB on the wire. And the comparison that ends the argument is sitting next to it: the catalog index agents already fetch from us โ measured at roughly 960 requests a day from crawlers and clients โ is 101 KB gzip. The expansion that sounded expensive is 42% cheaper than an artefact the same clients pull all day without complaint.
The OpenAPI document makes the same point louder. It is 5.5 MB uncompressed. On the wire it is 188 KB, served hundreds of times a day. A file size is a property of your serialiser's indent setting. A wire size is a property of your server.
Put a ladder on the decision, because "all of it" is not the only option:
| Scope | Entries | gzip |
|---|---|---|
| today | 81 | 6.7 KB |
| 10 per category | 180 | 7.4 KB |
| 20 per category | 296 | 11.5 KB |
| 50 per category | 526 | 20.7 KB |
| everything eligible | 1,683 | 58.3 KB |
Every row is measured on the same corpus with the same encoder, not extrapolated. Going from 180 to 1,683 entries costs 51 KB. That is the actual trade.
The catalog is 1,769; the eligible set is 1,683. The 86 that drop out are third-party listings whose forward URL is the seller's host, not ours. The discovery draft is explicit: a resource URL must be HTTPS and on the manifest's own domain or a subdomain, and a consumer must not dereference anything that is not โ an off-domain entry is treated as an abuse signal. Listing a seller's URL would not advertise their service; it would teach every conforming reader to distrust the document.
Which is why the exclusion count gets printed rather than subtracted. A list that grows from 81 to 1,683 is easy to trust. A list that quietly drops 86 entries without saying which, or why, is the kind of number that surfaces in a support thread six months later.
Every entry in resources is a promise, and our own guard dereferences all of them
daily to check the promise still holds โ each must answer with a payment challenge. At 81 entries
that is 81 requests a day. At 1,683 it is 1,683 requests a day against our own origin.
Cheap, but not free, and it is the cost a byte-count argument never sees. A manifest is not passive documentation. It is a test plan that runs every day, and its length is also the length of that plan.
A byte count of a file is not the cost of serving it. The number that matters crossed a wire, and almost every document you serve crosses it compressed. Before you call something too big, encode it the way your clients receive it.
And before you build an argument on a scanner that printed zero, find one thing you know is there and make the scanner print that. If it cannot, the zero was never a measurement.
Status, stated plainly: measured, and a projection tool written that reads the catalog and never writes the manifest. Not yet published โ the number above is the reason it can be.
minia2a is a pay-per-call API marketplace for agents: x402 challenges, USDC on Base, no accounts. The docs describe the payment path, including the five signed trial calls each wallet gets before the first 402.