An agent that meets https://nanoparse.app/fetch is one POST away from a charge. After ten free parses, that POST answers 402 and the caller either signs a payment or walks away. Everything needed to make that decision is available before the point of payment, from unauthenticated GETs that cost nothing and need no key: the price, the asset, the network, the receiving address, the free-tier size, the failure contract, and the product's own list of things it will not do.
That property is worth stating because it is not universal. A paid endpoint can put its price behind a login, describe itself only on a marketing page, or answer a machine with a human HTML page. This one publishes a machine-readable surface, and the whole surface can be fetched and read before the first cent. Here it is, captured 2026-10-04, with the byte counts.
probe status bytes
GET /.well-known/x402 200 392
GET /openapi.json 200 3983
GET /llms.txt 200 9446
GET /robots.txt 200 430
GET /health 200 102
GET /sitemap.xml 200 864
GET /.well-known/security.txt 200 186
GET /ops-canary 200 38
GET /status 400 26 (no jobId)
GET /status/account 400 91 (no address)
GET /.well-known/agent-card.json 404 14
Eleven reads, no key, no wallet, no account. What follows is what they answer.
The 402 is the last gate, not the first
An unauthenticated call — a GET, a body-less POST, or a POST with no Payment-Signature header — answers HTTP 402. The response is the quote: what it costs, what token, which chain, and where the money goes. Captured live today:
$ curl -sD - https://nanoparse.app/fetch
HTTP/2 402
pay-to: ethereum:base:0x57a1d06873db575baeae1f6af30862a75c7bd570
payment-required: <base64 x402 v2 PaymentRequired>
{"error":"payment_required",
"payment_required":{
"x402Version":2,
"accepts":[{"scheme":"exact","network":"eip155:8453",
"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount":"5000",
"payTo":"0x57a1d06873db575baeae1f6af30862a75c7bd570",
"maxTimeoutSeconds":60}]},
"catalog":[{"id":"nanoparse_fetch","price_usd":"0.005","price_unit":"per parse",
"free_tier":"10 free parses per network (subnet)"}]}
Four fields carry the weight: amount and asset say how much of what, network says where, and payTo says to whom. The same terms travel base64-encoded in the PAYMENT-REQUIRED header, and the recipient is repeated as a CAIP-style pay-to header. CORS exposes Content-Type, Payment-Signature, Authorization on this route, so a browser-side agent can read the response too.
One detail is easy to miss: the 402 also carries an extensions.bazaar block with an input/output schema for the call. The payment-required response doubles as a machine description of how to call the endpoint — so even the refusal is informative.
The price is stated in prose four times. Twice as a field.
Four documents print the per-parse price in a sentence: the info.description and the x-guidance extension inside openapi.json, the instructions string in /.well-known/x402, and the pricing rows in llms.txt. Prose is fine for a human and a poor contract for a client, because four sentences are four chances to drift.
Two of the machine surfaces state it as a value instead. openapi.json carries it as a field:
"x-payment-info": {
"price": { "mode": "fixed", "currency": "USD", "amount": "$0.005" },
"protocols": [{ "x402": {} }]
}
and the live 402 carries the same number in catalog[].price_usd, as "$0.005".
A client that needs the price should read x-payment-info.price from the spec, or catalog[].price_usd from the 402, and leave the sentences alone. Those two fields are the same figure the site quotes: $0.005 per parse, with no mode other than fixed — no tier to select and no format surcharge to discover later.
Two encodings of the same number
The decimal in the spec is not the number that settles. accepts[].amount above is "5000" — base units, six decimals for USDC, so 5000 / 10^6 is the dollar figure. The deployed source derives both from one constant:
// pricing.ts — a price change touches only this file
export const PRICE_USDC_AMOUNT = String(Math.round(parseFloat(PRICE_USD) * 1e6));
export const X402_AMOUNT = PRICE_USDC_AMOUNT;
So the pattern for a client is: read the spec for the quote in dollars, read the 402 for the amount in base units, and build the payment from the base units. Format with 6 decimals for display; sign the integer.
Verify the recipient before you sign
/.well-known/x402 publishes the address the endpoint claims:
$ curl -s https://nanoparse.app/.well-known/x402 | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["resources"], d["ownershipProofs"])'
['https://nanoparse.app/fetch'] ['0x57a1d06873db575baeae1f6af30862a75c7bd570']
The payTo in the 402 above is the same address. Before signing, compare them:
terms = json.loads(base64.b64decode(resp.headers["PAYMENT-REQUIRED"]))
accept = terms["accepts"][0]
disc = json.loads(urlopen("https://nanoparse.app/.well-known/x402").read())
if accept["payTo"].lower() not in [p.lower() for p in disc["ownershipProofs"]]:
raise SystemExit("recipient not published by the endpoint")
What that check buys is consistency, not identity. The discovery file and the 402 are two documents from the same origin; matching them catches a recipient substituted between the two, not a deployment that never belonged to anyone. It is still the cheapest pre-signature check available, and it costs one more GET.
The spec and the runtime disagree on one word
The 202 schema in openapi.json enumerates job status as queued, processing, completed, error. The endpoint that reports job status answers a different terminal word. From the deployed /status route:
if (rawData.startsWith("Error: ")) {
const { code, detail } = parseErrorBlob(rawData);
return c.json({
status: "failed",
error_code: code,
message: detail.message,
}, 200, { ...corsHeaders });
}
On the lookup queue the same word appears: if (parsed?.error) return c.json({ status: "failed", ... }). It is failed — never error.
A poller written against the spec's enum, stopping when status == "error", never terminates on a failed render. The correct reading of the two documents together: completed is the only success, failed is terminal, and anything else means poll again. queued deserves a note as well — it appears in the POST response, and /status does not return it, so the enum folds two responses into one list.
Both statements are checkable without spending anything. GET /status?jobId=9f2c1a3b-4b5d-4e6f-8a9b-0c1d2e3f4a5b answers {"status":"processing"} for a job that was never issued, and a non-UUID answers 400 {"error":"invalid jobId"}, so the poll path refuses noise and the terminal words are the runtime's.
What the free reads rule out before you pay
llms.txt carries a section titled "When NOT to Use NanoParse", in the endpoint's own words:
- "Bulk scraping at scale — NanoParse is designed for per-parse agent workflows, not mass crawling."
- "Raw HTML or screenshots — NanoParse returns Markdown only."
- "Paywalled or auth-gated content — NanoParse respects paywalls and login walls. It will not bypass subscription barriers."
- "Persistent storage of results — Results are cached for up to 6 hours (R2) for performance, then discarded."
A product that publishes its own refusal list tells an agent where not to route work before any work is paid for. robots.txt does the same for crawlers: it allows GPTBot, ChatGPT-User, Claude-Web, anthropic-ai, Google-Extended, CCBot, Applebot, and PerplexityBot by name, and points its Sitemap at both /sitemap.xml and /llms.txt. /.well-known/security.txt gives a contact and a policy URL. GET /health returns {"ok":true,…,"x402_supported":true}, and GET /ops-canary returns {"ok":true,"last_heartbeat_min_ago":2} — a liveness heartbeat an agent can read before it builds a call.
What the surface does not have
Two conventions are absent. /.well-known/agent-card.json answers 404 Not Found, and so does /.well-known/ai-plugin.json. There is no agent card here; discovery is /.well-known/x402 plus openapi.json plus llms.txt. An agent looking for a card should fall back to those three rather than assume the endpoint is undiscoverable.
A second gap sits closer to home: a prose example can lag the schema it describes. llms.txt shows a 402 body as a flat object with price, token, and recipient at the top level. The live body returned a payment_required envelope with an accepts[] array, and openapi.json defines exactly that shape. When an example and a schema disagree, the schema is what the endpoint was built against — read it for the shape, and treat the example as documentation of intent.
A first-call checklist
The whole decision is a handful of reads. This is the order that spends nothing:
BASE=https://nanoparse.app
#1 is this an x402 resource, and which address does it publish?
curl -s $BASE/.well-known/x402
#2 the machine price and the request contract
curl -s $BASE/openapi.json | python3 -c 'import json,sys; p=json.load(sys.stdin)["paths"]["/fetch"]["post"]; print(p["x-payment-info"]["price"]); print(sorted(p["requestBody"]["content"]["application/json"]["schema"]["properties"]))'
#3 what it will not do, in its own words
curl -s $BASE/llms.txt | sed -n '/## When NOT to Use/,/^## /p'
#4 alive?
curl -s $BASE/health
After that, one POST maps the rest. It either succeeds inside the free ten, or it answers 402 with the quote above. Polling GET /status?jobId=… is free and unauthenticated, and it never settles a payment, so a client can watch a job to completion without spending anything.
The paid half is one number
Everything above resolves to a single line: a rendered page costs $0.005 USDC on Base, paid per parse, with the fifteen Litmus signals included rather than billed as a second call. Ten free parses per network come first, with no account and no key. Refused reads — SSRF-restricted hosts, blocked pages, paywalled pages, a render that comes back too thin — are rejected before settlement, so a failure is never charged.
That is the shape of the deal, and it is invertible: the free half is the contract — the price, the recipient, the failure words, the limits — and the paid half is the same number wherever it appears. An agent can read the first without spending and decide about the second.
Documentation and the quickstart live at nanoparse.app. The agent-facing description is nanoparse.app/llms.txt, the machine spec is nanoparse.app/openapi.json, and the built-in MCP endpoint is nanoparse.app/mcp — one URL, no install, no key.