---
title: What a 402 Should Say — NanoParse Blog
url: https://nanoparse.app/blog/2026-10-26-what-a-402-should-say
description: A parse endpoint answers
---

A parse endpoint answers "no" more often than it answers "yes", and the answer a client gets when it is refused is the most-read document the service publishes. Most services spend that moment on a single sentence — pay, or leave — and then wonder why the traffic that reaches the wall does not convert.

This is the record of one 402 that was wrong, how the wrongness was measured, and the six rules the replacement follows. None of it is specific to this API.

## The wall as it stood

An unauthenticated `POST https://nanoparse.app/fetch`, with the free allowance spent, returned this:

```

{
"error": "payment_required",
"message": "This IP has used 3 of 3 free calls. Send $0.005 USDC on Base per parse to continue.",
"payment_required": { "x402Version": 2, "accepts": [ ... ] },
"error_code": "free_tier_exhausted",
"docs_url": "https://nanoparse.app/quickstart",
"free_calls_used": 3,
"free_calls_limit": 3
}
```

Read it as a developer with no wallet to hand. Two doors are named: pay, or stop. Both are honest, and the message is not lying about anything — which is exactly what made it hard to see.

The service also granted ten free parses a month to a wallet that signs a claim message. The claim was implemented, tested, and documented in the source; the string to sign was public by design. It was not mentioned on the landing page's wallet FAQ, not on `/quickstart` (the word "claim" appeared zero times on that page), and not in the 402. A caller who had spent three anonymous probes and hit the wall was told to pay, by a service that would have given them ten more pages for one signature.

That is a specific kind of defect: the copy was accurate about every mechanism it named and silent about the one the caller could still use for free.

## The count that motivated the fix, and why it was partly wrong

The first read of the funnel said 176 payment-path events against one settlement on a single day. That number looked like 175 people being told to pay and leaving.

Thirty days of the payment log said something else. Of 1,533 wall events, every single one asked for `example.com`. The three busiest client identities produced 70% of them with request gaps of exactly 1,800 seconds, ~5,410 seconds and irregular thousands — cron cadences, not people. All twelve settlements in the window were the service's own paid canary.

So the copy fix was correct and the demand story that motivated it was wrong. That is worth saying plainly, because the reverse — a wrong fix justified by a right story — is how a product ends up with a checkout nobody walks into.

## Six rules for the refusal

1. Name the limit in numbers. "This IP has used 10 of 10 free calls" is checkable. "Free tier exhausted" is not, and an agent cannot act on it.
2. Name every door that is still open, including the free one. If a caller can get more for nothing, say so before naming the price. A wall that only names the price is a wall that only converts people who already decided to buy.
3. Put the next step in the body, machine-readably. The claim block is now a field:

```

"free_tier_claim": {
"free_parses_per_month": 10,
"message_to_sign": "nanoparse.app free tier: sign this message to claim 10 free parses (no payment).",
"rest_headers": { "X-Nanoparse-Wallet": "0x<your address>", "X-Nanoparse-Signature": "0x<65-byte r||s||v>" },
"mcp_args": { "wallet_address": "0x...", "wallet_signature": "0x..." },
"docs_url": "https://nanoparse.app/quickstart#free-tier"
}
```

An agent that can read that field can unstick itself. An agent that reads only prose cannot.

1. Never report a count that reads like a bug. The counter can exceed an allowance that was lowered after it accrued, which produced "This IP has used 10 of 3 free calls" on live traffic. Clamp what is displayed; never clamp the decision.
2. Do not decorate the payment object. The x402 `PaymentRequired` structure is parsed strictly by validators that decide whether a listing exists. The claim block rides *beside* it — as extra top-level JSON on REST, as a second content block on MCP — never inside it.
3. A malformed credential is not an outage. Verifying a signature is arithmetic, and the arithmetic library throws on out-of-range scalars. An unhandled throw escaped the free-tier check and the route's catch-all answered 503 "Free-tier quota unavailable — retry shortly" for a signature of zeros. A client typo was reported as a server outage. An unverifiable claim is simply not a claim: return false and let the request fall through to the anonymous bucket.

## What the wall says now

```

{
"message": "This IP has used 10 of 10 free calls. Claim 10 free parses a month with one wallet signature (no payment, no account): https://nanoparse.app/quickstart#free-tier Or send $0.005 USDC on Base per parse."
}
```

Longer, and every clause earns its place: the limit, the free door, the cost of the paid door, the exact string to sign, both transports, and the anchor that explains it.

## The rule under the rules

A refusal is a service's only chance to be useful to someone who is not yet a customer. Spend it naming what is still possible, in the smallest number of words that a machine and a tired developer can both act on — and then check the thing you just promised, end to end, with a throwaway key. The signature test that found the 503 took four minutes and one ephemeral wallet. It was the only test that would have found it.
