●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Claude × Stripe Meter Events — Building a Usage-Based SaaS
A production-ready architecture and full implementation for billing Claude API token consumption through Stripe Meter Events — covering idempotency, usage caps, and a hybrid base + overage pricing model.
"The more your users hit Claude API, the higher your cost climbs in lockstep — but you're charging them a flat monthly fee. Are you really about to ship this SaaS at a planned loss?"
This is the question I asked myself over and over before launching an API-driven SaaS as a solo dev. Flat monthly pricing is easy to design, but a single power user can crush your margin. Pure usage-based pricing has the opposite problem — it raises the psychological barrier and tanks conversion.
In this article, you'll build a hybrid: a flat monthly plan plus token overage charges, billed accurately through Stripe Meter Events. The stack assumes Cloudflare Workers + Stripe + Cloudflare KV, but the patterns translate to other stacks.
Why Stripe Meter Events Over Self-Built Counters
There are roughly three ways to implement usage-based billing: roll your own counter in your database and charge at month-end, use Stripe's older Usage Records API, or use Stripe Meter Events.
I landed on Meter Events for three reasons.
First, real-time aggregation. Sent events appear in the Stripe Dashboard within seconds to minutes, so per-customer consumption is visible immediately. Self-built counters require an end-of-month batch — and recovering from batch errors is painful.
Second, idempotency is built in. Meter Events takes an identifier field for deduplication, which makes it straightforward to prevent double charges from webhook retries.
Third, integration with the Stripe Customer Portal. If users want to see their own consumption, you just point them at the Stripe-hosted portal. Skipping a custom dashboard is a real win for a solo developer.
That said, Meter Events isn't perfect. The pricing formula lives in the Stripe Dashboard, so if you want complex tiered pricing or time-of-day rates, you'll combine Meter Events with self-side calculations.
The Five-Layer Architecture
Before writing code, let's separate the system into five layers. Without clear responsibility boundaries, debugging usage-based billing becomes a nightmare of "where did this number come from?"
Layer 1: Stripe Dashboard configuration — create the Meter, attach it to a Price, and wire the Price to a Product on the Stripe side.
Layer 2: API gateway — receive client requests, authenticate the user, check quotas, and proxy to Claude.
Layer 3: Claude API client — call Claude through the Anthropic SDK, then extract token counts from the usage field in the response.
Layer 4: Meter Events sender — send those token counts to Stripe Meter Events, with proper idempotency keys and retries.
Layer 5: Monitoring — receive Stripe webhooks, store consumption summaries in KV, and surface real-time numbers in the user-facing UI.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll get the working code to bill Claude API token usage through Stripe Meter Events accurately, copy-paste ready
✦You'll preempt the production traps of usage-based billing — idempotency keys, retry on send failures, automatic caps to prevent runaway charges
✦You'll be equipped to design a hybrid plan combining a flat monthly base with token-overage billing on Cloudflare Workers + KV + Stripe
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Sign in to the Stripe Dashboard and head to "Billing → Meters" to create a new meter. Here's the configuration I run in production:
Display name: Claude API Token Usage
Event name: claude_token_usage
Aggregation: Sum
Payload key: tokens (the value of this field becomes the billable quantity)
Customer mapping: stripe_customer_id (the field used to identify the customer when sending events)
Once the meter exists, create a Product and a Price. When creating the Price, choose "Meter pricing" and attach the meter you just created. Set the unit cost — for example, "$20 per 1M tokens."
Tier 1: 0 - 1,000,000 tokens => $0.00 (included with base plan)
Tier 2: 1,000,001 tokens and beyond => $20.00 / 1M tokens
A structure like "$30 base monthly + 1M tokens included + overage charged" lets light users sit on the fixed cost while heavy users start triggering metered charges.
Step 2: Capture Token Counts From the Claude API Response
The Claude API response includes input and output token counts in the usage field. Extracting that and forwarding it to Stripe is the heart of this implementation.
// src/lib/claude-with-metering.tsimport Anthropic from "@anthropic-ai/sdk";import { sendMeterEvent } from "./stripe-meter";const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!,});export interface ClaudeRequest { userId: string; stripeCustomerId: string; prompt: string; model?: string;}export async function callClaudeWithMetering(req: ClaudeRequest) { // 1. Quota check (read this user's monthly usage from KV) const usageThisMonth = await getMonthlyUsage(req.userId); if (usageThisMonth > 5_000_000) { // Hard stop at 5M tokens to prevent runaway billing throw new Error("Monthly token limit exceeded"); } // 2. Call Claude API const response = await anthropic.messages.create({ model: req.model ?? "claude-sonnet-4-6", max_tokens: 2048, messages: [{ role: "user", content: req.prompt }], }); // 3. Pull token counts from the usage field const inputTokens = response.usage.input_tokens; const outputTokens = response.usage.output_tokens; const totalTokens = inputTokens + outputTokens; // 4. Send to Stripe Meter Events with an idempotency key // response.id is unique on Anthropic's side, so it's ideal as the dedup key await sendMeterEvent({ customerId: req.stripeCustomerId, tokens: totalTokens, identifier: response.id, }); // 5. Increment local usage counter for quota enforcement await incrementMonthlyUsage(req.userId, totalTokens); return { content: response.content, usage: { inputTokens, outputTokens, totalTokens }, };}
The critical piece here is Step 1: the quota check. Stripe's meter is for billing aggregation, not enforcement. You can keep sending Meter Events forever and Stripe will keep tallying — it never stops you. Hard quota logic lives in your application.
Why a Separate KV Counter Instead of Querying Stripe?
You might think, "Why not just query Stripe for current usage?" The answer is latency. Pulling current usage from the Stripe API has seconds-to-minutes of lag, which makes it useless for rate-limit decisions on the request path.
A low-latency store (KV or Redis) lets you increment per-request and check usage in sub-millisecond time. Either run a monthly reset batch, or align the TTL with the start of next month.
Step 3: Implement Meter Events Send With Retries
Meter Events looks deceptively simple. Idempotency, retries, and failure behavior all need to be designed up front.
// src/lib/stripe-meter.tsimport Stripe from "stripe";const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-10-28.acacia",});export interface MeterEventArgs { customerId: string; tokens: number; identifier: string;}export async function sendMeterEvent(args: MeterEventArgs) { let attempt = 0; const maxAttempts = 3; let lastError: unknown; while (attempt < maxAttempts) { try { const event = await stripe.billing.meterEvents.create({ event_name: "claude_token_usage", payload: { stripe_customer_id: args.customerId, tokens: args.tokens.toString(), // must be a string }, identifier: args.identifier, // same identifier => counted once }); return event; } catch (err) { lastError = err; attempt++; // Exponential backoff: 1s -> 2s -> 4s await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt - 1))); } } // After 3 failures, send to a Dead Letter Queue for later retry // Throwing here would mean Claude succeeded but billing did not — silent loss console.error("[CRITICAL] Meter event failed after 3 attempts", lastError); await saveToDLQ(args); return null;}
Idempotency Key Design Is Non-Negotiable
The identifier field is what keeps your billing safe. If you send the same identifier multiple times, Stripe counts it exactly once.
My recommendation: use the Anthropic API response's response.id directly. Anthropic guarantees uniqueness on their side, so retries and concurrent processing can never cause double-billing.
If you generate your own UUIDs, you must "create the UUID before the request, persist it, then use the same UUID when sending to Stripe." Generating a fresh UUID on each retry breaks idempotency.
Why a Dead Letter Queue Matters
Three retries can still fail in rare scenarios — network blips, Stripe outages, etc. If you bubble the exception up to your handler at that point, the user got their Claude response but you never billed for it (a silent loss on your side).
Stash unsent events in a DLQ — Cloudflare Queues, Durable Objects, or even a KV list works — and run a separate batch to retry them.
Step 4: Quota Enforcement to Prevent Runaway Charges
The scariest scenario in usage-based billing is a malicious user (or an infinite-loop bug) that burns through your account. To prevent waking up to a $10,000 daily bill, layer in three quota checks.
Layer 1: per-user monthly cap — managed in KV, evaluated on every request.
Layer 3: service-wide monthly cap — your last line of defense if the per-user controls slip.
// src/lib/rate-limiter.tsimport type { KVNamespace } from "@cloudflare/workers-types";interface RateLimiterArgs { kv: KVNamespace; userId: string; tokens: number;}export async function checkAndIncrementUsage(args: RateLimiterArgs) { const now = new Date(); const monthKey = `usage:${args.userId}:${now.toISOString().slice(0, 7)}`; const minuteKey = `rate:${args.userId}:${now.toISOString().slice(0, 16)}`; // Layer 1: monthly cap (5M tokens) const monthly = parseInt((await args.kv.get(monthKey)) ?? "0", 10); if (monthly + args.tokens > 5_000_000) { throw new Error("Monthly limit exceeded"); } // Layer 2: per-minute cap (50K tokens) const perMinute = parseInt((await args.kv.get(minuteKey)) ?? "0", 10); if (perMinute + args.tokens > 50_000) { throw new Error("Rate limit: too many tokens per minute"); } // If we cleared both, increment counters // expirationTtl auto-deletes them — no monthly reset job needed await args.kv.put(monthKey, String(monthly + args.tokens), { expirationTtl: 35 * 24 * 60 * 60, // 35 days }); await args.kv.put(minuteKey, String(perMinute + args.tokens), { expirationTtl: 120, // 2 minutes });}
Watch out for KV's eventual consistency. If a single user fires concurrent requests, both might pass the cap check simultaneously. For strict correctness, switch to Cloudflare Durable Objects or pair this with Stripe's quota-notification webhooks.
Step 5: Sync Billing State Through Webhooks
Sending Meter Events alone doesn't give users a "how much have I used this month?" view. Add a Stripe webhook handler that writes consumption summaries to KV.
// src/api/stripe-webhook.ts (Cloudflare Workers / Hono assumed)import type { Context } from "hono";import Stripe from "stripe";export const handleStripeWebhook = async (c: Context) => { const stripe = new Stripe(c.env.STRIPE_SECRET_KEY, { apiVersion: "2024-10-28.acacia", }); const sig = c.req.header("stripe-signature"); const body = await c.req.text(); let event: Stripe.Event; try { // On Cloudflare Workers, use constructEventAsync (the sync version doesn't work) event = await stripe.webhooks.constructEventAsync( body, sig!, c.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return c.json({ error: "Invalid signature" }, 400); } // Subscribe to invoice.created and write meter line items back to KV if (event.type === "invoice.created") { const invoice = event.data.object as Stripe.Invoice; const customerId = invoice.customer as string; // Loop line_items, find meter-related lines, write summary to KV // ... } return c.json({ received: true });};
On Cloudflare Workers you must call stripe.webhooks.constructEventAsync. The synchronous constructEvent uses Node's crypto module internally, which doesn't run in the Workers runtime.
Step 6: Monitoring for Production
Once everything is wired together, set up the monitoring you'll need on day one. At minimum, watch these three signals alongside the Stripe Dashboard:
DLQ depth: alert immediately on any non-zero count
Meter event send failure rate this month: investigate above 0.1%
Per-user token usage outliers: highlight anyone above 10x the average
In my own setup, I write event logs to Cloudflare Analytics Engine and visualize them in Grafana. Cross-checking against Stripe Dashboard meter values is how I catch "events that never made it to Stripe."
Common Mistakes and Pitfalls
Here are the traps I personally hit during implementation.
Mistake 1: Sending tokens as a number
Stripe Meter Events payloads only accept strings. Sending a number returns 400. Don't forget tokens.toString().
Mistake 2: Discarding response.id too early
If your code chains into other work right after the Claude call and discards response.id, retries can no longer use the same idempotency key. Save it the moment you get the response.
Mistake 3: Forgetting expirationTtl on KV
Without TTLs, your rate-limit keys grow KV storage indefinitely. Cloudflare KV's free tier is 1GB and writes have non-trivial cost — always set TTL.
Mistake 4: No idempotency on Stripe webhooks
Stripe sometimes sends the same event multiple times. Without an event.id-keyed idempotency check on your side, your KV summaries get double-incremented.
Mistake 5: Sending dev events to the production meter
Either separate dev and prod Stripe accounts, or split meter names (dev_claude_token_usage vs claude_token_usage). I learned this the hard way after dev test events polluted the production meter.
Designing the Flat + Overage Hybrid Plan
Finally, here's the plan structure I run in production:
Starter: $0/month + 100K tokens free, then $25 / 1M
Pro: $30/month + 2M tokens included, then $20 / 1M
Business: $200/month + 20M tokens included, then $15 / 1M
Notice that higher monthly tiers come with cheaper overage rates. This naturally pulls heavy users up the plan ladder.
On Stripe, you create one Price per plan and apply tiered pricing on the same shared meter. Enabling self-serve plan changes through the Customer Portal removes a lot of support load.
Run It Through Stripe Test Mode First
Don't ship all of the above straight to production. Run the full flow through Stripe test mode first, using the test card 4242 4242 4242 4242.
Three things to verify: Meter Events appear in the Dashboard, the end-of-month invoice is calculated correctly, and DLQ recovery actually moves stuck events through. Once those three pass, you're ready to flip to live.
Showing Usage to Users via the Customer Portal
Letting users see their own monthly usage massively reduces support volume. Stripe's Customer Portal supports metered products out of the box — with just a configuration change, users see their consumption and projected charges.
// src/api/portal-session.tsimport Stripe from "stripe";export const createPortalSession = async ( stripeCustomerId: string, returnUrl: string) => { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-10-28.acacia", }); const session = await stripe.billingPortal.sessions.create({ customer: stripeCustomerId, return_url: returnUrl, }); // Redirect the user to session.url return session.url;};
In the Stripe Dashboard, open "Settings → Customer portal" and enable "Show subscription usage." Now users see their own usage and the projected next-invoice total in real time.
You can build a custom usage screen later, but the Stripe Portal is more than enough for early-stage products. Switch to a custom UI only when you need branding consistency, and not before.
Refunds and Credit Notes
Refunds are a surprisingly common stumbling block in usage-based billing. Meter Events you've already sent cannot be retroactively cancelled. To refund users, issue a Credit Note on the Stripe side, which offsets the next invoice.
When you create the credit note, also adjust the in-month KV counter. If you skip that, the user's "this month's usage" view will diverge from the actual invoice amount.
Multi-Tenant Considerations
If your B2B SaaS has multiple organizations sharing the same system, the first design decision is the granularity of the meter. Per-organization or per-user-within-organization? The choice shapes everything downstream.
My recommendation: one Stripe Customer per organization, and store per-user usage breakdowns as your own metadata. Stripe billing aggregates at the org level; you compute internal allocation in your own DB.
Pros: the Stripe Dashboard stays clean, plan changes between orgs are simple, and you get full freedom in how you slice "who used what." Cons: you're responsible for per-user enforcement inside the org.
Batching Meter Events to Cut Latency and Cost
If you call the Stripe API on every request, you add tens to hundreds of milliseconds of latency. For high-frequency APIs that hurts perceived performance.
A common pattern is to buffer meter events and flush them on a 1-second interval. On Cloudflare Workers, use Durable Objects; on AWS Lambda, SQS + Lambda.
The catch: if your process crashes mid-buffer, those events are lost. Persist critical billing data temporarily to KV or DB, and replay anything unsent on startup.
Unit Economics — Surface Your Break-Even
Finally, work out where your unit economics actually sit. Skipping this and discovering "we make less than expected" only after launch is a painful pattern.
For a "Pro plan $30/month + 2M tokens included + $20 per 1M overage" structure, anchored on Sonnet 4.6 (output at $15 per 1M), per-user monthly cost caps at roughly $30, leaving you near 50% gross margin.
Now consider a heavy user burning 10M tokens per month. The 8M overage triggers $160 in additional charges. Your cost on that 8M is 8 × $15 = $120, leaving $40 of additional gross margin. That's the plan working as designed.
Document this calculation in Notion or a spreadsheet and track gross margin against active user count monthly. That early visibility is what saves you from misjudging when to raise prices.
Migrating From an Existing Flat-Rate Plan
If you already have customers on a flat-rate plan and want to introduce metered billing, the migration sequence matters. Forcing existing customers onto a new structure overnight is a churn event waiting to happen.
The pattern I use is a four-phase rollout. First, add the metered Price as a new option in your Stripe catalog — without touching anyone's existing subscription. Second, instrument your application to record token usage even for legacy users, but don't attach the meter to their Subscription yet. Watch the data for a few weeks to understand the real consumption distribution.
Third, identify which legacy customers would actually pay more under the new plan. For most SaaS products, the bottom 80% of users sit comfortably below the included token threshold and would not see a price change. Communicate the upcoming change clearly to that majority — they can stay flat-rate or migrate.
Fourth, invite the heavy users to the new plan with a transition discount. "$20 / month off the new plan for the first three months" softens the cost shock and gives you a structured negotiation window. Most heavy users either accept and stay, or accept and grow into the new structure.
Skipping the data-gathering phase is the most common mistake. You'll either price too high (losing the bottom 80%) or too low (still losing money on power users). Two to four weeks of usage data informs every other decision.
Tax Handling for Usage-Based Charges
If you sell internationally — and most SaaS does — usage-based billing intersects with tax in non-obvious ways. The amount each customer pays varies month to month, which means VAT/JCT/GST calculations also vary.
Stripe Tax handles this automatically when configured. Enable Stripe Tax for your account, mark each Product as taxable, and Stripe computes the tax per invoice based on the customer's verified address. The tax amount is shown alongside the metered charges on the same invoice.
The piece that's easy to miss: customer address verification. If you only collect billing addresses at checkout and never re-verify, customers who move countries can produce invoices with stale tax rates. Build a periodic re-verification flow — even just an email asking customers to confirm — to keep the data fresh.
For Japanese customers specifically, JCT (Japan Consumption Tax) is 10% as of writing, and Stripe Tax handles registration thresholds automatically. If your annual taxable sales in Japan cross the registration threshold, Stripe will surface a notification.
Looking back — Start With the Smallest Working Slice
Don't try to build all six layers at once. Start with the smallest path to a working invoice: create one Meter, send a single hardcoded Meter Event, watch it appear in the Dashboard, then walk it up to a real end-of-month invoice.
Once that round-trip works, layer in idempotency, retries, the DLQ, the rate limiter, and the customer-facing portal in that order. Each layer should ship to production independently so you can roll back any single change without taking down billing.
The first metered invoice that appears in your Stripe Dashboard, with a clean breakdown of base + overage, is the milestone worth aiming for. Everything after is optimization.
Operational Lessons From Three Months of Production
After running this stack in production for three months, a few things stand out beyond the implementation itself.
The first lesson: monitor the gap between Meter Events sent and Stripe-acknowledged events daily. Network blips occasionally mean events sent to Stripe never make it to their meter aggregation. Diff your local "events sent" count against Stripe Dashboard's meter total at the end of each day; the diff should be zero or trending toward zero as the DLQ retries clear.
The second lesson: prepare a manual override path for customer support. Inevitably, a customer will dispute a charge, or you'll discover that an internal bug double-counted some segment of their usage. You need a way to issue credit notes or adjust the next invoice without manual SQL surgery. Wire a small admin UI to the credit note API early.
The third lesson: price changes are easier than expected if your meter is stable. Once token-counting is reliable, swapping one Price for another (e.g., dropping the per-1M rate from $20 to $18) is a Stripe-side operation that takes minutes. The hard part is the underlying measurement, not the pricing surface.
The fourth lesson: publish a transparent pricing page with a usage estimator. Customers evaluating usage-based plans often hesitate because they can't predict their bill. A simple slider — "if you process X requests per month, your bill is $Y" — converts evaluators into customers far better than a static price list. Build the estimator from your own median user data once you have a few months of real usage.
The fifth lesson: keep the unit price simple at launch, complicate later. A single tier ("$X per 1M tokens, billed monthly") is far easier to explain than five tiers with different overage rates. Customers can model their costs in their head. Tier complexity should only emerge once you have data showing distinct customer segments with distinct willingness-to-pay.
These five lessons don't appear in any Stripe documentation, but each one came out of an actual production incident I'd rather you avoid.
Final Checklist Before Going Live
Before you announce the new metered plan to customers, walk through this checklist one more time. I keep a copy of it in my project README and tick each box manually before each major billing change.
First, confirm that your DLQ is empty and the retry job has been running cleanly for at least seven days. Any backlog suggests a configuration problem that will compound under real production load.
Second, run a end-to-end synthetic transaction every hour for 24 hours: create a test customer, make API calls, watch the Meter Event arrive in Stripe, generate a draft invoice, and verify the line items match expected values. A daily synthetic gives you confidence that the pipeline isn't silently failing.
Third, verify your billing failure handling: what happens if Stripe declines a card mid-month? Customers should still be served (or gracefully degraded), but their next request should trigger a payment update flow. This is policy more than code, but the policy needs to be explicit.
Fourth, document your incident response plan: if billing breaks for 2 hours, what's your communication template? Who pages whom? Most billing systems work for months without issue, but the one time something does break, you'll be glad you wrote the playbook in advance.
Once those four are clear, ship it. The first metered invoice that lands in your account, with token counts that match what you actually served, is the moment this whole architecture justifies itself.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.