Skip to content

Configuration Reference

tollbooth is configured via a single tollbooth.config.yaml file. All fields are documented below.

tollbooth.config.yaml
gateway:
port: 3000
discovery: true
wallets:
base: "0xYourBaseWallet"
solana: "YourSolanaWallet"
accepts:
- asset: USDC
network: base
- asset: USDC
network: solana
defaults:
price: "$0.001"
timeout: 60
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"
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: openai-compatible
models:
gpt-4o: "$0.05"
gpt-4o-mini: "$0.005"
hooks:
onSettled: "hooks/log-payment.ts"
onError: "hooks/handle-error.ts"

Top-level server configuration.

FieldTypeDefaultDescription
portnumber3000Port the gateway listens on
discoverybooleantrueExpose /.well-known/x402 discovery endpoint
hostnamestringOptional hostname to bind to
gateway:
port: 8080
discovery: true

Maps network names to your wallet addresses. These are the addresses that receive payments.

FieldTypeDescription
<network>stringWallet address for the given network
wallets:
base: "0xYourBaseWallet"
base-sepolia: "0xYourTestnetWallet"
solana: "YourSolanaWallet"

The network names must match the networks in your accepts array.


Array of payment methods the gateway accepts. Each entry specifies an asset and network combination.

FieldTypeDescription
assetstringToken symbol (e.g. USDC)
networkstringNetwork name (e.g. base, base-sepolia, solana)
accepts:
- asset: USDC
network: base
- asset: USDC
network: solana

Routes can override accepted payments with a route-level accepts field.


Default values applied to all routes unless overridden.

FieldTypeDefaultDescription
pricestringDefault price for routes without an explicit price (e.g. "$0.001")
timeoutnumber60Default payment timeout in seconds
defaults:
price: "$0.001"
timeout: 60

Prices are specified as dollar strings. "$0.01" = 10,000 USDC micro-units (6 decimals).


URL of the x402 facilitator service that verifies and settles payments.

FieldTypeDefault
facilitatorstringhttps://x402.org/facilitator
facilitator: https://custom-facilitator.example.com

Can be overridden per-route. Route-level facilitator takes precedence over this top-level setting. If neither is specified, defaults to https://x402.org/facilitator.


Named upstream API configurations. Each upstream defines where requests are proxied to.

FieldTypeDefaultDescription
urlstringrequiredBase URL of the upstream API
headersRecord<string, string>Headers to inject into proxied requests
timeoutnumberRequest timeout in seconds for this upstream
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}"

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}"

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"
FieldTypeDefaultDescription
upstreamstringrequiredName of the upstream (must match a key in upstreams)
pathstringsame as route pathPath to forward to on the upstream. Supports ${params.*} interpolation
pricestring | { fn: string }from defaults.priceStatic price or path to a custom pricing function
matchMatchRule[]Array of conditional pricing rules (evaluated top-to-bottom)
fallbackstringfrom defaults.pricePrice when no match rule matches
acceptsAcceptedPayment[]from top-level acceptsOverride accepted payments for this route
payTostring | PayToSplit[]from walletsOverride payment recipient or configure split payments
hooksRouteHooksConfigPer-route lifecycle hooks (override global hooks)
metadataRecord<string, unknown>Arbitrary metadata included in discovery responses
facilitatorstringfrom top-level facilitatorOverride the facilitator URL for this route

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

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"

For proxying OpenAI-compatible APIs (OpenAI, OpenRouter, LiteLLM, Ollama, etc.), set type: openai-compatible to enable automatic model-based pricing without writing match rules.

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.).

upstreams:
openai:
url: "https://api.openai.com/"
headers:
authorization: "Bearer ${OPENAI_API_KEY}"
routes:
"POST /v1/chat/completions":
upstream: openai
type: openai-compatible
"POST /v1/completions":
upstream: openai
type: openai-compatible

Add a models map to set custom prices or add models not in the default table:

routes:
"POST /v1/chat/completions":
upstream: openai
type: openai-compatible
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 table

When pricing an OpenAI-compatible request, the gateway checks:

  1. models (your route overrides) — exact match
  2. Built-in default table — exact match
  3. price / fallback / defaults.price — standard fallback chain

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.

FieldTypeDescription
whereRecord<string, string | number | boolean>Conditions to match against the request
pricestringPrice to charge when this rule matches
payTostring | PayToSplit[]Optional payment recipient override for this rule

The where object matches against request properties:

PrefixSourceExample
body.*JSON request bodybody.model: "claude-opus-*"
query.*URL query parametersquery.tier: "premium"
headers.*Request headersheaders.x-priority: "high"
params.*Path parametersparams.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.

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"

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"
HookWhenSignatureUse case
onRequestBefore anything(ctx: RequestHookContext) => Promise<HookResult | undefined>Block abusers, rate limit
onPriceResolvedAfter price is calculated(ctx: HookContext) => Promise<HookResult | undefined>Override or log pricing
onSettledAfter payment confirmed(ctx: SettledHookContext) => Promise<HookResult | undefined>Log payments to DB
onResponseAfter upstream responds(ctx: ResponseHookContext) => Promise<UpstreamResponse | undefined>Transform response, track usage
onErrorWhen upstream fails(ctx: ErrorHookContext) => Promise<void>Trigger refunds

Hooks that return HookResult can short-circuit the request:

hooks/on-request.ts
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.


Prices are specified as dollar strings and converted to USDC micro-units (6 decimal places):

Dollar stringUSDC micro-unitsActual USDC
"$0.001"1,0000.001 USDC
"$0.01"10,0000.01 USDC
"$0.05"50,0000.05 USDC
"$1.00"1,000,0001.00 USDC

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 →