Tollstile

Errors

Machine-readable denials — a stable code, whether retrying helps, and what to do next.

Public Beta · early access

Core denials use a common body shape, so an agent can recover without reading prose. Configuration errors and unexpected exceptions are separate TollstileError failures; adapters may rethrow them to the framework.

{
  "error": {
    "code": "insufficient_authorization",
    "retryable": true,
    "action": "pay",
    "message": "The payment authorizes $0.03; this request costs $0.04.",
    "detail": null
  },
  "resource": "POST /v1/translate",
  "required": "$0.04",
  "authorized": "$0.03"
}

A payment challenge carries price, variable, quote, nonce, expiresAt, and accepts. A requirement denial with status 402 has the error envelope but no payment offers. A settlement rejection after the handler consumed the body can also lack a fresh quote; request a new challenge before constructing a new proof.

FieldUse
codeStable. Branch on it.
retryableWhether the same logical request can still succeed
actionpay: pay using accepts in this response. retry_later: send the same request with the same payment and idempotency key after Retry-After. fix_request: change the request. stop: do not retry.
messageFor humans; may change
detailThe rail's or requirement's own reason, such as transfer_mismatch. Log it; do not branch on it.

Codes

codeStatusactionWhen
payment_required402payNo payment
quote_required402payThe price is computed per request and the payment carried no quote
quote_invalid402payThe quote is forged, expired, or for another resource
quote_mismatch402payThe request differs from the one the quote priced
quote_offer_missing402payThe quote has no offer for the rail used
proof_invalid402payThe rail rejected the payment; detail says why
insufficient_authorization402payThe payment authorizes less than the price; required, authorized
authorization_expired402payThe payment authorization expired
payment_rejected402payThe provider rejected an upfront payment
settlement_rejected402payThe provider rejected settlement after the handler; the output was withheld
requirement_failed402 · 403 · 429pay · stop · retry_laterA requirement refused; requirement, detail
access_denied403stopNo access policy admits the caller
proof_already_used409stopA single-use payment was already used by another request
request_in_progress409retry_laterSame idempotency key; the first attempt is still running
already_paid409stopSame idempotency key; the request was already paid; chargeId, settlement, result
idempotency_key_reused422fix_requestSame idempotency key, different request
invalid_request400fix_requestMalformed idempotency key or MCP envelope
payment_unavailable503retry_laterA provider needed to verify or challenge is down
payment_outcome_unknown503retry_laterSettlement outcome unknown; retry with the same payment and key
requirement_unavailable503retry_laterA requirement cannot check its evidence right now

Denials include Cache-Control: no-store. Only action: "retry_later" includes Retry-After (including retryable 429), and the body carries the same wait as retryAfter in seconds, for transports that have no headers. The wait is spread around five seconds rather than fixed, so a hundred clients denied at the same instant do not all come back at the same one; a client should add its own backoff on top, and never retry a denial whose action is not retry_later. already_paid and proof_already_used have action: "stop" and no retry header. On MCP, tool-result denials carry the body in _meta["tollstile/payment-required"]; MPP challenge errors use the protocol-specific JSON-RPC shape described in MCP.

An agent's loop

const response = await fetch(url, { headers });
if (response.ok) return response;

const { error, accepts } = await response.json();
switch (error.action) {
  case "pay":         return payAndRetry(accepts);            // new payment, same Idempotency-Key
  case "retry_later": return retryAfter(response.headers.get("retry-after"));
  case "fix_request":
  case "stop":        throw new Error(`${error.code}: ${error.message}`);
}

On this page