Skip to content

Refund Protection

By default, tollbooth settles payment before proxying to the upstream API. This is fast, but it means the client pays even if the upstream returns an error. With after-response settlement, tollbooth defers settlement until the upstream responds — and only charges if the response is successful.

tollbooth supports two settlement strategies:

ModeWhen payment settlesDefault?
before-responseBefore the upstream is calledYes
after-responseAfter the upstream responds successfullyNo
Client → Tollbooth → Facilitator (settle) → Upstream → Client

Payment is settled as soon as the signature is verified. The upstream request happens after. If the upstream fails, the payment has already been collected.

Client → Tollbooth → Facilitator (verify) → Upstream → Facilitator (settle) → Client

The facilitator verifies the payment signature upfront but doesn’t settle on-chain until the upstream responds. If the upstream fails, the payment is never settled and the client keeps their funds.

before-responseafter-response
LatencyLower — single facilitator round-tripHigher — two facilitator round-trips
Refund riskClient pays even on upstream failureClient only pays on success
Best forFast, reliable upstreamsExpensive calls, unreliable upstreams
ComplexitySimpleRequires deciding what “success” means

Set settlement: after-response on any route:

routes:
"POST /ai/claude":
upstream: anthropic
path: "/v1/messages"
price: "$0.075"
settlement: after-response

You can mix modes — some routes settle before, others after:

routes:
"GET /weather":
upstream: weather
price: "$0.001"
# default: before-response (fast, cheap, reliable upstream)
"POST /ai/claude":
upstream: anthropic
path: "/v1/messages"
price: "$0.075"
settlement: after-response # expensive call, protect the client

When using after-response, tollbooth decides whether to settle based on the upstream’s HTTP status code:

Status codeSettles?Reason
2xxYesSuccessful response
3xxYesRedirect (upstream handled the request)
4xxYesClient error (not the upstream’s fault)
5xxNoServer error — upstream failed
Timeout / no responseNoUpstream unreachable

In short: the client is only protected from upstream failures (5xx and timeouts). Client-side errors like 400 Bad Request still settle because the upstream processed the request correctly.

The default rules work for most cases, but you can override them with an onResponse hook for full control over what settles:

routes:
"POST /ai/claude":
upstream: anthropic
path: "/v1/messages"
price: "$0.075"
settlement: after-response
hooks:
onResponse: "hooks/refund-policy.ts"

The hook receives the upstream response and can prevent settlement by returning { settle: false }:

hooks/refund-policy.ts
import type { ResponseHookContext } from "x402-tollbooth";
export default async (ctx: ResponseHookContext) => {
const { status, body } = ctx.response;
// Don't settle on any error
if (status >= 400) {
return { settle: false };
}
// Don't settle if the model returned an empty response
if (!body?.content?.length || body.content[0]?.text?.length === 0) {
return { settle: false };
}
// Settle normally
return { settle: true };
};
FieldTypeDescription
ctx.response.statusnumberUpstream HTTP status code
ctx.response.bodyunknownParsed response body (if JSON)
ctx.response.headersRecord<string, string>Upstream response headers
ctx.req.bodyunknownOriginal request body
ctx.routeRouteConfigThe matched route configuration
ctx.paymentPaymentInfoPayment details (amount, payer, etc.)
hooks/refund-on-rate-limit.ts
export default async (ctx) => {
// Anthropic returns 429 when rate-limited
if (ctx.response.status === 429) {
return { settle: false };
}
return { settle: true };
};
hooks/refund-empty-completion.ts
export default async (ctx) => {
const body = ctx.response.body as
| { choices?: unknown[]; content?: unknown[] }
| undefined;
// OpenAI-style: check if choices array is empty
if (body?.choices?.length === 0) {
return { settle: false };
}
// Anthropic-style: check if content is empty
if (body?.content?.length === 0) {
return { settle: false };
}
return { settle: true };
};

Next: Configuration Reference →