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

terminal
$ 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:

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.

src/index.ts — complete, runnable
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

terminal
$ npx wrangler dev --local --port 8789

4 · Get a key, roll the dice

terminal
$ 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:

the 402 of MCP
{"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

terminal
$ 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
src/index.ts — the only change
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.