Skip to main content
HomeLanding pagesHow to add payments to an MCP server
LANDING PAGE

How to add payments to an MCP server

8 min read·Last updated June 2, 2026

Run your MCP server over the Streamable HTTP transport and mount createX402Plugin (Fastify) or createX402Middleware (Express) from @blockchain0x/x402 in front of the paid route. Set a price, a payToAddress, and a paymentRequestId. The plugin returns a real 402 with an exact-usdc requirement; on retry it verifies the X-Payment header and settles USDC on Base before your handler runs.

What this guide covers

The code-level steps to take an MCP server that serves tools for free and start charging for the premium ones, settled in USDC on Base. This is the technical "how to add" guide. For the business framing (what to charge, which tools to gate), see how-to-monetize-mcp-server. For the broader reference, the MCP integration page.

One thing to set straight first, because the mental model matters. Payment is enforced at the HTTP layer, not per MCP tool name. You run your server over the Streamable HTTP transport, and you mount an x402 adapter in front of the route. The adapter answers 402 Payment Required, and on the caller's retry it verifies the payment and settles before your handler runs. That is the whole shape.

Prerequisites

Have these ready:

  • An MCP server built with the official Model Context Protocol SDK, exposed over the Streamable HTTP transport on Fastify or Express.
  • A Blockchain0x account. The server is the payee, so you need its wallet address (the payToAddress) and a paymentRequestId for the priced route.
  • A sk_test_ API key for this walkthrough.
  • Node 18+. The SDK targets >=18.

Set the server's env:

BASH
export B0X_API_KEY=sk_test_...        # sk_test_ -> Base Sepolia
export B0X_PAYTO_ADDRESS=0x...        # the server wallet that receives USDC
export B0X_PRICE_REQUEST_ID=pr_...    # payment-request id for the priced route

Install the package

Two real packages: the client and the x402 server adapter.

BASH
npm install @blockchain0x/node @blockchain0x/x402

@blockchain0x/node gives you the client the adapter uses to settle. @blockchain0x/x402 ships the server adapters at ./server/fastify and ./server/express. There is no MCP-specific package, and you do not need one. The adapter gates the route your MCP transport already uses.

Mount the x402 plugin

Put the adapter in front of the route your MCP server handles. The pricing table is keyed by METHOD path.

Fastify:

TYPESCRIPT
import Fastify from "fastify";
import { createClient } from "@blockchain0x/node";
import { createX402Plugin } from "@blockchain0x/x402/server/fastify";

const sdk = createClient({ apiKey: process.env.B0X_API_KEY! }); // sk_test_ -> Base Sepolia

const app = Fastify();
await app.register(createX402Plugin, {
  sdk,
  defaultNetwork: "testnet",
  pricing: {
    "POST /mcp/premium": {
      amountUsdc: "0.005",
      payToAddress: process.env.B0X_PAYTO_ADDRESS!,
      paymentRequestId: process.env.B0X_PRICE_REQUEST_ID!,
    },
  },
});

// Your existing MCP-over-HTTP handler is unchanged.
app.post("/mcp/premium", async (req, reply) => {
  return handleMcpRequest(req.body); // your streamable-HTTP handler
});

await app.listen({ port: 8080 });

Express:

TYPESCRIPT
import express from "express";
import { createClient } from "@blockchain0x/node";
import { createX402Middleware } from "@blockchain0x/x402/server/express";

const sdk = createClient({ apiKey: process.env.B0X_API_KEY! });
const app = express();

app.use(
  createX402Middleware({
    sdk,
    defaultNetwork: "testnet",
    pricing: {
      "POST /mcp/premium": {
        amountUsdc: "0.005",
        payToAddress: process.env.B0X_PAYTO_ADDRESS!,
        paymentRequestId: process.env.B0X_PRICE_REQUEST_ID!,
      },
    },
  }),
);

app.post("/mcp/premium", express.json(), (req, res) => {
  res.json(handleMcpRequest(req.body));
});

That is the integration. The adapter intercepts POST /mcp/premium, and your handler only runs once payment is verified.

What the 402 looks like

When an unpaid caller hits the priced route, the adapter returns a real x402 body. This is the wire format, not an invented shape:

JSON
{
  "version": 1,
  "resource": "POST /mcp/premium",
  "accepts": [
    {
      "scheme": "exact-usdc",
      "network": "testnet",
      "chainId": "eip155:84532",
      "payToAddress": "0x...",
      "amountWeiUsdc": "5000",
      "paymentRequestId": "pr_...",
      "maxAgeSeconds": 300
    }
  ]
}

amountWeiUsdc is 0.005 USDC expressed in 6-decimal base units, so 5000. A paying client reads the requirement, pays the payToAddress in USDC, and retries the same request with an X-Payment: exact-usdc:<base64> header. The adapter verifies that header and, if it checks out, passes the request to your handler. From the agent's side it is one tool call that took a beat longer.

Free and paid tools side by side

Because pricing is per route, free and paid tools coexist by living on different paths. Keep your discovery and cheap tools on an unpriced route, and put the premium ones behind a priced one.

TYPESCRIPT
await app.register(createX402Plugin, {
  sdk,
  defaultNetwork: "testnet",
  pricing: {
    "POST /mcp/premium": { amountUsdc: "0.01", payToAddress: process.env.B0X_PAYTO_ADDRESS!, paymentRequestId: process.env.B0X_PRICE_REQUEST_ID! },
  },
});

// /mcp/free is not in the pricing table, so it is never gated.
app.post("/mcp/free", async (req) => handleMcpRequest(req.body));
app.post("/mcp/premium", async (req) => handleMcpRequest(req.body));

This is the honest granularity. If you want ten tools at ten prices, that is ten routes, not ten decorators on one path. Most servers only gate one or two premium routes, so this stays simple in practice.

Verify with a paid client

The fastest end-to-end check is to call the priced route from a client that holds a Blockchain0x test wallet and speaks x402 (the pay-side createX402Client from @blockchain0x/x402/client does this automatically). Start your server, point the client at /mcp/premium, and watch the 402, the settle, and the 200 in your logs.

For a quick smoke test without a wallet, curl the route with no X-Payment header and confirm you get the 402 body above. That proves the adapter is mounted and quoting the right price before you wire a real payer.

When a payment does fail, the adapter tells you why rather than returning a vague error. A missing header reads as header_missing, a corrupt one as header_malformed, a payment that does not match the quoted price or network as requirement_mismatch, and a settle call the chain rejected as settle_rejected. Log that reason on every non-2xx. In the first week it is the single fastest way to tell "the client never paid" apart from "the client paid the wrong amount", and the two have completely different fixes.

Test on Base Sepolia

Run the whole loop on testnet first. With defaultNetwork: "testnet" and a sk_test_ key, settlement happens on Base Sepolia (eip155:84532) with the same shapes as mainnet. The paying client funds its wallet with test USDC from a public Base Sepolia faucet, then calls your route.

One honest caveat: settlement is in active rollout, and the verify path calls paymentRequests.settle, which can return a 503 on a network whose chain adapter is not live yet. Handle a non-2xx from the adapter as "payment not confirmed" rather than assuming success. The test-agent-payments guide has the full sandbox flow, and production pricing is on the pricing page.

Common pitfalls

Four mistakes that cost an afternoon in week one.

Gating the whole server. Putting every route in the pricing table means clients pay just to list your tools. Keep discovery and cheap tools on an unpriced path. Gate only the premium routes.

Trying to price per tool name on one path. The adapter keys on METHOD path. Ten tools on one JSON-RPC route get one price. Split premium tools onto their own routes if you need separate prices, and do not try to read the tool name inside the pricing table.

Shipping a stdio-only server and expecting payment. There is no HTTP layer to gate on pure stdio. Expose the Streamable HTTP transport for any server that needs to charge.

Booting with the wrong key prefix. A server that starts with sk_test_ in production quotes Base Sepolia requirements that real wallets will not pay. The server stamps the network from the key, so check the prefix at boot and refuse to start on a mismatch.

What to ship today

The shortest path from "free MCP server" to "paid MCP server":

  1. Make sure your server runs over the Streamable HTTP transport.
  2. npm install @blockchain0x/node @blockchain0x/x402.
  3. Register createX402Plugin (or createX402Middleware) with one priced route.
  4. Curl the route with no payment, confirm the 402, then pay it from a test wallet.

That is the integration. Adding a second priced route later is one more entry in the pricing table. For the decision side (what to charge and which tools to gate), continue with how-to-monetize-mcp-server or mcp-server-monetization.

FAQ

Frequently asked questions.

Can I add payments without rewriting my MCP server?

Yes. You mount one plugin in front of the HTTP route your MCP server already listens on. Tool registration, schemas, and the transport stay the same. The plugin sits at the HTTP layer and decides whether a request reaches your handler, so your MCP code does not change.

Does this work over stdio, SSE, and HTTP?

The x402 adapters are HTTP middleware: createX402Plugin for Fastify and createX402Middleware for Express. They gate the Streamable HTTP transport (and SSE, which rides HTTP). A pure stdio server has no HTTP layer to gate, so a paid public MCP server should expose the HTTP transport. That is the transport agent clients use to reach you anyway.

What stops a caller from forging a payment?

The server verifies it. On the retry, the adapter reads the X-Payment header and calls paymentRequests.settle against Blockchain0x to confirm the on-chain transfer matches the quoted requirement. A missing, malformed, or mismatched payment is rejected and the caller gets a fresh 402. You never trust the client's claim.

Can different tools on the same server have different prices?

Pricing is keyed by route, as METHOD and path, not by individual MCP tool name. Everything sharing one JSON-RPC path shares one price. To price tools separately, put the premium ones behind their own path and add a pricing entry for it. Free tools stay on an unpriced route.

Which network and token does settlement use?

USDC, 6 decimals, on Base. Set defaultNetwork to testnet for Base Sepolia (eip155:84532) or mainnet for Base (eip155:8453); a per-route entry can override it. The only scheme is exact-usdc, and amounts are quoted in USDC decimals that the adapter converts to amountWeiUsdc.

How is this different from selling API access via Stripe?

Stripe expects a human signup, an account, a stored card, and an invoice. x402 expects none of that. The calling agent's wallet pays per request with no prior relationship. For agent traffic that has no human to onboard, x402 collapses signup, billing, and payment into a single 402-then-retry round trip.

Create your free agent wallet in 5 minutes.

First payment confirmed in under ten minutes. Free to start.