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

How to build an MCP server with payments

8 min read·Last updated June 2, 2026

Follow one example end to end: scaffold an MCP server with the Model Context Protocol SDK, register a single tool, expose it over the Streamable HTTP transport, then mount createX402Plugin from @blockchain0x/x402 on its route so callers pay in USDC on Base before the tool runs. This walkthrough builds that one paid tool from an empty folder to a verified call.

What you will build

One paid MCP tool, end to end, from an empty folder to a call that pays. The example is a single premium tool, call it summarize, exposed by an MCP server over HTTP and gated so a caller pays in USDC on Base before it runs. This is the follow-along version: copy each step, run it, and you have a working paid MCP server you can adapt.

For the structural view (which tools to gate, freemium shape, deploy and scale), see how-to-build-a-paid-mcp-server; to retrofit an existing server, see how-to-add-payments-to-mcp-server. The MCP integration page is the canonical reference. Here we just build the one thing.

Scaffold the project

Start empty and install what you need: the MCP SDK, a web server for the HTTP transport, and the Blockchain0x packages.

BASH
mkdir paid-mcp && cd paid-mcp && npm init -y
npm install @modelcontextprotocol/sdk fastify @blockchain0x/node @blockchain0x/x402

Set the env the server will read:

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

Register one tool

Build the MCP server and register the single summarize tool with the Model Context Protocol SDK. The tool logic is ordinary; nothing about it knows it will be paid.

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

const mcp = new McpServer({ name: "paid-mcp", version: "1.0.0" });

mcp.tool(
  "summarize",
  { url: z.string() },
  async ({ url }) => {
    const text = await summarize(url); // your real work
    return { content: [{ type: "text", text }] };
  },
);

That is a normal MCP tool. If you stopped here you would have a free MCP server; the next two steps make it paid without touching this handler.

Serve it over HTTP

Expose the server over the Streamable HTTP transport so it answers on an HTTP route. Mount that route on Fastify, which is also where the paywall will sit.

TYPESCRIPT
import Fastify from "fastify";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = Fastify();
const transport = new StreamableHTTPServerTransport({ /* session config per MCP SDK */ });
await mcp.connect(transport);

app.post("/mcp", (req, reply) => transport.handleRequest(req.raw, reply.raw, req.body));
await app.listen({ port: 8080 });

The exact transport wiring follows the MCP SDK docs; the point for payments is only that the server now answers on POST /mcp, an HTTP route the adapter can gate.

Add the paywall

Now the one new piece. Mount the x402 adapter on the route, with a price, your wallet, and a payment-request id. This is the only Blockchain0x code in the build.

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

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

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

Register this before the route handler. Now an unpaid POST /mcp gets a 402 quoting two cents, the adapter verifies the caller's X-Payment header through paymentRequests.settle on the retry, and your summarize tool runs only once the payment confirms.

Why payment sits in front, not inside

Notice what the build did and did not do. The summarize handler from step two never changed. It does not know a price, it does not check a receipt, it does not import anything from Blockchain0x. All the payment logic lives in the adapter you registered in front of the route, and the tool stays a pure piece of work.

That separation is deliberate and worth keeping as you grow. Your tools stay testable and portable, you can run the exact same tool free in development and paid in production by toggling whether the route is in the pricing table, and the payment behavior is configured in one place rather than threaded through every handler. It also means a bug in your tool cannot accidentally bypass payment, because payment is enforced before the tool is even reached. When you add more tools later, resist the urge to put pricing logic inside them; keep the handlers payment-free and let the route-level adapter do the gating. The discipline that makes this build clean is the same one that keeps a larger paid server maintainable.

Call it and confirm payment

Run the server on Base Sepolia and exercise it. First, confirm the paywall with a plain request and no payment:

BASH
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8080/mcp   # expect 402

Then pay it from a client that holds a test wallet and speaks x402 (the pay side is createX402Client); the call settles in USDC and the tool returns the summary. Watch a payment.received event land for the paid call. When the unpaid 402 and the paid 200 both behave, you have a working paid MCP server. If a paid call comes back non-2xx, check the adapter's logged reason: it names whether the payment was missing, malformed, mismatched on amount or network, or rejected at settlement, which tells you exactly which side to fix before you move on.

Add a free tool alongside

A real server pairs paid tools with free ones, so callers can discover what you offer before paying. Adding a free tool is the same pattern with one difference: its route is not in the pricing table, so it is never gated.

TYPESCRIPT
// a free tool: register it like summarize, but expose it on a route the
// pricing table does not mention, so the adapter leaves it open
app.post("/mcp/free", (req, reply) => transport.handleRequest(req.raw, reply.raw, req.body));

Because the pricing table only lists POST /mcp, the POST /mcp/free route answers normally with no 402. That is the whole freemium mechanism: paid tools on the priced route, discovery and cheap tools on an unpriced one. Keep your listing and metadata tools free so a calling agent can see what the server does before it decides to pay for the premium tool, which is what gets it to pay at all.

Where to go next

You built one paid tool. Growing it is small steps: add a free discovery route by adding a handler with no pricing entry, add a second priced tool on its own route with its own pricing entry, and when you are happy, swap the sk_test_ key for a sk_live_ one to settle on Base mainnet. The structural guidance for those choices is in how-to-build-a-paid-mcp-server.

Common pitfalls

Three traps in this build.

Registering the adapter after the route. Mount createX402Plugin before the handler so it can intercept the request. Registered after, the handler runs unpaid.

Using stdio for a paid server. The adapter gates HTTP. Use the Streamable HTTP transport so there is a route to charge on.

Putting the tool's price in the handler. The price lives in the adapter's pricing table, not in your tool code. Keep the handler payment-free and let the adapter gate the route.

What you will ship

From an empty folder: scaffold, register one tool, serve it over Streamable HTTP, gate the route with the x402 adapter, and confirm an unpaid call gets a 402 while a paid one runs. That is a complete paid MCP server. For structure and scaling see how-to-build-a-paid-mcp-server. Pricing is on the pricing page.

FAQ

Frequently asked questions.

Is this different from how-to-build-a-paid-mcp-server?

Same goal, different format. That page covers the shape and the decisions (which tools to gate, how to deploy). This one is a single follow-along example: one tool, built from an empty folder to a working paid call, so you can copy it and adapt. Use this to get something running, that one to think about structure.

What MCP SDK does this use?

The official Model Context Protocol SDK for TypeScript. You register a tool and connect a transport with it the normal way; the payment part is purely the x402 adapter in front of the HTTP route, so this works with the MCP SDK as-is and adds nothing to your tool code.

Why the Streamable HTTP transport and not stdio?

The x402 adapter gates HTTP, so the server needs an HTTP route to charge on. The Streamable HTTP transport gives you that, and it is the transport remote agent clients use to reach a hosted MCP server anyway. A stdio server has no HTTP layer to gate.

Which network and token does it settle in?

USDC, 6 decimals, on Base. Build with a sk_test_ key on Base Sepolia (eip155:84532), then switch to a sk_live_ key for Base mainnet (eip155:8453). The only scheme is exact-usdc, and the adapter converts your decimal price to amountWeiUsdc.

Do I have to charge for every tool?

No. Pricing is per route, so this tutorial gates one route and you would keep discovery and free tools on an unpriced one. The example charges for a single premium tool to keep the walkthrough focused; adding a free route is one more handler with no pricing entry.

Create your free agent wallet in 5 minutes.

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