MCP · Stripe ·

How to Charge for an MCP Server with Stripe Usage Billing

Building a useful MCP server takes a weekend. Getting paid for it is the hard part. You need to know who is calling, count what they used, and bill it without double-charging when something retries. This guide walks through the architecture using Stripe's usage-based billing. Stripe details were checked against Stripe's docs on recording usage, configuring meters and webhook signatures, and MCP details against the MCP specification (current version 2026-07-28), all on 14 Oct 2026.

This guide pairs with Ship-Ready AI Rules, and if you want the next step, read Supabase Service Role Key Exposed? How to Check, Rotate and Stop It Happening Again.

The architecture in one picture

MCP client (Claude Code / Cursor) ── Authorization: Bearer <api key> ──▶ /api/mcp (Streamable HTTP)
                                                                         │
                       1. hash key → look up user      2. rate limit     │
                       3. check free quota / plan      4. run the tool   │
                       5. record usage row (DB)        6. send meter event to Stripe
Stripe webhook ──▶ /api/stripe/webhook  (verify signature → update plan)

1. Identify the caller with API keys

A remote MCP server is just an HTTP endpoint, so the simplest way to identify users is an API key sent as a bearer token. - Store only a hash of each key, never the key itself. Show the key once at creation. - On every request, hash the incoming key and look it up. Reject unknown or revoked keys with 401. - Let users create and revoke keys from a dashboard.

Authorization is optional in the MCP specification. When a server does implement the spec's authorization flow, it's based on OAuth 2.1. A static API key in a header is simpler and works with clients that let the user set request headers in their config. Some clients, such as web-based connectors, expect the OAuth flow instead, so check what your target clients support before you pick an auth scheme.

2. Create a meter and a usage-based price

In Stripe, usage billing has two parts: - The meter: in the Dashboard, open the Meters page and click Create meter. Give it an event name such as mcp_tool_calls and set the aggregation method to Sum over the payload key value. - The price: create a recurring price that uses that meter (for example, $0.01 per unit), then subscribe customers to it through Checkout.

Never accept a price ID from the client. Pick the price on the server, so a user can't swap in a cheaper one.

3. Send a meter event per paid call, idempotently

After a paid tool call succeeds, record it. With the Node SDK:

await stripe.billing.meterEvents.create({
  event_name: "mcp_tool_calls",
  identifier: usageRowId,               // unique per call → retries can't double-bill
  payload: {
    stripe_customer_id: customerId,
    value: String(units),
  },
});

The Stripe facts that shape this design: - identifier deduplicates. Stripe uses it to stop the same usage being reported more than once, and it generates one if you don't set it. Use your own database row ID, so a retry sends the same identifier. - Processing is asynchronous. Meter summaries and upcoming invoices can lag behind recent events. - Timestamps must be within the past 35 calendar days and no more than 5 minutes in the future (Stripe: event timestamps). Report usage before the period's invoice finalises, or it may never be billed. - Rate limits: in live mode, the meter event endpoint allows 1,000 calls per second per account and one concurrent call per customer per meter. Above that, pre-aggregate or use the v2 meter event streams. (Source: Stripe, recording usage with the API.) - Error events: invalid events surface asynchronously as v1.billing.meter.error_report_triggered (a thin event). Subscribe to it if you want alerts.

Write first, send second. Insert a usage_events row, then call Stripe, then mark the row as sent. A cron that re-sends unsent rows (well inside the 35-day window) means a Stripe outage delays billing but doesn't lose it.

4. Give a free tier, then an upgrade path

Count usage per user per month from your own usage_events table. Don't use Stripe for this, because Stripe's numbers lag. When a free user hits their quota, return a normal tool result that explains the limit and includes an upgrade (Checkout) link. That's friendlier than an opaque error, and the agent can show it to the user.

5. Grant access only from verified webhooks

Don't upgrade a user because they landed on your success page. - Verify the webhook with stripe.webhooks.constructEvent(rawBody, signature, secret). Stripe's docs are explicit that this needs the raw request body string Stripe sent. In a Next.js route handler that means await req.text(), not req.json(). - Store processed event IDs so a redelivered event isn't applied twice. (Granting access from the success page is one of the bugs in our free Supabase + Stripe security checklist.) - Then update the user's plan.

6. Rate-limit per user

Billing doesn't protect you from a runaway agent loop. Add a per-user limit, for example an atomic counter in Postgres per time window. When it's exceeded, respond with 429 and a Retry-After header.

7. Show users what they're paying for

A small dashboard with plan, usage this period, usage by tool, and API keys cuts down "why was I charged?" support emails a lot.

Checklist

  • [ ] Keys hashed, revocable, 401 on bad keys
  • [ ] Meter + metered price created in test mode first
  • [ ] One meter event per paid call, with identifier = your usage row ID
  • [ ] Usage written to the DB before the Stripe call, plus a re-send cron
  • [ ] Free quota counted from your DB, upgrade link in the tool result
  • [ ] Webhook verifies the raw body and de-duplicates event IDs
  • [ ] Per-user rate limit with 429 + Retry-After

Skip the plumbing

Disclosure: Paid MCP Starter is our own product.

Paid MCP Starter implements the above and ships with its own test suite: - Stack: a Next.js 16 + Supabase remote MCP server on the official TypeScript SDK v2. - Billing: hashed API keys, a free monthly quota, and Stripe Billing Meters with idempotent identifiers and a re-send cron. - Hardening: a hardened webhook, an atomic Postgres rate limiter, and a usage dashboard. - Tests: 42 tests covering protocol, auth, quotas, metering, outage recovery, rate limits, webhook signatures and SQL/RLS. Wrap any handler with paid(deps, "my_tool", units, handler). $49 one-time. - Honest limits: auth is API keys, not OAuth, and live Supabase and live Stripe calls weren't tested, so run it in Stripe test mode first.

Building it with Claude Code? Pair it with Ship-Ready AI Rules v2 ($19). Before you go live, run Launch Audit ($12) to check the webhook, keys and RLS. AI Rules and Launch Audit also come together with Solo Founder OS Pro in the Ship It Bundle ($39). It doesn't include Paid MCP Starter.