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.
mkdir paid-mcp && cd paid-mcp && npm init -y
npm install @modelcontextprotocol/sdk fastify @blockchain0x/node @blockchain0x/x402Set the env the server will read:
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 routeRegister 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.
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.
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.
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:
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8080/mcp # expect 402Then 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.
// 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.