← Blog

How to Monetize an MCP Server: Paid Tools with x402

The Model Context Protocol solved distribution for tools. Any assistant can now call your code. What it never solved is getting paid for it, because every payment system on the web assumes a human with a credit card completes a signup form. This guide fixes that: you will price a tool, make it answer unpaid requests with a real 402 Payment Required challenge, verify and settle in USDC on Solana, publish it where agents look for paid capabilities, and test the buyer side from Claude.

Everything below runs on the same rails three.ws uses in production, and every endpoint quoted here is live while you read this. No sandbox, no test tokens, no mocks.

What you will build

  1. Why per-call payment fits agent tools
  2. The x402 handshake, seven steps
  3. Two shapes for a paid MCP server
  4. Step 1: choose work worth charging for
  5. Step 2: price it in atomic units
  6. Step 3: make the endpoint paid
  7. Step 4: charge inside a tool call
  8. Step 5: verify it against a real 402
  9. Step 6: get discovered
  10. Step 7: test the buyer side
  11. Production rules that keep you honest
  12. FAQ

Why per-call payment fits agent tools

A subscription is a bet that a customer will use a service repeatedly and predictably. An autonomous agent is the opposite: it discovers a tool the moment it needs one, calls it two or three times, and may never return. Three properties make per-call payment the natural fit.

The x402 handshake, seven steps

x402 takes the HTTP status code that has been reserved and unused since 1997 and turns it into a working payment rail. The whole protocol:

  1. The client requests your resource with no payment attached.
  2. You answer 402 with a JSON body whose accepts[] lists every payment you will take: scheme, network, asset, amount, pay-to address, timeout.
  3. The client picks one accept and signs a payment for it (an SPL transfer on Solana, a gasless EIP-3009 authorization on Base).
  4. The client retries the same request with the signed payload in an X-PAYMENT header.
  5. You verify the payment with a facilitator's /verify. Nothing has moved yet.
  6. You do the work. Only now.
  7. You settle with the facilitator's /settle, then return 200 with the receipt in X-PAYMENT-RESPONSE.

The ordering in steps 5 to 7 is the whole design. Verify before you work, so you never do unpaid work. Settle after the work succeeds, so a failure moves no money and the buyer can retry the same payment. Get this backwards and you will either be farmed for free compute or charge for errors.

Two shapes for a paid MCP server

There are two places the payment can live, and picking the wrong one is the most common early mistake.

ShapeWhere payment happensChoose it when
A. Paid HTTP endpoint An ordinary HTTPS route answers 402 and settles the X-PAYMENT header. Your capability is useful to any caller: agents, scripts, backends, MCP clients through a buyer-side payer. This is the default. You may never need to ship an MCP server at all.
B. Paid MCP tool Inside tools/call. The payment travels in _meta["x402/payment"] and the receipt comes back in _meta["x402/payment-response"]. The value is the tool ergonomics themselves: schemas, prompts, and a conversation-native surface, and you want per-tool prices inside one connector.

Most teams should build A first and add B when a client base asks for it. Shape A is a smaller surface, is testable with curl, and is what discovery indexes. Shape B is worth it when you run several priced tools behind one connector, which is how Granite over MCP exposes five metered models with a free explainer tool in front of them.

Prerequisites

Step 1: choose work worth charging for

A paid endpoint that wraps a free public API is not a business, it is a toll booth on an open road, and agents route around it as soon as one of them reads the docs. Put a candidate through three tests before writing code.

  1. Can it be done client-side? If the calling model can do it in-context, it will, and it should. Charge for what requires your data, your credentials, your hardware, or your index.
  2. Does each call cost you something? Marginal cost is what makes per-call pricing defensible instead of extractive.
  3. Is the output structured? The buyer is a language model. JSON it can reason over is worth more than prose it has to parse.

Three that pass, all running on three.ws today: model inspection (fetch a glTF or GLB, return vertex and material counts plus optimization advice), agent reputation lookup (aggregate on-chain review history), and ticker availability (check a symbol against the live registry). Each needs something the caller does not have locally.

Step 2: price it in atomic units

Prices are strings of atomic units, never floats. USDC has six decimals, so:

PriceAtomic unitsFits
$0.001'1000'Cheap lookups, validation, metadata reads
$0.01'10000'A small model call, a parse, a summary
$0.05'50000'Multi-step inference, a forecast, a render
$0.25'250000'Heavy GPU work, long generation

For calibration, here is what the live Granite tool tier charges per call: chat $0.02, code $0.025, embeddings $0.005, document analysis $0.04, time-series forecast $0.05. Agents are price sensitive in aggregate, so the practical ceiling for an impulse call with no prior relationship sits somewhere near a nickel. Above that, expect the caller to check a spend cap first, and make sure your 402 metadata explains what the money buys.

The seller side is one wrapper. Install the merchant package:

npm install @three-ws/x402-server

Then wrap the handler. Unpaid requests get the challenge, paid ones run your code:

import { paid } from '@three-ws/x402-server';

export default paid(
  {
    price: '10000',                    // $0.01 in 6-decimal USDC atomics
    asset: 'usdc',
    payTo: { solana: 'YOUR_SOLANA_ADDRESS' },
    description: 'Document summarization',
    serviceName: 'Summarize',
    tags: ['nlp', 'summary'],
  },
  async (req, res) => {
    const { text } = req.body;
    res.json({ summary: await summarize(text) });
  },
);

That is a complete paid API. The wrapper builds the v2 challenge envelope, mirrors it into the PAYMENT-REQUIRED header for header-only clients, verifies the retry against a facilitator, runs your handler, settles, and attaches the receipt. It is framework agnostic: mount it as Express or Connect middleware, a Fastify hook, or a bare Node handler.

Two chains, one config

Add a second lane and the challenge advertises both accepts. The buyer picks whichever chain it holds funds on. Solana leads when both are present.

import { paid } from '@three-ws/x402-server';

export default paid(
  {
    price: '50000',                          // $0.05
    asset: 'usdc',
    payTo: {
      solana: 'YOUR_SOLANA_ADDRESS',
      base: '0xYourBaseAddress',
    },
    network: ['solana', 'base'],
    maxTimeoutSeconds: 60,
    onSettled: (receipt) => recordSale(receipt),
  },
  async (req, res, payment) => {
    const result = await render(req.body.prompt);
    res.json({ result, paidBy: payment.payer });
  },
);

Two details worth internalizing. The handler's third argument, payment, exists only on a paid call and carries { payer, network, accept, amount }, which is your per-caller analytics key and your rate-limit key. And throwing from the handler skips settlement entirely: no funds move, the buyer gets a 500, and the same signed payment can be retried. Let your errors throw. Do not catch them into a fake success.

Response ordering

By default the wrapper buffers your response, settles, then flushes the 200 with the receipt attached, so the buyer never receives the goods before payment lands. For responses you cannot buffer, a large file or an SSE stream, set streaming: true and settlement runs first, before your handler writes a byte.

Taking a platform fee without marking up the buyer

If you host other people's tools, feeBps splits your cut out of the listed price rather than adding to it. feeSplit() computes it as floor(price × bps / 10000), and returns nothing when the fee would round to zero atomic units, in which case the creator receives the full amount. The buyer's total is whatever the challenge said. Surprise billing is how you lose a marketplace.

paid({
  price: '50000',
  asset: 'usdc',
  payTo: { solana: 'CREATOR_SOLANA_ADDRESS' },
  feeBps: 250,                                // 2.5%, capped at 1000 (10%)
  feeTo: 'YOUR_TREASURY_ADDRESS',
}, handler);

Step 4: charge inside a tool call

For shape B the payment rides in the MCP message itself. An unpaid tools/call returns a payment-required envelope quoting the price and the receiving address. The client signs, retries with the payload in _meta["x402/payment"], and the settlement receipt comes back in _meta["x402/payment-response"]. x402-capable clients run that loop without asking the user anything.

Do not hand-roll that envelope. The @x402/mcp transport package builds it, and one resource server per process handles verification and settlement for every paid tool you register:

import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
import { declareDiscoveryExtension } from '@x402/extensions/bazaar';
import { createPaymentWrapper, createToolResourceUrl } from '@x402/mcp';
import { registerExactSvmScheme } from '@x402/svm/exact/server';

const NETWORK_SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';

const resourceServer = new x402ResourceServer([
  new HTTPFacilitatorClient({ url: 'https://facilitator.payai.network' }),
]);
registerExactSvmScheme(resourceServer, {});
await resourceServer.initialize();

const resourceUrl = createToolResourceUrl('summarize_document');
const accepts = await resourceServer.buildPaymentRequirementsFromOptions(
  [{
    scheme: 'exact',
    network: NETWORK_SOLANA_MAINNET,
    payTo: process.env.X402_PAY_TO_SOLANA,
    price: '$0.02',
    maxTimeoutSeconds: 60,
    extra: { name: 'USDC', decimals: 6 },
  }],
  { resourceUrl },
);

const wrap = createPaymentWrapper(resourceServer, {
  accepts,
  resource: { url: resourceUrl, description, mimeType: 'application/json' },
  extensions: declareDiscoveryExtension({ toolName: 'summarize_document', description, transport: 'stdio', inputSchema }),
});

server.registerTool('summarize_document', schema, wrap(async (args) => ({
  content: [{ type: 'text', text: await summarize(args.text) }],
})));

The wrapper answers an unpaid call with the challenge, verifies the retry, runs your handler, settles, and attaches the receipt. declareDiscoveryExtension is the part people skip: it is what puts your tool in the catalogs agents search when they go shopping.

Ship one free tool alongside the paid ones. A tool named something like getting_started that returns your price list and explains the payment flow costs you nothing and removes the guesswork for a client that has never paid an MCP tool before. Every paid connector we run has one, and it is consistently the first tool a new caller invokes.

A complete, runnable version of this shape (one free tool, one paid tool, 15 tests, real SSRF-guarded work) is in examples/paid-mcp-server, and the step-by-step build is the tutorial Monetize an MCP server.

Step 5: verify it against a real 402

Before trusting your own implementation, look at a working one. This is a live production endpoint, and the response below is what it returns right now to an unpaid request:

curl -s 'https://three.ws/api/x402/model-check?url=https://three.ws/avatars/cesium-man.glb'
{
  "x402Version": 2,
  "error": "X-PAYMENT header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "amount": "1000",
      "payTo": "wwwwwDxFWRn7grgr3Esrsg5C6NvDoDHSA4gaCffccrU",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "maxTimeoutSeconds": 60,
      "resource": "https://three.ws/api/x402/model-check",
      "extra": { "name": "USDC", "decimals": 6, "feePayer": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW" }
    }
  ]
}

Four things to copy from it. The status is 402, not 400 or 403, because buyer libraries branch on the code. The network is a CAIP-2 identifier, not a friendly name. The Solana accept carries extra.feePayer, which is mandatory on that lane and the single most common omission. And the same price is offered on more than one chain so a caller with the wrong balance is not turned away.

Now check your own endpoint the same way, and confirm the unhappy paths: a malformed X-PAYMENT header returns a fresh 402 rather than a 500, an underpayment is rejected, and a handler that throws leaves the buyer's balance untouched.

Step 6: get discovered

A paid endpoint nobody can find earns nothing. Three surfaces matter, in this order.

One nuance about facilitators: PayAI is free, community run, and covers both Solana and Base. Coinbase CDP settlements additionally get indexed by catalogs that autonomous agents query. The protocol lets you advertise several networks in one challenge, so running both is normal and costs nothing architecturally.

Step 7: test the buyer side

The last mile is proving an assistant can find, price, and buy your tool without a human in the loop. Give Claude a self-custodial x402 wallet:

claude mcp add x402 --env SOLANA_SECRET_KEY=<base58> -- npx -y @three-ws/x402-mcp

That connector exposes four tools: x402_wallet (address and live balances), find_services (search the bazaar), inspect_endpoint (read a 402's requirements without paying), and pay_and_call (pay in USDC and return the result). The first three need no key at all, which makes inspect_endpoint the fastest way to see your own challenge exactly as a buyer sees it.

Note how the spending controls are layered, because this is the pattern to imitate if you build your own payer. pay_and_call is annotated as destructive so the client prompts first. The server probes the 402 and refuses before any money moves if the price exceeds MAX_PAY_USD, which defaults to one dollar. And with REQUIRE_CONFIRM on, the call refuses until it is reissued with confirm: true. Three independent brakes, none of which trust the seller's quoted price.

Production rules that keep you honest

FAQ

Do buyers need an account with me?

No. That is the point. A wallet with USDC and an x402-capable client is the entire onboarding. There is no signup, no API key issuance, and no billing relationship to maintain.

Which chain should I settle on?

Solana first. Fees are negligible relative to a fraction-of-a-cent call, confirmation is fast enough to sit inside a request, and it is where the agent-payment volume is. Add Base as a second accept so EVM-funded callers are not excluded, and let the buyer choose.

What stops an agent from paying for something worthless?

Buyer-side caps, which is why every serious payer enforces them independently of the seller. A price ceiling per call, an explicit confirmation step, and a pre-payment probe of the 402 mean the wallet holder sets the risk, not the merchant.

Can I charge different prices for different tools in one MCP server?

Yes. In shape B the price is per tool call, so one connector can hold a free explainer tool, a half-cent lookup, and a five-cent inference call. Quote the price in each tool's own challenge.

What happens if my handler crashes after the payment is verified?

Nothing moves. Verification is not settlement. A thrown error skips settlement, the buyer keeps their funds, and the signed payment stays valid for a retry inside its timeout window.

Is this only for AI tools?

No. Any HTTP endpoint can take payment this way: data lookups, file conversion, rendering, scraping, geocoding. Agents are simply the first buyers with a wallet and no patience for signup forms.


Further reading

Packages used here: @three-ws/x402-server (seller), @three-ws/x402-mcp (buyer), and @three-ws/x402-fetch (a paying fetch for your own clients). Source for all three is in the three.ws repository.


← All posts