Configuration Reference
tollbooth is configured via a single tollbooth.config.yaml file. All fields are documented below.
Full example
Section titled “Full example”gateway: port: 3000 discovery: true cors: true trustProxy: true
wallets: base: "0xYourBaseWallet" solana: "YourSolanaWallet"
accepts: - asset: USDC network: base - asset: USDC network: solana
defaults: price: "$0.001" timeout: 60 rateLimit: windowMs: 60000 max: 100
facilitator: https://x402.org/facilitator
upstreams: anthropic: url: "https://api.anthropic.com" headers: x-api-key: "${ANTHROPIC_API_KEY}" anthropic-version: "2023-06-01"
openai: url: "https://api.openai.com" headers: authorization: "Bearer ${OPENAI_API_KEY}"
routes: "POST /ai/claude": upstream: anthropic path: "/v1/messages" settlement: after-response match: - where: { body.model: "claude-haiku-*" } price: "$0.005" - where: { body.model: "claude-sonnet-*" } price: "$0.015" - where: { body.model: "claude-opus-*" } price: "$0.075" fallback: "$0.015"
"POST /v1/chat/completions": upstream: openai type: token-based models: gpt-4o: "$0.05" gpt-4o-mini: "$0.005"
hooks: onSettled: "hooks/log-payment.ts" onError: "hooks/handle-error.ts"gateway
Section titled “gateway”Top-level server configuration.
| Field | Type | Default | Description |
|---|---|---|---|
port | number | 3000 | Port the gateway listens on |
discovery | boolean | true | Expose /.well-known/x402 and /.well-known/openapi.json discovery endpoints |
hostname | string | — | Optional hostname to bind to |
cors | boolean | CorsConfig | false | Enable CORS headers. true uses permissive defaults; pass an object to configure origin, methods, and headers. See Security & Hardening |
trustProxy | boolean | false | Trust X-Forwarded-For and X-Forwarded-Proto headers from a reverse proxy. Enable when running behind Nginx, Caddy, or a cloud load balancer |
metrics | boolean | false | Expose a Prometheus-compatible metrics endpoint at /metrics |
gateway: port: 8080 discovery: true cors: true trustProxy: truewallets
Section titled “wallets”Maps network names to your wallet addresses. These are the addresses that receive payments.
| Field | Type | Description |
|---|---|---|
<network> | string | Wallet address for the given network |
wallets: base: "0xYourBaseWallet" base-sepolia: "0xYourTestnetWallet" solana: "YourSolanaWallet"The network names must match the networks in your accepts array.
accepts
Section titled “accepts”Array of payment methods the gateway accepts. Each entry specifies an asset and network combination.
| Field | Type | Description |
|---|---|---|
asset | string | Token symbol (e.g. USDC) |
network | string | Network name (e.g. base, base-sepolia, solana) |
accepts: - asset: USDC network: base - asset: USDC network: solanaRoutes can override accepted payments with a route-level accepts field.
defaults
Section titled “defaults”Default values applied to all routes unless overridden.
| Field | Type | Default | Description |
|---|---|---|---|
price | string | — | Default price for routes without an explicit price (e.g. "$0.001") |
timeout | number | 60 | Default payment timeout in seconds |
rateLimit | RateLimitConfig | — | Default rate-limit applied to all routes. Override per-route with routes.<route>.rateLimit |
defaults: price: "$0.001" timeout: 60 rateLimit: windowMs: 60000 max: 100RateLimitConfig fields:
| Field | Type | Description |
|---|---|---|
windowMs | number | Time window in milliseconds |
max | number | Maximum requests per window |
Prices are specified as dollar strings. "$0.01" = 10,000 USDC micro-units (6 decimals).
facilitator
Section titled “facilitator”URL of the x402 facilitator service that verifies and settles payments.
| Field | Type | Default |
|---|---|---|
facilitator | string | https://x402.org/facilitator |
facilitator: https://custom-facilitator.example.comCan be overridden per-route. Route-level facilitator takes precedence over this top-level setting. If neither is specified, defaults to https://x402.org/facilitator.
upstreams
Section titled “upstreams”Named upstream API configurations. Each upstream defines where requests are proxied to.
| Field | Type | Default | Description |
|---|---|---|---|
url | string | required | Base URL of the upstream API |
headers | Record<string, string> | — | Headers to inject into proxied requests |
timeout | number | — | Request timeout in seconds for this upstream |
openapi | string | — | URL or file path to an OpenAPI spec. Routes are auto-imported at startup. See OpenAPI integration |
defaultPrice | string | — | Default price applied to auto-imported routes (e.g. "$0.01"). Optional — when omitted, imported routes use defaults.price |
upstreams: anthropic: url: "https://api.anthropic.com" headers: x-api-key: "${ANTHROPIC_API_KEY}" anthropic-version: "2023-06-01" timeout: 120
dune: url: "https://api.dune.com/api" headers: x-dune-api-key: "${DUNE_API_KEY}"
myapi: url: "https://api.example.com" openapi: "https://api.example.com/openapi.json" # auto-import routes at startup defaultPrice: "$0.01" # applied to all imported routesEnvironment variable interpolation
Section titled “Environment variable interpolation”Header values support ${ENV_VAR} syntax. Variables are resolved from process.env at startup. This keeps secrets out of your config file.
headers: authorization: "Bearer ${API_KEY}"routes
Section titled “routes”The core of your config. Maps public-facing routes to upstream APIs with pricing.
Route keys use the format "METHOD /path":
routes: "GET /weather": upstream: myapi price: "$0.01"
"POST /ai/claude": upstream: anthropic path: "/v1/messages" price: "$0.015"
"GET /data/:query_id": upstream: dune path: "/v1/query/${params.query_id}/results" price: "$0.05"Route fields
Section titled “Route fields”| Field | Type | Default | Description |
|---|---|---|---|
upstream | string | required | Name of the upstream (must match a key in upstreams) |
path | string | same as route path | Path to forward to on the upstream. Supports ${params.*} interpolation |
price | string | { fn: string } | from defaults.price | Static price or path to a custom pricing function |
match | MatchRule[] | — | Array of conditional pricing rules (evaluated top-to-bottom) |
fallback | string | from defaults.price | Price when no match rule matches |
accepts | AcceptedPayment[] | from top-level accepts | Override accepted payments for this route |
payTo | string | PayToSplit[] | from wallets | Override payment recipient or configure split payments |
hooks | RouteHooksConfig | — | Per-route lifecycle hooks (override global hooks) |
metadata | Record<string, unknown> | — | Arbitrary metadata included in discovery responses |
facilitator | string | from top-level facilitator | Override the facilitator URL for this route |
settlement | "before-response" | "after-response" | "before-response" | When to settle payment relative to the upstream request. See Refund Protection |
rateLimit | RateLimitConfig | from defaults.rateLimit | Override rate-limit for this route |
verificationCache | VerificationCacheConfig | — | Cache payment verification results to reduce facilitator calls on repeated payments |
Free routes (no payment)
Section titled “Free routes (no payment)”Set price: "$0" to bypass the x402 payment flow entirely. The route acts as a transparent proxy — no 402 response, no payment required.
routes: "GET /health": upstream: myapi price: "$0" # free — proxied directly, no payment
"GET /data": upstream: myapi price: "$0.01" # paid — requires x402 paymentThis is useful for health checks, public listings, or mixed free/paid APIs where some endpoints should be openly accessible.
Path parameters
Section titled “Path parameters”Route paths support Express-style parameters with :param syntax:
routes: "GET /data/:query_id": upstream: dune path: "/v1/query/${params.query_id}/results" price: "$0.05"GET /data/12345 → proxied to https://api.dune.com/api/v1/query/12345/results
Use * as a catch-all wildcard that matches the rest of the path. Access the matched value with params["*"]:
routes: "GET /articles/*": upstream: blog price: fn: "./pricing/article-price.ts"Path rewriting
Section titled “Path rewriting”The path field lets your public API shape differ from the upstream:
routes: "POST /ai/claude": upstream: anthropic path: "/v1/messages" # upstream path differs from public path price: "$0.015"Token-Based Routes
Section titled “Token-Based Routes”For proxying OpenAI-compatible APIs (OpenAI, OpenRouter, LiteLLM, Ollama, etc.), set type: token-based to enable automatic model-based pricing without writing match rules. openai-compatible is also accepted as an alias.
The gateway auto-extracts the model field from the JSON request body and prices the request using a built-in table of common models (GPT-4o, Claude, Gemini, Llama, Mistral, DeepSeek, etc.).
Basic example
Section titled “Basic example”upstreams: openai: url: "https://api.openai.com/" headers: authorization: "Bearer ${OPENAI_API_KEY}"
routes: "POST /v1/chat/completions": upstream: openai type: token-based
"POST /v1/completions": upstream: openai type: token-basedOverride or extend model pricing
Section titled “Override or extend model pricing”Add a models map to set custom prices or add models not in the default table:
routes: "POST /v1/chat/completions": upstream: openai type: token-based models: gpt-4o: "$0.05" # override default gpt-4o-mini: "$0.005" # override default my-fine-tune: "$0.02" # custom model fallback: "$0.01" # price for models not in any tablePrice resolution order
Section titled “Price resolution order”When pricing a token-based request, the gateway checks:
models(your route overrides) — exact match- Built-in default table — exact match
price/fallback/defaults.price— standard fallback chain
Streaming support
Section titled “Streaming support”Streaming responses (SSE) work out of the box — the gateway preserves the ReadableStream without buffering.
Conditional pricing rules evaluated on each request. Rules are checked top-to-bottom; the first match wins.
Match rule fields
Section titled “Match rule fields”| Field | Type | Description |
|---|---|---|
where | Record<string, string | number | boolean> | Conditions to match against the request |
price | string | Price to charge when this rule matches |
payTo | string | PayToSplit[] | Optional payment recipient override for this rule |
where clauses
Section titled “where clauses”The where object matches against request properties:
| Prefix | Source | Example |
|---|---|---|
body.* | JSON request body | body.model: "claude-opus-*" |
query.* | URL query parameters | query.tier: "premium" |
headers.* | Request headers | headers.x-priority: "high" |
params.* | Path parameters | params.query_id: "123*" |
Values support glob patterns for flexible matching:
match: - where: { body.model: "claude-haiku-*" } price: "$0.005" - where: { body.model: "claude-sonnet-*" } price: "$0.015" - where: { body.model: "claude-opus-*" } price: "$0.075"fallback: "$0.015"Lifecycle hooks let you run custom code at key points in the request lifecycle. Hooks can be defined globally or per-route.
Global hooks
Section titled “Global hooks”hooks: onRequest: "hooks/on-request.ts" onPriceResolved: "hooks/log-price.ts" onSettled: "hooks/log-payment.ts" onResponse: "hooks/track-usage.ts" onError: "hooks/handle-error.ts"Per-route hooks
Section titled “Per-route hooks”Route-level hooks override global hooks for that route:
routes: "POST /ai/claude": upstream: anthropic price: "$0.015" hooks: onResponse: "hooks/track-claude-usage.ts"Hook lifecycle
Section titled “Hook lifecycle”| Hook | When | Signature | Use case |
|---|---|---|---|
onRequest | Before anything | (ctx: RequestHookContext) => Promise<HookResult | undefined> | Block abusers, rate limit |
onPriceResolved | After price is calculated | (ctx: HookContext) => Promise<HookResult | undefined> | Override or log pricing |
onSettled | After payment confirmed | (ctx: SettledHookContext) => Promise<HookResult | undefined> | Log payments to DB |
onResponse | After upstream responds | (ctx: ResponseHookContext) => Promise<UpstreamResponse | undefined> | Transform response, track usage |
onError | When upstream fails | (ctx: ErrorHookContext) => Promise<void> | Trigger refunds |
Hook return values
Section titled “Hook return values”Hooks that return HookResult can short-circuit the request:
export default async (ctx) => { if (isBlocked(ctx.req.headers['x-forwarded-for'])) { return { reject: true, status: 403, body: 'Blocked' }; } // return undefined to continue normally};The onResponse hook can return a modified UpstreamResponse to transform the response before it’s sent to the client.
Pricing format
Section titled “Pricing format”Prices are specified as dollar strings and converted to USDC micro-units (6 decimal places):
| Dollar string | USDC micro-units | Actual USDC |
|---|---|---|
"$0" | 0 | Free — skips x402 payment flow |
"$0.001" | 1,000 | 0.001 USDC |
"$0.01" | 10,000 | 0.01 USDC |
"$0.05" | 50,000 | 0.05 USDC |
"$1.00" | 1,000,000 | 1.00 USDC |
stores
Section titled “stores”Optional external store configuration for sharing state across gateway instances (e.g. rate-limit counters, verification cache).
| Field | Type | Default | Description |
|---|---|---|---|
redis | string | RedisConfig | — | Redis connection URL or config object. When set, rate-limit counters and verification cache use Redis instead of in-memory stores |
stores: redis: "redis://localhost:6379"When no store is configured, all state is kept in-memory and resets on restart.
Environment variables
Section titled “Environment variables”Any string value in the config supports ${ENV_VAR} interpolation:
wallets: base-sepolia: "${GATEWAY_ADDRESS}"
upstreams: myapi: url: "https://api.example.com" headers: authorization: "Bearer ${API_KEY}"Variables are resolved from process.env at config load time. Use a .env file or pass them directly.
Next: CLI Reference →