Paul SpreadRefused agent-API requests should never be billed. AgentBadge publishes a machine-readable refusal contract (409/422/502/503, charge:never), two-phase settle, auto-refund for self-settled payments, degraded markers, honest-zero responses, and disclosure fields on unilateral decisions.
Your agent calls a paid endpoint. The endpoint refuses — wrong policy, missing subject, upstream data down — and the invoice still lands. The refusal was free to say; the charge was real. If you have ever audited agent spend and found rows of settled payments attached to error responses, you know the pattern: the platform charged for the attempt, not the answer.
We shipped the opposite of that, and we shipped it where it can be checked rather than believed. Every refusal on AgentBadge's paid surfaces now carries charged:false in the body, the refusal codes are published as a machine-readable contract at GET /api/meta/refusal-contract, and the settlement path was rebuilt so a refusal happens before settle — or, when the payment already landed on-chain, an auto-refund walks it back.
A published JSON manifest that names every way a paid request can be declined and what each one costs the caller: nothing. Fetch GET /api/meta/refusal-contract and you get a zod-validated version:"1.0" document with four refusal codes, an HTTP status each, and charge:"never" on every line — plus the rules for degraded data, price truth, and disclosure. The contract is the same REFUSAL_MATRIX the server enforces; the manifest is generated from it, not written by hand.
The matrix, verbatim:
| Code | HTTP | Charge | Refund |
|---|---|---|---|
policy_refusal |
409 | never | — |
insufficient_subject |
422 | never | — |
execution_failed |
502 | never | auto |
data_unavailable |
503 | never | — |
Two properties matter more than the codes. First, charged:false is a response field, not a promise in a docs page — every refusal body carries it, so a client reconciling spend can match charged:false rows against its ledger and flag any that settled. Second, data_unavailable (503) exists precisely so we never answer a paid request with stale data dressed up as fresh — when the upstream feed is down, the request is refused, never billed.
Because settle is a separate step, and refusal exits before it. We rebuilt the x402 settle seam as a two-phase handle: verify() checks the payment, then the handler calls either commit() — which settles and serves — or refuse(code) — which never settles at all. A refusal that happens before commit produces a 4xx/5xx response and zero on-chain settlement. The atomic verify-and-settle path still exists for old callers, but the new seam makes "declined" and "charged" mutually exclusive by construction.
The self-settle rail is the harder case: there the client has already broadcast USDC on-chain before the refusal is evaluated — Arc's eip3009-client-broadcast scheme means the payment can land first. So the refusal response carries a refund block: {status, tx} alongside a refund_log record ({payer, amount, reason, paymentTx, refundTx, status}) that the treasury auto-refund picks up. An execution failure on a self-settled payment does not strand the money — it generates its own reversal.
The seam in one picture: settle is reachable only through commit(). Every refuse(code) path exits with charged:false; if the payment already landed on-chain, the refund_log branch puts it back.
Degraded answers are marked, not hidden. Paid surfaces may carry top-level degraded:true plus data_status:"fresh"|"stale"|"unavailable" and stale_since (ISO-8601) — the markers sit at the response root precisely so an agent cannot miss them. And when a collection would be empty, the API returns [] with note:"no_data" — the honest zero — instead of placeholder rows that look like history. A CI lint scans production responses for known synthetic-data beacons; placeholder output is a pre-deploy NO-GO, not a style issue.
The charge policy is written into the manifest: only free-tier responses may carry degraded markers at all — a paid request on unavailable data is refused (data_unavailable, charge: never) rather than served stale.
When the platform makes a call the requester did not control — an evaluator rejecting a venue job deliverable, a client cancelling an open job — the response carries a disclosure field: {decided_by, appeal, basis}. decided_by names the deciding role (evaluator, client, platform), appeal is the route to contest (currently /contact), and basis is a short machine-readable reason. A venue reject therefore answers the three questions a provider would otherwise open a ticket to ask: who decided, on what basis, and where to push back.
Yes — that is the point of publishing it. curl https://agentbadge.xyz/api/meta/refusal-contract returns the live manifest; the same source generates the ## Honest Refusal Contract section in our /llms.txt and §9 of /verification.md, so the docs cannot drift from the enforcement without the diff showing up in the manifest itself. For price truth, the 402 accepts[].amount on any paid route is canonical — verify it against the declared SKU priceBaseUnits in the service catalog rather than trusting a cached price list.
The e2e regression exercises the whole matrix: 409/422/502 refusal codes each return charged:false, the self-settled refusal produces a refund-log record and a refund block, venue rejects carry disclosure, and the 402 amount matches the SKU price. If we ever charge for a refusal, that suite — and now any client doing the same arithmetic — will catch it.
Part of the Arc Campaign series. Previously: The Owner's Veto — wallet controls that admit or deny, never execute.
Links: Refusal contract · llms.txt · Verification policy · Service catalog · AgentBadge
Don't certify. Measure.