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 May 15, 2026

Install the Blockchain0x MCP middleware, wrap any tool handler with requirePayment, attach a price and reason, and deploy. The server returns 402 with a hosted payment URL on the first call from each caller; once the caller's wallet settles, the same call returns 200 with the result. Free tools on the same server keep returning 200 directly.

What this guide covers

The mechanical, code-level steps to take an MCP server that exposes tools for free and turn one or more of those tools into paid tools. This is the technical "how to add" guide. If you are still deciding whether to monetize and what price to charge, the how-to-monetize-mcp-server guide covers the business-side framing; the /integrations/mcp page is the broader integration reference.

You will learn how to install the middleware, wrap a single tool, inspect the 402 response your clients will see, plug in a Redis receipt cache for production, and verify that a paid MCP client can complete the full settlement cycle against your server.

Prerequisites

Before you start, have:

  • A working MCP server using the official Model Context Protocol SDK in Node or Python. If you are scaffolding fresh, start there first.
  • A Blockchain0x account with one agent profile created for your MCP server. The server itself is the payee.
  • A test API key (sk_test_).
  • Redis (or any shared key-value store) for the receipt cache - optional in development, important for production once you have more than one server instance.
  • A way to invoke your MCP server from a paid client during testing. Most teams use a local agent runtime with a Blockchain0x test wallet, or curl against the JSON-RPC endpoint directly with a mocked receipt for unit tests.

Install the middleware

Two packages: the core SDK and the MCP-specific helper that exposes requirePayment / require_payment.

BASH
# Node
npm install @blockchain0x/sdk @blockchain0x/mcp

# Python
pip install blockchain0x blockchain0x-mcp

Set the env vars your server already reads from:

BASH
BLOCKCHAIN0X_API_KEY=sk_test_01J9...
BLOCKCHAIN0X_AGENT_ID=agt_01J9...

BLOCKCHAIN0X_AGENT_ID is the Blockchain0x profile representing the MCP server itself (the entity receiving payments).

Wrap a single tool

Pick one tool on the server that you want to make paid. Wrap its handler. Everything else - the tool's name, schema, description, and registration with the MCP server - stays unchanged.

TypeScript:

TYPESCRIPT
import { Server } from "@modelcontextprotocol/sdk/server";
import { requirePayment } from "@blockchain0x/mcp";

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

server.tool(
  "get_quote_realtime",
  { ticker: { type: "string" } },
  requirePayment(
    {
      agentId: process.env.BLOCKCHAIN0X_AGENT_ID!,
      apiKey: process.env.BLOCKCHAIN0X_API_KEY!,
      priceUsdc: "0.005",
      reason: "Real-time quote",
    },
    async ({ ticker }, { receipt }) => {
      const quote = await fetchLiveQuote(ticker);
      return { content: [{ type: "text", text: JSON.stringify(quote) }] };
    },
  ),
);

Python:

PYTHON
from mcp.server import Server
from blockchain0x_mcp import require_payment
import os, json

server = Server("premium-data-mcp", version="1.0.0")

@server.tool(
    "get_quote_realtime",
    schema={"ticker": {"type": "string"}},
)
@require_payment(
    agent_id=os.environ["BLOCKCHAIN0X_AGENT_ID"],
    api_key=os.environ["BLOCKCHAIN0X_API_KEY"],
    price_usdc="0.005",
    reason="Real-time quote",
)
async def get_quote_realtime(ticker: str, receipt):
    quote = await fetch_live_quote(ticker)
    return [{"type": "text", "text": json.dumps(quote)}]

The wrapper's contract: the inner function only runs if the caller has paid. The receipt argument is populated with the validated payment receipt before the handler sees it. If the caller has not paid, the wrapper returns a 402-shaped error and the inner function is never invoked.

Inspect the 402 response

The first call from any new caller to a paid tool returns this shape:

JSON
{
  "error": {
    "code": "payment_required",
    "message": "Payment required for tool 'get_quote_realtime'",
    "payment": {
      "amount_usdc": "0.005",
      "hosted_url": "https://wallet.blockchain0x.com/.../pay/pr_01J9R6Y",
      "expires_at": "2026-05-15T09:30:00Z"
    }
  }
}

A paid MCP client (any agent runtime with a Blockchain0x wallet, or a custom client that knows the x402 protocol) does three things on seeing this:

  1. Reads the amount_usdc and checks it against its own spend policy.
  2. POSTs to the hosted_url to settle in USDC.
  3. Re-invokes the same MCP tool call, this time with the receipt attached.

The retry hits your wrapped handler, the wrapper validates the receipt, and the actual quote returns. From the agent's planner's perspective it was one tool call that took 2-3 seconds longer than usual.

Plug in a shared receipt store

By default, requirePayment caches validated receipts in process memory. That is fine for one-instance dev servers and wrong for production behind a load balancer - a paying caller hitting instance A then instance B would see a 402 on the second call. Swap in a Redis store:

TypeScript:

TYPESCRIPT
import { requirePayment, createRedisReceiptStore } from "@blockchain0x/mcp";
import IORedis from "ioredis";

const redis = new IORedis(process.env.REDIS_URL!);

const paid = requirePayment({
  agentId: process.env.BLOCKCHAIN0X_AGENT_ID!,
  apiKey: process.env.BLOCKCHAIN0X_API_KEY!,
  priceUsdc: "0.005",
  reason: "Real-time quote",
  receiptStore: createRedisReceiptStore(redis, { ttlSeconds: 3600 }),
});

Python:

PYTHON
from blockchain0x_mcp import require_payment, RedisReceiptStore
from redis.asyncio import Redis
import os

redis = Redis.from_url(os.environ["REDIS_URL"])

paid = require_payment(
    agent_id=os.environ["BLOCKCHAIN0X_AGENT_ID"],
    api_key=os.environ["BLOCKCHAIN0X_API_KEY"],
    price_usdc="0.005",
    reason="Real-time quote",
    receipt_store=RedisReceiptStore(redis, ttl_seconds=3600),
)

The TTL controls how long a paying caller gets to re-call the same tool without re-paying. One hour is the default we recommend; tune it based on how fresh your data needs to be. Volatile data (real-time quotes) wants shorter TTLs; reference data (definitions, configs) wants longer.

Validate the payment server-side

This is automatic if you use requirePayment, but understanding the validation flow matters for debugging. When a client retries with a receipt, the wrapper:

  1. Reads the receipt token from the request.
  2. Looks up the receipt in the receipt store. If cached and not expired, accept it; skip the network round trip.
  3. If not cached, calls Blockchain0x's API to validate. The API confirms the receipt was issued for this agentId, paid for at least the priceUsdc, and not been revoked or expired.
  4. If valid, stores the receipt in the cache with the configured TTL and invokes the inner handler.
  5. If invalid (forged, expired, wrong agent), returns a 402 with a fresh payment URL.

The point: you never trust the client. Receipts the wrapper accepts have been validated against the issuer. If you write a custom wrapper, do not skip this step; trusting the client's claim is the most common way to bleed revenue.

Verify with a paid client

The shortest path to "this works end-to-end" is to invoke the paid tool from an MCP client that has a wallet configured with Blockchain0x test keys. Start your server, point a test agent at it, call the paid tool, watch the 402 → settle → 200 dance in the logs.

If you do not have a paid client handy, curl with a manually-crafted receipt against the JSON-RPC endpoint also works for smoke testing. The /integrations/mcp page lists the test-environment endpoints and the sandbox-receipt format. Pricing for production - free tier, Pro, Business - is documented at /pricing/.

Five things to get right

Five small mistakes that cost teams an afternoon in the first week. Each one is cheap to fix on day one and expensive after launch.

Gating every tool by default. Free tools alongside paid tools is the whole point - clients use discovery and metadata for free and pay only for the premium operations. Wrap selectively.

Hardcoding priceUsdc for variable-cost work. Pass a callable when the work scales with input size; otherwise small inputs subsidize large ones and the unit economics drift.

Skipping the boot-time sk_test_ vs sk_live_ check. The most common production incident in the first week is deploying with the wrong key prefix.

Reusing process memory as the receipt store in production. Behind a load balancer the cache fragments per replica. Plug in Redis on day one even if you only have one instance today.

Trusting a client-supplied receipt without validation. The wrapper validates against Blockchain0x's API; if you write a custom path, do the same.

Once one tool is paid and verified, gating additional tools is a one-line change per tool. Free tools and paid tools coexist on the same server without coordination. That is the whole shape of the integration. For the business-side decisions (what to charge, which tools to gate, day-one operator metrics), 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. The wrapper is a one-line decoration around your existing tool handler. The server's tool registration, transport, schema definitions, and surrounding infrastructure all stay the same. The payment layer is additive; nothing about how MCP clients discover or invoke tools changes.

Does this work over stdio, SSE, and HTTP transports?

All three. The 402 mechanism is at the tool-response layer, not the transport layer - a tool that returns a 402-shaped response works the same way regardless of how the MCP message gets to the client. Stdio-transport servers do hit one wrinkle: the calling agent's wallet needs an out-of-band way to settle the payment URL, since stdio cannot serve a browser redirect. The wallet runtime handles this; the server does not need to know.

What stops an attacker from forging a receipt?

Server-side validation. Every receipt the wrapper accepts is validated against Blockchain0x's API before the tool runs. A client cannot mint a receipt locally and present it; the server checks with the issuer and refuses unrecognised receipts. This is the same trust model as Stripe's webhook signatures or any other receipt-based payment system.

Can different tools on the same server have different prices?

Yes. `requirePayment` is applied per tool. Tool A can be free, tool B can cost $0.005, tool C can have a price function that derives a per-call price from arguments. The wallet sees each price quoted at call time and decides whether the agent's spend policy allows it.

Do I need to expose payment status as an MCP tool?

No. The 402-and-retry dance is handled by the MCP client's wallet runtime, not by an explicit tool call. From your server's perspective the request comes in, you return either 402 or 200 based on the receipt check, and that is the whole protocol-level interaction.

How is this different from selling API access via Stripe?

Stripe expects a human-driven signup, an account, a stored payment method, and a recurring invoice. x402 on MCP expects nothing - the agent's wallet pays per call without any prior relationship. For agent-driven traffic that does not have a human to onboard, x402 collapses signup, billing, and payment into one round trip.

Create your free agent wallet in 5 minutes.

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