Skip to content

Guides

Recipe: Paid MCP server

Charge for an MCP server's tools per call with credits. A person buys a pack once; each call spends, then runs.

View as Markdown

Agents should not pay on their own. A person buys a credit pack in their browser, then every tool call checks the balance, spends credits with an idempotency key, and runs. Out of credits, the tool answers with a top-up link the agent shows. No x402, no wallet inside the agent, nothing debited on its own.

With an assistant: the paid-mcp-tools skill of the Perly Pay plugin for Claude Code does this recipe in your project — the plugin and its example, examples/paid-mcp-server (MIT), are on their way. Until then, any assistant can read the same skill from /.well-known/skills/perly-pay-paid-mcp-tools/SKILL.md.

How it works

  1. 1

    A credit pack

    As in API credits. Give each tool a cost and put it in the tool's description.

  2. 2

    Who is calling

    A hosted server maps each caller's API key to a customer external_id. Only your server holds PERLY_PAY_SECRET_KEY.

  3. 3

    Charge, then run

    credits.consume with the tool's name as action and a key made of the session id and the request id (JSON-RPC ids start again every session), then do the work.

  4. 4

    Short?

    Return a tool result with isError: true and the top-up link, not a protocol error: clients show it.

  5. 5

    Free helpers

    balance and top_up tools, so an agent can explain what is going on.

A paid tool
server.registerTool("summarize", { description: "Costs 3 credits.", inputSchema: { text: z.string() } }, async ({ text }, extra) => {
  const refused = await charge(userId, 3, "summarize", extra.requestId); // credits.consume, or a top-up message
  if (refused) return { isError: true, content: [{ type: "text", text: refused }] };
  return { content: [{ type: "text", text: summarize(text) }] };
});

Run the example

  1. 1

    Install

    npm install in examples/paid-mcp-server (Node 20.12+).

  2. 2

    Key

    Copy .env.example to .env and put a test key (sk_test_…) in PERLY_PAY_SECRET_KEY.

  3. 3

    Set up

    npm run setup makes the product and price with your key and prints their ids for .env.

  4. 4

    Run

    npm start (or npm run dev), as its README says.

  5. 5

    Pay without a wallet

    Add it to Claude Code (claude mcp add paid-tools -- npx tsx "$PWD/src/server.ts"), run word_stats: it answers with a top-up page. Pick the pack, press Simulate payment and run it again.

Pitfalls

  • Putting PERLY_PAY_SECRET_KEY in an MCP config other people copy. Host the server, or have it call your backend.
  • Charging after the work: a failed charge then means free work.
  • Keying idempotency on the JSON-RPC id alone: a new session reuses it and replays an old charge.