# 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.

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.

> **Note:** 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. **A credit pack** — As in [API credits](https://pay.perly.io/docs/recipe-api-credits.md). Give each tool a cost and put it in the tool's description.
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. **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. **Short?** — Return a tool result with `isError: true` and the top-up link, not a protocol error: clients show it.
5. **Free helpers** — `balance` and `top_up` tools, so an agent can explain what is going on.

**A paid tool**

```js
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. **Install** — `npm install` in `examples/paid-mcp-server` (Node 20.12+).
2. **Key** — Copy `.env.example` to `.env` and put a test key (`sk_test_…`) in `PERLY_PAY_SECRET_KEY`.
3. **Set up** — `npm run setup` makes the product and price with your key and prints their ids for `.env`.
4. **Run** — `npm start` (or `npm run dev`), as its README says.
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.
