DEV Community

Roman V
Roman V

Posted on

Build a Paid MCP Server with x402 Micropayments: A Step-by-Step Tutorial

You want to build an MCP server that AI agents pay to use — per call, in USDC, no subscriptions, no invoices. Just: agent calls your tool, payment happens, response comes back.

This tutorial walks through building a paid MCP server from scratch using x402 micropayments and mcp-billing-gateway for billing infrastructure. By the end, you'll have a working MCP server that charges $0.01 per tool call.

What Is x402?

x402 brings back HTTP's forgotten status code — 402 Payment Required. The flow is simple:

Agent sends request
    → Server returns 402 + payment details
    → Agent pays (USDC on Base network)
    → Agent retries with payment proof in header
    → Server verifies payment, returns data
Enter fullscreen mode Exit fullscreen mode

No API keys for payment. No monthly invoices. The agent pays per call using USDC stablecoins on the Base L2 network. Transactions settle in seconds and cost fractions of a cent in gas.

This is ideal for agent-to-agent services where the "customer" is an AI agent with a crypto wallet, not a human with a credit card.

Architecture Overview

We'll build three components:

┌─────────────────┐
│   AI Agent      │
│  (Claude, etc.) │
└────────┬────────┘
         │ JSON-RPC + X-402-Payment header
         ▼
┌─────────────────────────────┐
│   mcp-billing-gateway       │
│                             │
│  • Validates x402 payment   │
│  • Tracks usage per caller  │
│  • Splits revenue           │
│  • Forwards to upstream     │
└────────────┬────────────────┘
             │ JSON-RPC (authenticated)
             ▼
┌─────────────────────────────┐
│   Your MCP Server           │
│                             │
│  • Implements tools         │
│  • Returns data             │
│  • Knows nothing about      │
│    billing                  │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The key insight: your MCP server never handles payments. The billing gateway sits in front as a reverse proxy. It verifies payment, deducts credits or validates x402 claims, then forwards the request. Your server just serves data.

Step 1: Build the MCP Server

Let's build a simple financial data MCP server with two tools. We'll use TypeScript and the official MCP SDK.

Project Setup

mkdir stock-mcp && cd stock-mcp
npm init -y
npm install @modelcontextprotocol/sdk express zod
npm install -D typescript @types/node @types/express
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Implement the Server

Create src/server.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function createServer() {
  const server = new McpServer({
    name: "stock-mcp",
    version: "1.0.0",
  });

  server.tool(
    "get_stock_quote",
    "Get current stock price and daily change",
    { symbol: z.string().describe("Stock ticker symbol (e.g. AAPL)") },
    async ({ symbol }) => {
      const ticker = symbol.toUpperCase();

      // Replace with your real data source
      const res = await fetch(
        `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?range=1d`
      );
      const data = await res.json();
      const meta = data.chart.result[0].meta;

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              symbol: ticker,
              price: meta.regularMarketPrice,
              previousClose: meta.previousClose,
              change: (
                meta.regularMarketPrice - meta.previousClose
              ).toFixed(2),
              changePercent: (
                ((meta.regularMarketPrice - meta.previousClose) /
                  meta.previousClose) *
                100
              ).toFixed(2),
              currency: meta.currency,
              exchange: meta.exchangeName,
            }),
          },
        ],
      };
    }
  );

  server.tool(
    "get_market_summary",
    "Get major market indices overview",
    {},
    async () => {
      const indices = ["^GSPC", "^DJI", "^IXIC"];
      const results = await Promise.all(
        indices.map(async (idx) => {
          const res = await fetch(
            `https://query1.finance.yahoo.com/v8/finance/chart/${idx}?range=1d`
          );
          const data = await res.json();
          const meta = data.chart.result[0].meta;
          return {
            index: meta.shortName || idx,
            price: meta.regularMarketPrice,
            change: (
              meta.regularMarketPrice - meta.previousClose
            ).toFixed(2),
          };
        })
      );

      return {
        content: [
          { type: "text", text: JSON.stringify(results, null, 2) },
        ],
      };
    }
  );

  return server;
}
Enter fullscreen mode Exit fullscreen mode

Add HTTP Transport

Create src/index.ts:

import express from "express";
import { createServer } from "./server.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

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

const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId && sessions.has(sessionId)) {
    const transport = sessions.get(sessionId)!;
    await transport.handleRequest(req, res);
    return;
  }

  // New session
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
  const server = createServer();
  await server.connect(transport);

  sessions.set(transport.sessionId!, transport);

  transport.onclose = () => {
    sessions.delete(transport.sessionId!);
  };

  await transport.handleRequest(req, res);
});

app.get("/health", (_, res) => res.json({ status: "ok" }));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Stock MCP server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Build and test:

npx tsc
node dist/index.js
Enter fullscreen mode Exit fullscreen mode

Your MCP server is running. Agents can call get_stock_quote and get_market_summary — but right now, for free. Let's fix that.

Step 2: Register with the Billing Gateway

The mcp-billing-gateway handles payment verification, usage tracking, and revenue collection. You register your server, set pricing, and it proxies all requests.

Register as an Operator

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

You'll get back:

{
  "operator_id": "op_01jvz...",
  "api_key": "cgwop_abcd1234...",
  "stripe_onboard_url": "https://connect.stripe.com/setup/..."
}
Enter fullscreen mode Exit fullscreen mode

Save your api_key — it's shown once. Complete the Stripe onboarding if you want fiat payouts (optional for x402-only).

Register Your Server

curl -X POST https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/servers \
  -H "Authorization: Bearer cgwop_abcd1234..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stock MCP",
    "description": "Real-time stock quotes and market data",
    "upstream_url": "https://your-deployed-server.com/mcp",
    "proxy_slug": "stock-data",
    "auth_mode": "none",
    "transport": "http",
    "is_public": true
  }'
Enter fullscreen mode Exit fullscreen mode

Your server is now accessible via the gateway at /proxy/stock-data.

Step 3: Set x402 Pricing

Create a per-call pricing plan:

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

This sets pricing at $0.01 per call (10,000 micro-USD) with 50 free calls per month. The free tier lets agents try your tools before committing.

The billing gateway accepts both Stripe credits and x402 payments. For x402, agents include a X-402-Payment header with their USDC payment proof — no API key needed.

Step 4: How Agents Pay

Here's what the payment flow looks like from the agent's perspective.

With API Key (Stripe Credits)

# Agent calls through the gateway
curl -X POST https://mcp-billing-gateway-production.up.railway.app/proxy/stock-data \
  -H "Authorization: Bearer cgwcl_agent_key..." \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_stock_quote",
      "arguments": { "symbol": "AAPL" }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

With x402 (USDC on Base)

For x402, the agent doesn't need an API key. The flow is:

  1. Agent sends the request without payment
  2. Gateway returns 402 with pricing info:
{
  "error": {
    "code": 402,
    "message": "Payment Required",
    "data": {
      "price_usd_micro": 10000,
      "x402_address": "0xYourWalletAddress",
      "chain": "base"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Agent sends USDC payment on Base and retries with proof:
curl -X POST https://mcp-billing-gateway-production.up.railway.app/proxy/stock-data \
  -H "X-402-Payment: eyJ0eXBlIjoiZXhhY3QiLC..." \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_stock_quote",
      "arguments": { "symbol": "AAPL" }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The gateway validates the payment on-chain, records usage, and forwards the request to your server. Replay protection prevents the same payment from being used twice.

Step 5: Monitor Revenue

Check how your server is performing:

# Usage breakdown
curl https://mcp-billing-gateway-production.up.railway.app/api/v1/operator/servers/SERVER_ID/usage \
  -H "Authorization: Bearer cgwop_abcd1234..."
Enter fullscreen mode Exit fullscreen mode

Response includes calls by day, by tool, and by billing rail (fiat vs. x402):

{
  "total_calls": 1247,
  "total_revenue_usd_cents": 1197,
  "by_billing_rail": {
    "x402": 891,
    "fiat_credit": 306,
    "free": 50
  },
  "by_tool": {
    "get_stock_quote": 823,
    "get_market_summary": 424
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Configure Agent Access (Claude Code)

For agents using Claude Code, add this to their MCP config:

{
  "mcpServers": {
    "stock-data": {
      "type": "streamableHttp",
      "url": "https://mcp-billing-gateway-production.up.railway.app/proxy/stock-data",
      "headers": {
        "Authorization": "Bearer cgwcl_agent_api_key..."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The agent can now call get_stock_quote and get_market_summary as regular MCP tools. Billing happens transparently — the agent doesn't even know it's being charged.

What You Get Without Building

By using the billing gateway instead of rolling your own payment stack, you skip building:

  • Payment verification — x402 proof validation + Stripe charge handling
  • Usage metering — per-call tracking with tool-level granularity
  • Credit system — prepaid balances with atomic debits and auto-refunds on errors
  • Subscription management — Stripe-managed recurring billing with period tracking
  • Revenue splitting — configurable operator/platform splits
  • API key management — SHA256-hashed keys with prefixes, rotation, and revocation
  • Replay protection — x402 payment hash dedup within 5-minute windows
  • Abuse prevention — IP logging, account suspension, rate tracking

That's 2–4 weeks of billing engineering you don't have to do. Your server stays pure — it just serves data.

When to Use x402 vs. Stripe

Scenario Payment Method
Agent-to-agent services x402 (agents have wallets)
Human developers Stripe subscription
Mixed audience Both — gateway supports dual-rail
Free/open-source tools No billing needed

x402 shines for machine-to-machine payments where there's no human to enter a credit card. If your MCP server is consumed primarily by AI agents, x402 with per-call pricing is the natural fit.

Next Steps

The billing gateway is open for operator registration. The SDK documentation has full API reference and examples.


This tutorial uses mcp-billing-gateway for billing infrastructure and the MCP TypeScript SDK for the server implementation.

Top comments (0)