Proxy (any language)
Charge for a FastAPI, Python MCP, Go, or Rails service without changing its code — put a Tollstile gateway in front of it.
@tollstile/proxy is a paid gateway. It prices HTTP routes and MCP tools before they reach your service, forwards paid requests unchanged, and settles on your service's answer. The service needs no Tollstile code, so it can be written in any language.
agent ──► tollstile-proxy ──► your service (private address)
402 · verify · settle · receiptConfigure
import { defineProxyConfig } from "@tollstile/proxy";
import { createTollstile, memoryLedger, testRail, upTo } from "tollstile";
export default defineProxyConfig({
toll: createTollstile({ rails: [testRail()], ledger: memoryLedger() }),
upstream: "http://127.0.0.1:8000",
mcp: { path: "/mcp" },
routes: [
{ method: "GET", path: "/weather", price: "$0.01" },
{ method: "POST", path: "/summarize", price: upTo("$0.50") },
{ tool: "generate_image", price: "$0.04" },
],
});npm install tollstile @tollstile/proxy
npx tollstile-proxy --config tollstile.proxy.mjs --port 8402Routes take the same options as toll.price(): access, require, flow, commit, resource. Paths support :param segments and a trailing *. Requests no route prices are forwarded free, or refused with unmatched: "deny".
Your service
A FastAPI example with no payment code except two optional headers:
@app.get("/weather")
def weather(request: Request):
return {"forecast": "clear", "paid_by": request.headers.get("tollstile-payer")}
@app.post("/summarize")
async def summarize(request: Request, response: Response):
words = len((await request.body()).split())
response.headers["tollstile-fulfill-amount"] = f"${words / 1000:.3f}" # upTo: what this call used
return {"words": words}MCP servers built with the MCP Python SDK work as they are: the proxy reads tools/call on the MCP endpoint, and every other message passes through.
| Header | Direction | Meaning |
|---|---|---|
Tollstile-Payment-Via, Tollstile-Payer, Tollstile-Charge-Id, Tollstile-Amount | to your service | Who paid, the charge, and the amount. Client-sent tollstile-* headers are removed first |
Tollstile-Fulfill-Amount | from your service | On upTo() routes, the amount to charge (at most the maximum). Stripped before the client sees the response |
Tollstile-Result-Ref | from your service | Where the result is stored, returned to idempotent retries |
How it decides
| Upstream answer | Charge | Client gets |
|---|---|---|
HTTP status below 400 (tools: a result without isError) | settled before the response is sent | the response, with the rail's receipt |
400 or above, isError, or an invalid Tollstile-Fulfill-Amount | released | the response, no receipt |
No answer within upstreamTimeoutMs | released | 502 upstream_unavailable |
| Settlement rejected by the provider | failed | a fresh 402, the upstream response withheld |
Unpaid MCP tool calls get a payment-required tool result — x402's PaymentRequired when an x402 rail is configured, otherwise Tollstile's denial in _meta["tollstile/payment-required"]. Paid results carry the receipt in _meta, in JSON and SSE responses.
Security
Only the proxy may reach your service
Bind the service to 127.0.0.1 or a private network. Anything that reaches it directly skips payment and can forge the Tollstile-* headers.
- Paths are matched and forwarded in canonical form, so percent-encoding, repeated slashes, or trailing slashes cannot route around a price.
- Priced tools inside JSON-RPC batches, and MCP bodies over
maxMcpBodyBytes, are refused. Idempotency-Keyand_meta["tollstile/idempotency-key"]work as everywhere: a retried paid request is never paid or forwarded twice.- MCP tool responses are buffered to add the receipt, so progress notifications arrive with the result. HTTP bodies stream after settlement.
Run anywhere
createProxy() returns a Web-standard handler, so the gateway also runs on Cloudflare Workers, Deno, and Bun. On Node, @tollstile/proxy/node exports serve(handler, { port }).
Verification status
Tested with unit tests and end to end in front of a real FastAPI app with the MCP Python SDK 2.2. Not yet run in production or on Workers. Full example: examples/proxy-python.