Quickstart · ~15 minutes
Meter your first MCP server.
A fresh Cloudflare Worker with one tool, wrapped by Tollgate: free credits, metering, and a payment-required error you can trigger from your terminal. The free tier in this walkthrough is 3 credits on purpose — you'll see the whole money loop in your first minute of calls.
1 · New project
$ mkdir paid-mcp && cd paid-mcp $ npm init -y $ npm install @tollgate/sdk $ npm install -D wrangler typescript
Until the package hits npm, install the tarball instead:
npm install ../tollgate/tollgate-sdk-0.1.0.tgz (built with npm pack).
2 · The server
Two files. First wrangler.jsonc:
{
"name": "paid-mcp",
"main": "src/index.ts",
"compatibility_date": "2026-05-01"
}
Then src/index.ts — a one-tool MCP server, metered. MemoryStore keeps this
walkthrough zero-setup; step 6 swaps in D1.
import { Tollgate, MemoryStore, rpcResult, rpcError, jsonResponse, isNotification, METHOD_NOT_FOUND, } from "@tollgate/sdk"; // Module scope: one meter per isolate. Use D1Store in production. const tollgate = new Tollgate({ store: new MemoryStore(), freeCredits: 3, // tiny on purpose — see the 402 fast signupUrl: "http://localhost:8789/signup", checkoutUrl: "http://localhost:8789/topup", pricingHint: "$9 per 1,000 calls", }); const TOOLS = [{ name: "roll_dice", description: "Rolls n six-sided dice. Costs 1 credit.", inputSchema: { type: "object", properties: { n: { type: "number" } } }, }]; function handle(message: any) { if (isNotification(message)) return null; switch (message.method) { case "initialize": return rpcResult(message.id, { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "paid-dice", version: "0.1.0" }, }); case "ping": return rpcResult(message.id, {}); case "tools/list": return rpcResult(message.id, { tools: TOOLS }); case "tools/call": { const n = Math.min(message.params?.arguments?.n ?? 1, 10); const rolls = Array.from({ length: n }, () => 1 + Math.floor(Math.random() * 6)); return rpcResult(message.id, { content: [{ type: "text", text: JSON.stringify({ rolls }) }], }); } default: return rpcError(message.id, METHOD_NOT_FOUND, "no such method"); } } export default { async fetch(req: Request): Promise<Response> { const url = new URL(req.url); // Self-serve keys: email in, key out. Rate-limit this in production. if (url.pathname === "/signup" && req.method === "POST") { const { email } = await req.json() as { email?: string }; const r = await tollgate.signup(email ?? ""); return jsonResponse(r.ok ? { api_key: r.rawKey, credits: r.freeCredits } : r, r.ok ? 200 : 400); } // The actual gate: one wrapper, metering included. if (url.pathname === "/mcp") { return tollgate.protect(req, async ({ message }) => { const out = message ? handle(message) : null; return out ? jsonResponse(out) : new Response(null, { status: 202 }); }); } return jsonResponse({ error: "not found" }, 404); }, };
3 · Run it
$ npx wrangler dev --local --port 8789
4 · Get a key, roll the dice
$ curl -s localhost:8789/signup -d '{"email":"me@example.com"}' {"api_key":"tg_…","credits":3} $ curl -s localhost:8789/mcp \ -H "authorization: Bearer tg_…" -H "content-type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"roll_dice","arguments":{"n":3}}}' {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"rolls\":[4,2,6]}"}]}}
5 · Hit the paywall
Call it three more times. The fourth metered call answers:
{"jsonrpc":"2.0","id":1,"error":{
"code":-31402,
"message":"Out of credits (0 remaining). Buy more at http://localhost:8789/topup ($9 per 1,000 calls)",
"data":{"reason":"insufficient_credits","credits_remaining":0,"checkout_url":"http://localhost:8789/topup"}}}
That error is your business model: agents read the message, tooling reads
data.checkout_url. Wire the checkout with a billing driver —
with no keys configured you get the mock driver, with LEMONSQUEEZY_API_KEY or
STRIPE_SECRET_KEY set the same code sells real credit blocks.
6 · Production storage
$ npx wrangler d1 create paid-mcp # add the d1_databases binding to wrangler.jsonc as DB $ npx wrangler d1 execute paid-mcp \ --file=node_modules/@tollgate/sdk/schema.sql
import { Tollgate, D1Store } from "@tollgate/sdk"; // inside fetch(req, env): const tollgate = new Tollgate({ store: new D1Store(env.DB), freeCredits: 200, … });
Everything else — webhooks, dashboards, per-tool prices — is in the SDK reference.