Skip to main content
LearnGuidesMonetize your MCP server
GUIDE

Monetize your MCP server in 10 minutes.

10 minutes
SHORT ANSWER

Install @blockchain0x/mcp, and inside a premium tool call requirePayment when the caller has not paid - it mints an x402 402 challenge with a hosted checkout URL, which you return. Once the payment.received webhook confirms settlement, you mark the caller paid in your own store and the tool runs. Free tools stay free. For a plain HTTP server, the receive-side x402 adapter does the same job.

PREREQUISITES

Before you start.

  • A working MCP server using the official Model Context Protocol SDK in Node or Python. If you do not have one yet, scaffold one with the upstream template first.
  • A Blockchain0x account and an agent profile (see the add-payments-to-agent guide for the 5-minute setup).
  • An API key (use sk_test_ for this guide).
  • A small store to remember who has paid (a database row or a Redis key) - your code owns this, updated by the payment webhook.
  • A clear sense of which tools you want to charge for and the price per call. See the paid MCP tool glossary entry for design patterns.
STEP 1 OF 4

Install the package.

@blockchain0x/mcp exports requirePayment, a pure function that mints an x402 402 challenge for a tool. It is npm (TypeScript) only. If you run a plain HTTP server instead of an MCP one, install the receive-side x402 adapter and gate routes with it.

Install
# Gate your own MCP tools with the requirePayment 402 builder:
npm install @blockchain0x/mcp

# Or gate a plain HTTP server with the receive-side x402 adapter + SDK:
npm install @blockchain0x/x402 @blockchain0x/node
STEP 2 OF 4

Gate a tool with requirePayment.

Inside the tool, check your own paid-state for the caller. If they have not paid, call requirePayment and return the resulting 402 body; if they have, run the work. requirePayment is a pure builder - it does not wrap the handler and does not track payment, so the gating policy stays in your code.

MCP tool (TypeScript)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { requirePayment } from "@blockchain0x/mcp";
import { z } from "zod";

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

server.tool(
  "get_quote_realtime",
  "Real-time quote (paid)",
  { ticker: z.string() },
  async ({ ticker }, extra) => {
    if (!hasPaid(extra)) {
      // Pure function: mint an x402 402 challenge and hand the body back.
      const { body } = requirePayment({
        amountUsdc: "0.005",
        payTo: "0xYourWallet",
        hostedUrl: "https://pay.blockchain0x.com/checkout/abc",
      });
      return { content: [{ type: "text", text: JSON.stringify(body) }], isError: true };
    }
    const quote = await fetchLiveQuote(ticker);
    return { content: [{ type: "text", text: JSON.stringify(quote) }] };
  },
);
Plain HTTP server (receive-side x402)
import express from "express";
import { createX402Middleware } from "@blockchain0x/x402/server/express";
import { createClient } from "@blockchain0x/node";

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

// Not an MCP server? Gate a plain HTTP route the same way. The middleware
// answers unpaid requests with a 402 and lets paid ones through.
// Configure the price and recipient per the x402 docs.
app.use("/quote", createX402Middleware({ sdk }));

The 402 body requirePayment returns to an unpaid caller:

// requirePayment returns { status: 402, body }. The body an unpaid caller sees:
{
  "error": "payment_required",
  "amountUsdc": "0.005",
  "payTo": "0xYourWallet",
  "hostedUrl": "https://pay.blockchain0x.com/checkout/abc",
  "network": "mainnet"
}
STEP 3 OF 4

Confirm payment, then remember who paid.

When a caller pays the checkout, Blockchain0x POSTs a signed payment.received event to your webhook. Verify it with webhooks.verify from @blockchain0x/node, then write the paid state to a store you control - a database row, a Redis key, your call. That store is what the tool checks in Step 2. There is no shipped receipt cache; you own where paid-state lives and how long it lasts.

Webhook handler (TypeScript)
import express from "express";
import { webhooks } from "@blockchain0x/node";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/payment", (req, res) => {
  const result = webhooks.verify({
    headers: req.headers,
    rawBody: req.body, // RAW bytes
    secret: process.env.BLOCKCHAIN0X_WEBHOOK_SECRET!,
  });
  if (!result.ok) return res.status(400).json({ code: result.code });

  if (result.eventType === "payment.received") {
    // Remember the payer however you like - a DB row, a Redis key, your call.
    markPaid(result.eventId);
  }
  res.status(200).send("ok");
});

How long a payment grants access is your decision - a single call, a session, an hour. Set an expiry on the paid-state key that matches how you price the tool. Long enough that a normal session reuses one payment, short enough that abuse is bounded.

STEP 4 OF 4

Deploy and verify.

Ship the server. Free tools should still return their result immediately; gated tools should return the 402 body on the first call from a fresh client, then run once that caller is marked paid. Verify both paths on a sk_test_ key against Base Sepolia before going live.

Two signals to watch on day one: the count of 402s returned (your top-of-funnel) and the count of successful tool runs after a 402 (your conversion). If conversion is much lower than expected, the price is probably wrong. Watch your paid-state store's hit rate too - if it is near zero, your access window is too short and paying callers are being asked to pay again.

COMMON PITFALLS

Five things that bite first-time MCP monetizers.

Gating the free tools by accident

It is tempting to gate every tool 'just in case'. Do not. The whole value of paid MCP servers is that free tools coexist with paid ones on the same server, so the client can use the free discovery and metadata tools without paying. Mint a 402 only for the tools that actually consume premium resources; leave the rest as plain results.

requirePayment is a builder, not middleware

requirePayment is a pure function: you call it when a tool is unpaid, it returns { status: 402, body }, and you hand the body back. It does not wrap your handler and it does not track who paid. It takes amountUsdc, payTo, hostedUrl, and an optional network and description - nothing else. Whether a caller has paid is a check you run against your own store.

There is no shipped receipt cache

Blockchain0x ships the 402 builder and the settlement webhook, not a receipt-store helper. You decide where 'this caller paid' lives - a database row, a Redis key, an in-memory map for a single process - and you flip it when the payment.received webhook arrives. That keeps the policy (how long a payment grants access) entirely in your hands.

Trusting a receipt the client claims to have

Do not let the caller assert it paid. The source of truth is the payment.received webhook, verified with webhooks.verify (or the documented HMAC) against your webhook secret. Mark the payer paid only after a verified event, and gate the tool on that server-side state - never on something the client sends.

No metrics on paid-tool latency

Putting a payment step between the client and tool execution adds the time the caller takes to pay and settle on the first call, then near-zero once you have marked them paid. Instrument both branches so you can tell 'tool is slow' from 'payment is slow' when a customer complains. Without the metric you will misdiagnose the bottleneck.

NEXT STEPS

Once paid traffic is flowing.

With monetization in place, the most useful follow-ups are dependable webhook handling (so you do not miss payment events), spend controls (so an MCP server you build that also pays other agents stays bounded), and a testnet-first flow (so you can ship pricing changes without burning real money).

Full API reference at docs.blockchain0x.com. Related product surface: MCP integration.

Last reviewed: 2026-05-15. Published under CC BY 4.0.

Charge per tool call.

Return 402, set your price, accept USDC. Free to start.