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
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 │
└─────────────────────────────┘
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
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
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;
}
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}`);
});
Build and test:
npx tsc
node dist/index.js
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"
}'
You'll get back:
{
"operator_id": "op_01jvz...",
"api_key": "cgwop_abcd1234...",
"stripe_onboard_url": "https://connect.stripe.com/setup/..."
}
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
}'
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
}'
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" }
}
}'
With x402 (USDC on Base)
For x402, the agent doesn't need an API key. The flow is:
- Agent sends the request without payment
- Gateway returns
402with pricing info:
{
"error": {
"code": 402,
"message": "Payment Required",
"data": {
"price_usd_micro": 10000,
"x402_address": "0xYourWalletAddress",
"chain": "base"
}
}
}
- 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" }
}
}'
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..."
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
}
}
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..."
}
}
}
}
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
- Deploy your server to Railway, Fly.io, or any cloud provider
- Register with the billing gateway at mcp-billing-gateway-production.up.railway.app
- Set your pricing — start with a generous free tier to attract early adopters
- Submit to MCP registries like Glama and the official MCP registry to get discovered
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)