How to sell a paid MCP server
Ship one server, give paying users a license key, and charge per tool call, without a phone-home check on every request and without double-charging when a call fails.
The three problems
- Who paid? The server runs on the user's machine, often offline. You need a key the server can check without network and that users cannot forge.
- How much did they use? Plans sold as credits need an atomic counter that never double-charges on retries and refunds calls that fail.
- How do keys reach buyers? Checkout, webhook, key issuance, email and activation in the MCP client must happen without you in the loop.
Architecture
| Piece | Runs where | Network per call |
|---|---|---|
| License check (Ed25519 signature, expiry, features, machine/domain lock) | Inside your MCP server (SDK) | None |
| Credit debit / refund (idempotent) | Your KeyFlint gateway | 1 request, metered tools only |
| Checkout → license issuance → email | Stripe or Lemon Squeezy webhook → gateway | Once per purchase |
1. Deploy the gateway
docker run -d -p 8000:8000 -v keyflint_data:/app/data ghcr.io/arceedev/keyflint:1.2.2
curl -s localhost:8000/readyz
First boot generates the signing key and secrets into the volume. One container with SQLite is the design; Fly.io and Render configs ship in the repo (any host works if it gives the container a persistent volume).
2. Issue a license and get the public key
docker exec <container> python -m app.cli issue --email dev@example.com --tier pro
curl -s localhost:8000/api/v1/licenses/public-key
Tokens look like LIC-PRO-<payload>.<signature>. Only the public key ships with your server, so a leaked binary cannot mint keys.
3. Protect and meter your tools
Python (pip install keyflint-verify):
from keyflint_verify.mcp import KeyFlint
kf = KeyFlint() # reads KEYFLINT_LICENSE, KEYFLINT_PUBLIC_KEYS, KEYFLINT_URL
@mcp.tool()
@kf.require_license(features=["core"]) # offline only
def search(query: str) -> str: ...
@mcp.tool()
@kf.metered(credits=5) # offline check + one idempotent debit
async def summarize(url: str) -> str: ...
TypeScript (npm install keyflint-verify):
import { createKeyFlint } from "keyflint-verify/mcp";
const kf = createKeyFlint();
server.registerTool("summarize", { inputSchema: { url: z.string() } },
kf.metered(5, async ({ url }) => ({ content: [{ type: "text", text: url }] })));
A Go verifier (sdks/go-verify) is available too. All three SDKs pass the same 50 shared test vectors.
Errors reach the client as JSON-RPC codes it can act on: -32060 invalid license, -32061 feature not in plan, -32062 credits exhausted (with a checkout_url), -32063 gateway error.
4. Sell it
Point a Stripe Payment Link (or Lemon Squeezy product) webhook at /api/v1/webhooks/stripe (or /lemonsqueezy). Each purchase issues exactly one license, emails it, and a refund revokes it. Subscriptions top up credits each billing cycle.
5. Let buyers activate in one command
uvx keyflint-verify activate LIC-PRO-... # or: npx keyflint-verify activate ...
It writes the key into the Claude Desktop, Cursor or Windsurf config and keeps a .bak copy.
How fast is the offline check?
About 0.12 ms per verification in Python when the optional cryptography package (OpenSSL) is installed, and about 3 ms with the zero-dependency pure-Python fallback. Measured 2026-11-03: Python 3.13, 2.1 GHz Xeon, best of 5 runs.
FAQ
Can users bypass the check by patching my server? Anyone who controls the machine can patch local code; no client-side scheme prevents that. Offline signatures stop key forgery and sharing across machines; metered tools are enforced on the gateway, which they do not control.
What if my gateway is down? License checks keep working offline. Only metered calls, activation and webhooks pause; Stripe retries webhooks.
Do I pay a cut per sale? No. Self-hosted, you pay only your payment processor.
See plans Star on GitHub