KeyFlint GitHub

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

  1. 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.
  2. How much did they use? Plans sold as credits need an atomic counter that never double-charges on retries and refunds calls that fail.
  3. How do keys reach buyers? Checkout, webhook, key issuance, email and activation in the MCP client must happen without you in the loop.

Architecture

PieceRuns whereNetwork per call
License check (Ed25519 signature, expiry, features, machine/domain lock)Inside your MCP server (SDK)None
Credit debit / refund (idempotent)Your KeyFlint gateway1 request, metered tools only
Checkout → license issuance → emailStripe or Lemon Squeezy webhook → gatewayOnce 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