If you've built MCP servers for Claude Code, you know the distribution problem: anyone who installs your server gets unlimited free access. There's no clean way to charge per call without rolling your own auth + billing.
x402 solves this at the HTTP layer. Here's how to build a pay-per-call MCP server where every tool call costs USDC — settled instantly on Base.
What is x402?
x402 is an HTTP payment protocol that revives the abandoned 402 Payment Required status code. The flow:
- Client requests a resource
- Server returns
402with aPAYMENT-REQUIREDheader containing payment details (price, wallet, network) - Client signs a USDC transfer using EIP-3009 (no gas needed from the client)
- Client retries with a
PAYMENT-SIGNATUREheader - Coinbase facilitator validates and settles on-chain
- Server returns the resource
No API keys. No billing infrastructure. No subscriptions. Just HTTP + crypto.
The Stack
@modelcontextprotocol/sdk — MCP server
@x402/core — x402 client protocol
@x402/evm — EVM payment signing
viem — wallet/account management
Building the Payment Client
The key is wiring up the x402 client correctly. The v2 API (current as of @x402/core 2.11) looks like this:
const { x402Client, x402HTTPClient } = require("@x402/core/client");
const { ExactEvmScheme } = require("@x402/evm/exact/client");
const { toClientEvmSigner } = require("@x402/evm");
const { privateKeyToAccount } = require("viem/accounts");
function buildHttpClient() {
const key = process.env.WALLET_PRIVATE_KEY;
const pk = key.startsWith("0x") ? key : "0x" + key;
const account = privateKeyToAccount(pk);
const signer = toClientEvmSigner(account);
const coreClient = new x402Client().register("eip155:*", new ExactEvmScheme(signer));
return new x402HTTPClient(coreClient);
}
Important: pass account to toClientEvmSigner, not walletClient. In viem v2, walletClient.address is undefined — the address lives on account.address.
Making a Paid Request
The http client doesn't have a .fetch() method in v2. You handle the 402 flow manually:
async function paidFetch(httpClient, url) {
const res = await fetch(url);
if (res.status !== 402) {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Extract payment requirements from header
let body;
try { body = await res.clone().json(); } catch (_) {}
const paymentRequired = httpClient.getPaymentRequiredResponse(
(name) => res.headers.get(name),
body
);
// Sign and send payment
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
const paidRes = await fetch(url, {
headers: httpClient.encodePaymentSignatureHeader(paymentPayload),
});
const raw = await paidRes.text();
if (!paidRes.ok) throw new Error(`Payment rejected (${paidRes.status}): ${raw}`);
return JSON.parse(raw);
}
Wiring it into an MCP Tool
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { CallToolRequestSchema, ListToolsRequestSchema } = require("@modelcontextprotocol/sdk/types.js");
const BASE_URL = "https://your-x402-endpoint.com";
const TOOLS = [{
name: "get_data",
description: "Get premium data. Costs $0.01 USDC.",
inputSchema: { type: "object", properties: {} }
}];
async function main() {
const httpClient = buildHttpClient();
const server = new Server(
{ name: "my-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
try {
const data = await paidFetch(httpClient, `${BASE_URL}/api/data`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
} catch (e) {
return { content: [{ type: "text", text: "Error: " + e.message }], isError: true };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();
Building the Server Side
On the server, use @x402/express with paymentMiddleware:
const { paymentMiddleware } = require("@x402/express");
const { x402ResourceServer, HTTPFacilitatorClient } = require("@x402/core/server");
const { ExactEvmScheme } = require("@x402/evm/exact/server");
const facilitator = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" });
const server = new x402ResourceServer(facilitator);
server.register("eip155:8453", new ExactEvmScheme());
app.use(paymentMiddleware({
"GET /api/data": {
accepts: {
scheme: "exact",
price: "$0.01",
network: "eip155:8453",
payTo: "0xYourWalletAddress"
},
description: "Premium data endpoint"
}
}, server));
app.get("/api/data", (req, res) => {
res.json({ result: "your data here" });
});
Every request to /api/data now requires a $0.01 USDC payment on Base before the route handler fires.
The CDP Bazaar Bonus
If you add @x402/extensions/bazaar to your server, Coinbase's Bazaar protocol automatically indexes your service on agentic.market. Autonomous agents with funded wallets can discover and pay for your endpoint without any human involvement.
const { declareDiscoveryExtension } = require("@x402/extensions/bazaar");
app.use(paymentMiddleware({
"GET /api/data": {
accepts: { scheme: "exact", price: "$0.01", network: "eip155:8453", payTo: "0x..." },
extensions: { ...declareDiscoveryExtension({ output: { example: { result: "..." } } }) }
}
}, server));
Publishing
// package.json
{
"name": "my-mcp",
"bin": { "my-mcp": "index.js" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.1",
"@x402/core": "^2.11.0",
"@x402/evm": "^2.11.0",
"viem": "^2.0.0"
}
}
npm publish --access public
Users install with:
{
"mcpServers": {
"my-mcp": {
"command": "npx",
"args": ["my-mcp"],
"env": { "WALLET_PRIVATE_KEY": "0x..." }
}
}
}
A Working Example
I built this for coinopai-mcp — 6 tools covering agent automation prompts and crypto signals, all pay-per-call via x402.
npm install coinopai-mcp — source on GitHub at coinopai/coinopai-mcp.
The whole server is under 150 lines. x402 does the hard part.
Top comments (0)