DEV Community

Roman V
Roman V

Posted on • Originally published at dev.to

Monetize any MCP server in 10 minutes — no billing code required

You built an MCP server. It works. AI agents call it. But you're paying for compute and API calls out of pocket.

Adding billing to an MCP server usually means: Stripe integration, API key management, usage tracking, free tier logic, subscription management, webhook handling. That's 2–4 weeks of work that has nothing to do with the tool you actually built. Most operators give up and leave money on the table.

MCP Billing Gateway solves this. It's a reverse proxy that sits in front of your MCP server and handles all billing — Stripe subscriptions, per-call credits, and x402 crypto payments. Your server stays unchanged.

Architecture

AI Agent (caller)
    │
    ├─ Authorization: Bearer cgwcl_...
    │
    ▼
┌──────────────────────────┐
│   mcp-billing-gateway    │
│                          │
│  1. Authenticate caller  │
│  2. Check credits/sub    │
│  3. Debit payment        │
│  4. Forward JSON-RPC     │
│  5. Record usage         │
│  6. Return response      │
└──────────┬───────────────┘
           │
           ▼
┌──────────────────────────┐
│  Your MCP Server         │
│  (unchanged, untouched)  │
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Your MCP server never sees billing logic. If a caller has no credits, they get a 402 Payment Required before your server is even contacted.

Step 1: The MCP server (2 minutes)

Here's a minimal MCP server with two text-analysis tools. Plain Express, no frameworks:

// server.js
import express from "express";

const app = express();
app.use(express.json());

const tools = {
  word_count: {
    description: "Count words, characters, and sentences in text",
    inputSchema: {
      type: "object",
      properties: { text: { type: "string" } },
      required: ["text"],
    },
    handler({ text }) {
      return {
        words: text.split(/\s+/).filter(Boolean).length,
        characters: text.length,
        sentences: text.split(/[.!?]+/).filter(Boolean).length,
      };
    },
  },
  text_transform: {
    description: "Transform text: uppercase, lowercase, titlecase, reverse",
    inputSchema: {
      type: "object",
      properties: {
        text: { type: "string" },
        mode: { type: "string", enum: ["uppercase", "lowercase", "titlecase", "reverse"] },
      },
      required: ["text", "mode"],
    },
    handler({ text, mode }) {
      const transforms = {
        uppercase: () => text.toUpperCase(),
        lowercase: () => text.toLowerCase(),
        titlecase: () => text.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()),
        reverse: () => text.split("").reverse().join(""),
      };
      return transforms[mode]();
    },
  },
};

function handleJsonRpc({ id, method, params }) {
  if (method === "initialize")
    return { jsonrpc: "2.0", id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "word-tools", version: "1.0.0" } } };
  if (method === "tools/list")
    return { jsonrpc: "2.0", id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema })) } };
  if (method === "tools/call") {
    const tool = tools[params?.name];
    if (!tool) return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${params?.name}` } };
    const result = tool.handler(params.arguments || {});
    return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }] } };
  }
  return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } };
}

app.post("/mcp", (req, res) => res.json(handleJsonRpc(req.body)));
app.listen(4000, () => console.log("MCP server on http://localhost:4000/mcp"));
Enter fullscreen mode Exit fullscreen mode

Test it works:

curl -X POST http://localhost:4000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"word_count","arguments":{"text":"Hello world from MCP"}}}'
# → {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"words\":4,\"characters\":20,\"sentences\":1}"}]}}
Enter fullscreen mode Exit fullscreen mode

Anyone can call it for free. Let's fix that.

Step 2: Register as an operator (1 minute)

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/register \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "name": "Your Name"}'
Enter fullscreen mode Exit fullscreen mode
{
  "operator_id": "op_01KW...",
  "api_key": "cgwop_abcd1234..."
}
Enter fullscreen mode Exit fullscreen mode

Save the api_key. Visit the stripe_onboard_url in the full response to connect your Stripe account for payouts — takes about 2 minutes.

Step 3: Register your server (1 minute)

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/servers \
  -H "Authorization: Bearer YOUR_OPERATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Word Tools",
    "upstream_url": "https://your-server.example.com/mcp",
    "proxy_slug": "word-tools",
    "auth_mode": "header",
    "upstream_auth_token": "your-server-secret",
    "is_public": true
  }'
Enter fullscreen mode Exit fullscreen mode

Your server is now proxied at:

https://mcp-billing-gateway-production.up.railway.app/proxy/word-tools
Enter fullscreen mode Exit fullscreen mode

The upstream_auth_token is your server's own secret — callers never see it. The gateway injects it when forwarding requests to your upstream.

Step 4: Set pricing (1 minute)

Per-call (pay-as-you-go):

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/servers/SERVER_ID/plans \
  -H "Authorization: Bearer YOUR_OPERATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pay-as-you-go",
    "billing_model": "per_call",
    "price_per_call_usd_micro": 10000,
    "free_calls_per_month": 100,
    "is_default": true
  }'
Enter fullscreen mode Exit fullscreen mode

$0.01 per call (prices are in micro-units: 10,000 = $0.01), with 100 free calls/month. The free tier resets monthly.

Or subscription:

-d '{"name": "Pro", "billing_model": "subscription", "subscription_price_usd_cents": 999, "subscription_interval": "monthly", "subscription_call_limit": 10000}'
Enter fullscreen mode Exit fullscreen mode

Or tiered:

-d '{"name": "Volume", "billing_model": "tiered", "tiers": [{"up_to": 1000, "price_usd_micro": 0}, {"up_to": 10000, "price_usd_micro": 5000}, {"up_to": null, "price_usd_micro": 10000}]}'
Enter fullscreen mode Exit fullscreen mode

First 1,000 calls free, next 9,000 at $0.005, everything above at $0.01.

Step 5: Callers connect (2 minutes)

Callers register and get an API key:

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/caller/register \
  -H "Content-Type: application/json" \
  -d '{"email": "agent@example.com", "name": "My AI Agent"}'
Enter fullscreen mode Exit fullscreen mode

Then call through the proxy:

curl -X POST https://mcp-billing-gateway-production.up.railway.app/proxy/word-tools \
  -H "Authorization: Bearer CALLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"word_count","arguments":{"text":"Hello world"}}}'
Enter fullscreen mode Exit fullscreen mode

Same JSON-RPC response — but now it's billed. After 100 free calls, each call costs $0.01 from their credit balance.

For Claude Desktop or Cursor, callers add to their MCP config:

{
  "mcpServers": {
    "word-tools": {
      "url": "https://mcp-billing-gateway-production.up.railway.app/proxy/word-tools/mcp",
      "headers": { "Authorization": "Bearer CALLER_API_KEY" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

x402 Crypto Payments

For agent-to-agent transactions, the gateway also supports x402 — USDC on Base chain. No API key required. The caller sends a payment claim header:

curl -X POST https://mcp-billing-gateway-production.up.railway.app/proxy/word-tools \
  -H "X-402-Payment: <base64-encoded-payment-claim>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"word_count","arguments":{"text":"Hello"}}}'
Enter fullscreen mode Exit fullscreen mode

If a caller sends a request without valid payment, they get a structured 402 response with pricing info. This means the same server accepts both Stripe credits and USDC payments — whatever the caller supports.

Step 6: Check revenue (1 minute)

curl https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/servers/SERVER_ID/usage \
  -H "Authorization: Bearer YOUR_OPERATOR_KEY"
Enter fullscreen mode Exit fullscreen mode
{
  "total_calls": 47,
  "total_revenue_usd_cents": 37,
  "by_tool": [
    {"tool_name": "word_count", "calls": 30, "revenue_usd_cents": 20},
    {"tool_name": "text_transform", "calls": 17, "revenue_usd_cents": 17}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Default revenue split: 85/15 — you keep 85%. Request a payout when you're ready:

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/payouts/request \
  -H "Authorization: Bearer YOUR_OPERATOR_KEY"
Enter fullscreen mode Exit fullscreen mode

Funds hit your Stripe account in 1–2 business days. Minimum: $1.00.

What you did not have to build

Concern Without gateway With gateway
API key management Build auth system Built-in (hashed, scoped, revocable)
Stripe integration Stripe SDK + webhooks Pre-integrated via Connect
Usage metering Build tracking Per-call metering, per-tool breakdown
Subscription management Handle webhook lifecycle Automatic period tracking + cancellation
Credit system Build ledger + balance Prepaid credits with auto-refund on errors
x402 crypto payments Implement verification Built-in USDC validation + anti-replay
Revenue analytics Build dashboard Usage by day, tool, billing rail, caller
Payouts Manual transfers One API call, automatic splits

Try the demo

Clone and run the full interactive demo:

git clone https://github.com/sapph1re/mcp-billing-demo.git
cd mcp-billing-demo
npm install && npm start  # Terminal 1: starts the MCP server
npm run demo              # Terminal 2: runs all 6 steps against live gateway
Enter fullscreen mode Exit fullscreen mode

The demo walks through every step with real API calls.


Live gateway: mcp-billing-gateway-production.up.railway.app
SDK docs: sapph1re/mcp-billing-gateway-sdk
Demo repo: sapph1re/mcp-billing-demo

Top comments (0)