What arrives before the parse is the question a caller can answer for free.
A parse is a purchase. The decision in front of every call is not whether a page is interesting, it is whether the call will settle — because a caller that signs first and finds out second has spent a round trip, a nonce and a wallet signature learning something a single GET would have said outright.
NanoParse answers that at a URL that needs no key, no signature and no payment. The whole surface is one unauthenticated read:
GET https://nanoparse.app/status/account?address=0x000000000000000000000000000000000000dEaD
{"wallet_address":"0x000000000000000000000000000000000000dead",
"usdc_balance":"25919.18",
"usdc_balance_raw":"25919180000",
"balance_error":null,
"balance_known":true,
"estimated_parses_remaining":5183836,
"price_per_parse":"0.005",
"quota":{"free_calls_used":10,"free_calls_limit":3,"is_free_available":false},
"next_call_will_succeed":true,
"funding_uri":"ethereum:0x57a1d06873db575baeae1f6af30862a75c7bd570@8453?token=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"docs_url":"https://nanoparse.app/quickstart"}
That address is a burn address — a wallet nobody controls, queried here to keep the example neutral. The check works for any address on the chain, because a balance is public data. Nothing above requires an account, and nothing above is billed.
Every payload in this post was captured from the live endpoint on 2026-10-16 and is committed beside it, along with the requests that produced it.
The ladder before the answer
Two of the three calls are refusals, and both are cheap enough to read as documentation. A missing address and a malformed one answer the same way, HTTP 400 with a body that names the field:
GET /status/account -> 400
{"error":"invalid_address",
"message":"Provide a valid Ethereum address via ?address=0x..."}
GET /status/account?address=0x123 -> 400
{"error":"invalid_address",
"message":"Provide a valid Ethereum address via ?address=0x..."}
GET /status/account?address=0x9f3a7c1b2d4e5f60718293a4b5c6d7e8f9012345 -> 200
{"usdc_balance":"0.00", "usdc_balance_raw":"0",
"balance_known":true, "estimated_parses_remaining":0,
"next_call_will_succeed":false, ...}
The forty-hex-character test is the gate, and it is the only one. The address is echoed back lowercased: a checksummed input is accepted and normalised, which matters if a caller stores addresses the way a wallet displays them and then compares them to what came back.
The response also carries the full discovery header set that every other route carries — rel="describedby" pointing at /llms.txt, the MCP server card, the api-catalog linkset, openapi.json as service-desc, /quickstart as service-doc — so a client that follows headers lands on the same documentation it would have found from the front door. That header surface was walked in The Free Half; the subject here is the body.
The eleven fields
The payload is short on purpose. Each field answers one question a caller would otherwise have to guess at:
wallet_address— the address the numbers belong to, lowercased.usdc_balance— the balance in decimal USDC, as a string, so no float rounding enters the contract.usdc_balance_raw— the same balance in base units,25919180000against25919.18, which is the integer a settlement path actually moves.balance_errorandbalance_known— whether the balance read succeeded at all.price_per_parse— the price the endpoint would charge on the next call.estimated_parses_remaining— the balance divided by that price.quota—free_calls_used,free_calls_limit,is_free_available.next_call_will_succeed— the single boolean the whole check exists to produce.funding_uri— an EIP-681 URI naming the chain, the recipient and the token contract, ready to hand to a wallet.docs_url— the quickstart.
Two of those fields are the interesting ones, and neither of them is the balance.
The arithmetic is one line of source
estimated_parses_remaining is not a projection from a dashboard. It is a division, and it is one line of the deployed source:
const estimatedParses = balanceKnown && balanceNum > 0
? Math.floor(balanceNum / pricePerParse)
: 0;
The example above holds 25,919.18 USDC, the price is $0.005 per parse, and the payload says 5,183,836 parses — the floor of the quotient, and nothing else. The second line is the verdict:
next_call_will_succeed: quota.is_free_available
|| (balanceKnown && balanceNum >= pricePerParse),
So next_call_will_succeed is true in exactly two situations: the free tier is still open, or the balance covers one parse at the current price. An exhausted free tier plus an empty wallet is the only way to see false, which is what the zero-balance probe above returns. There is no in-between and no soft warning — a caller that reads this field never has to parse prose about insufficient funds.
The price is one constant in pricing.ts and the payload echoes it verbatim, so a price change appears in the check at the same time it appears in the charge.
Unknown is not zero
The distinction the payload spends two fields on is the one that matters most in production: a balance read that fails is not a balance of zero.
The rule is written into the deployed source as a fix, with the reason attached:
// P1 fix 2026-09-17: never present a failed read as a $0 wallet (see
// fetchUSDCBalance) — expose whether the balance is actually known.
const balanceKnown = !balanceError;
When the Base RPC read fails, balance_known goes false, balance_error carries the reason, and estimated_parses_remaining reports 0 — but next_call_will_succeed stays false rather than pretending the wallet is empty, because the field that drives the decision is guarded by balanceKnown on the same line. A caller reading only estimated_parses_remaining would conclude bankruptcy; a caller reading the pair concludes unknown, and retries the check.
The quota read follows the same instinct in the other direction. It fails open:
// P2 fix 2026-08-23: fail-open — report 0 used rather than 500 the check.
A reporting endpoint that returns 500 because a KV read timed out is worse than a reporting endpoint that says less than it knows.
Whose quota is in the payload
The quota block describes the caller, not the address in the query string. Three different addresses were queried from the same host in the run behind this post and all three returned the identical quota object, because the bucket is resolved from the request's own network — a fact worth stating plainly, since an agent checking a treasury wallet might reasonably assume the quota block travels with the wallet.
The bucket is resolved read-only, by design:
// P2 fix 2026-08-23: never mint buckets from a reporting endpoint. Resolve
// the device bucket plus the wallet bucket read-only (register=false) — any
// caller could send a Payment-Signature-shaped header and displace
// legitimate wallets otherwise.
The limit printed beside the used count is chosen by one branch, and the branch is what makes the payload readable:
const bucketLimit = bucketKey.startsWith("free:wallet:")
? FREE_CALLS
: ANON_FREE_CALLS;
In the captured payload above, that produced free_calls_used: 10 against free_calls_limit: 3 — the used count read from one bucket, the ceiling printed from the anonymous branch. The two numbers come from different places and they are published here exactly as they arrived. The canonical free-tier sentence lives on /quickstart and in /llms.txt: ten free parses per month with a signed wallet, or three anonymous free tries per network. This check is about the wallet; read the free tier from the surfaces that define it.
The check is also rate limited, because each one can fire up to four Base RPC requests:
if (!(await checkIPRateLimit(c, "status-account", 30, 3600))) {
return c.json({ error: "rate_limited",
message: "Too many account checks from this IP — try again later." }, 429);
}
Thirty checks per hour per IP. A pre-flight before a batch costs one of them, not one per URL.
The same answer over MCP
An MCP client does not have to know the HTTP route. The endpoint's tool list carries two tools, and the second one is this check:
nanoparse_status(address: string)
"Check wallet balance, USDC balance, quota state, and whether the next
/fetch call will succeed without payment. Use before spending — tells
the agent exactly how much to fund."
Called with an address, it returns a rendered sentence plus the same structured object:
Wallet: 0x000000000000000000000000000000000000dead
USDC Balance: $25919.18
Price per parse: $0.005
Estimated parses from balance: 5183836
Free tier (this network, try it): 10/3 used — EXHAUSTED
Free tier (wallet): not registered — sign the free-tier claim or settle a paid parse to register
Next call will succeed: YES
To fund: send USDC on Base to 0x57a1d06873db575baeae1f6af30862a75c7bd570
EIP-681: ethereum:0x57a1d06873db575baeae1f6af30862a75c7bd570@8453?token=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Its structuredContent carries two fields the HTTP route does not: the quota block is split into a device bucket and a wallet bucket, and the wallet bucket reports null until a wallet signs the free-tier claim or settles a paid parse. That is the difference between the network a request arrives from and the wallet doing the paying, and it is the only place either is visible separately.
A malformed address is refused at the protocol layer rather than as a tool result: {"code":-32602,"message":"Missing or invalid address parameter. Provide a valid Ethereum address (0x...)."}.
What a caller can do with one boolean
The value of a pre-flight is that it turns a policy into a branch. Reading next_call_will_succeed before a batch is four lines of shell:
WALLET=0x9f3a7c1b2d4e5f60718293a4b5c6d7e8f9012345
STATE=$(curl -s "https://nanoparse.app/status/account?address=$WALLET")
echo "$STATE" | jq -r '.next_call_will_succeed'
echo "$STATE" | jq -r '.estimated_parses_remaining'
if [ "$(echo "$STATE" | jq -r '.next_call_will_succeed')" != "true" ]; then
echo "$STATE" | jq -r '.funding_uri' # hand this to a wallet
fi
A funded wallet returns a parse count before the first call rather than after the last one, and usdc_balance_raw lets a settlement path that works in base units compare against the number the chain will actually move instead of a rounded decimal.
One thing the check does not cover
/openapi.json describes a single path: /fetch. The balance check is not in it. The route is reachable from /llms.txt, which names the tool and its argument, and from the MCP tool list, which is where an MCP client finds it — but an agent that discovers the API by reading the OpenAPI document never sees that a free money check exists. That is worth knowing before assuming a caller has read the whole surface: the spec is the narrowest of the three documents.
What the check cannot answer is whether the page a caller wants is readable. That verdict belongs to the render, and it arrives after the decision — a refusal returns a named reason and settles nothing, which is the contract walked in What Survives the Convert. The pre-flight answers the money question, and answers it in a form a program can act on: known or unknown, affordable or not, before a signature exists.