I pushed a brand‑new Claude proxy to production on a Friday night, confident that the token‑count middleware was rock‑solid. By midnight the finance team was yelling about “ghost usage” – the system had billed **twice** for every request because a retry loop replayed the same metering event. The whole thing came crashing down until I added an idempotency key and a dead‑letter queue. The lesson? Metered billing is only as good as its safety nets.
- Intercept Claude calls and pull token counts at the edge.
- Use Redis for fast per‑request metering, fall back to a durable ledger.
- Record every increment as an immutable event – PostgreSQL or DynamoDB work.
- Trigger Stripe/Paddle invoices with idempotent keys and circuit breakers.
- Monitor latency, leakage, and storage; alert on balance depletion.
Before you start: Node 20+, Go 1.24 (optional), Redis 8.x, PostgreSQL 16+, DynamoDB (on‑demand), Stripe Billing 2026 SDK, Kafka 3.5 or Redpanda, Prometheus 2.50, Grafana 10.2. Familiarity with Claude’s token response field.
How to Build a Usage‑Metered Billing API for a Claude Proxy
Build a usage-metered billing API for a Claude proxy by: 1) intercepting requests & extracting token counts, 2) metering with Redis for speed, 3) logging to a persistent ledger (PostgreSQL/DynamoDB) for audit, 4) triggering billing via Stripe/Paddle. Critical for production: implement idempotency, circuit breakers, and monitor for billing latency and leakage.
Introduction: Why Metered Billing Is Essential for AI Proxies
The Shift from Flat Pricing to Utility Models
A year ago most AI SaaS products sold “unlimited tokens” for a flat monthly fee. Today customers demand pay‑as‑you‑go, and investors ask for predictable unit economics. Metering lets you charge per 1 K tokens, just like cloud providers charge per GB‑hour. It also gives you the data you need to optimize model prompts.
Business Case: Protecting Against Token Abuse & Controlling Costs
Claude can generate 4 K tokens in a single response; a malicious user can script thousands of calls, inflating your bill faster than you can detect it. Without a hard limit you’ll see “cost spikes” on your Stripe dashboard and angry finance folks. Metering + rate‑limiting lets you cap exposure per API key while still offering generous tiers for good users.
Core Architecture of a Metered Billing API
The 4‑Tier System: Rate Limiter, Meter, Ledger, Billing Engine
client → Claude‑Proxy → RateLimiter → Meter → Ledger → BillingEngine → PaymentProvider
- **RateLimiter** – stops abuse before you waste compute.
- **Meter** – increments counters in a low‑latency store (Redis).
- **Ledger** – immutable, queryable event log for audits and reconciliation.
- **BillingEngine** – aggregates daily usage, creates invoices, talks to Stripe/Paddle.
Decoupling Components for Scale and Reliability
Each tier lives in its own process or container. The proxy only cares about the first two tiers; the ledger and billing engine run as background workers behind a message bus (Kafka or Redpanda). If the ledger goes down, the meter can keep operating with an in‑memory fallback, and the billing worker will replay events once the DB is back.
flowchart LR
A[Client] --> B[Claude‑Proxy]
B --> C[RateLimiter]
C --> D[Redis Meter]
D --> E[Kafka Topic]
E --> F[Ledger Worker]
F --> G[PostgreSQL Ledger]
G --> H[Billing Engine]
H --> I[Stripe / Paddle]
Step 1: Integrating Usage Collection into Your Claude Proxy
Intercepting Requests: Middleware Pattern vs. Sidecar Proxy
If your service already runs an Express‑style HTTP server, a middleware is the quickest path. For Kubernetes‑native deployments a sidecar (Envoy filter or tiny Go reverse‑proxy) isolates metering logic and lets you reuse the same binary across languages.
// middleware.js – Node 20, express@4.19
// version: 1.0.0
import express from 'express';
import fetch from 'node-fetch';
export const claudeMeter = async (req, res, next) => {
const start = Date.now();
const upstream = await fetch('https://api.anthropic.com/v1/complete', {
method: 'POST',
headers: req.headers,
body: JSON.stringify(req.body),
});
const data = await upstream.json();
const usage = data.usage?.output_tokens ?? 0; // Claude returns `usage` field
// Attach usage for downstream meters
res.locals.claudeTokens = usage;
res.locals.elapsedMs = Date.now() - start;
res.locals.rawResponse = data;
// Let downstream handler decide what to do
next();
};
The middleware puts `claudeTokens` on `res.locals`. Downstream you can push it into Redis (see Step 2).
Extracting Token Counts from Claude API Responses
Claude’s JSON payload includes:
{
"completion": "...",
"usage": {
"input_tokens": 123,
"output_tokens": 456
}
}
Always prefer `output_tokens` because that’s what you’ll bill for. If the field is missing (older API version), fall back to estimating length from `completion` string – not perfect, but better than zero.
Handling Streaming Responses for Accurate Metering
Claude supports Server‑Sent Events (SSE). In a streaming scenario you must accumulate token counts as chunks arrive.
// stream_meter.go – Go 1.24
// version: 1.0.0
package main
import (
"bufio"
"context"
"encoding/json"
"net/http"
"github.com/go-redis/redis/v9"
)
type usagePayload struct {
Usage struct {
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
func streamHandler(w http.ResponseWriter, r *http.Request, rdb *redis.Client) {
ctx := r.Context()
// Proxy request to Claude
resp, err := http.Post("https://api.anthropic.com/v1/complete", "application/json", r.Body)
if err != nil {
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
totalTokens := 0
for scanner.Scan() {
line := scanner.Bytes()
var payload usagePayload
if json.Unmarshal(line, &payload) == nil {
totalTokens += payload.Usage.OutputTokens
}
// Forward raw line to client
w.Write(line)
w.Write([]byte("\n"))
}
// After stream ends, push to Redis
if err := rdb.IncrBy(ctx, "usage:customer:1234", int64(totalTokens)).Err(); err != nil {
// Log but don't break the stream
}
}
This pattern guarantees you never lose a token count even if the client disconnects early.
Step 2: Building the Metering & Rate Limiting Layer
Choosing a High‑Throughput Store: Redis vs. PostgreSQL vs. DynamoDB
| Store | Latency (95th) | Cost @ 10k RPM | Strong Consistency | Scaling Model |
|---|---|---|---|---|
| Redis 8.x | 0.4 ms | $120/mo | **Eventual** (single‑node) | Horizontal via clustering |
| PostgreSQL 16+ | 2.1 ms | $250/mo (RDS) | Strong (transactional) | Read replicas |
| DynamoDB (on‑demand) | 1.9 ms | $180/mo | Strong (per‑partition) | Auto‑scale |
Redis wins on raw speed but offers only eventual consistency across shards. PostgreSQL gives you ACID guarantees, useful for audit. DynamoDB sits in the middle and works well in serverless stacks.
Implementing Token Bucket & Leaky Bucket Algorithms
A token bucket lets you allocate, say, 500 K tokens per hour per API key. The Lua script below runs atomically inside Redis:
-- token_bucket.lua – Redis 8
-- KEYS[1] = bucket key
-- ARGV[1] = capacity
-- ARGV[2] = refill_rate per second
-- ARGV[3] = requested tokens
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1]) or ARGV[1]
local last = tonumber(bucket[2]) or 0
local now = redis.call('TIME')[1]
local delta = now - last
tokens = math.min(tonumber(ARGV[1]), tokens + delta * tonumber(ARGV[2]))
if tokens < tonumber(ARGV[3]) then
return -1 -- not enough
end
tokens = tokens - tonumber(ARGV[3])
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
return tokens
Node code to call it:
// meter.js – Node 20
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
export async function checkAndConsume(key, tokens) {
const script = await import('fs').then(fs => fs.readFileSync('./token_bucket.lua', 'utf8'));
const result = await client.eval(script, {
keys: [key],
arguments: [500000, 138, tokens], // capacity, refill per sec, request
});
if (result === -1) throw new Error('Rate limit exceeded');
return result;
}
Synchronizing Limits Across Multiple Proxy Instances
Deploy the proxy behind a Service Mesh (Istio or Linkerd). The mesh can expose a **distributed lock** via Consul that guarantees each request hits the same Redis shard for the same API key. In practice, simply sharding on a hash of the API key (`SHA256(key) % N`) gives you deterministic routing.
**Tip:** Keep the bucket script (≈ 1 KB) in Redis’s `SCRIPT LOAD` cache on startup; avoid the round‑trip of sending the script text on every request.
Step 3: Creating the Persistent Ledger (The Critical Data Backbone)
Designing Idempotent Event Sourcing for a Reliable Audit Trail
Every metering increment becomes an immutable event:
{
"event_id": "uuid-v4",
"customer_id": "c_1234",
"timestamp": "2026-08-25T14:03:12Z",
"tokens": 342,
"request_id": "req_5678"
}
Use the `event_id` as a primary key and a **unique constraint** on (`customer_id`, `request_id`) to guarantee idempotency. If a retry re‑submits the same request, the DB will reject the duplicate.
Storing Events: Time‑Series DB vs. Append‑Only SQL Tables
- **TimescaleDB (PostgreSQL extension)** – excellent for range queries, compression, and automatic down‑sampling.
- **Append‑only table** – simple `INSERT` with a `BIGSERIAL` PK; you can partition by month to keep tables manageable.
For most SaaS back‑ends, the partitioned table approach is enough and avoids the extra ops burden of a separate time‑series engine.
Hybrid Approach: In‑Memory Aggregation with Periodic Snapshots
We ran a 20 M‑event/month workload on a Redis‑backed aggregator that flushed every 5 minutes into PostgreSQL. Snapshot latency stayed under 150 ms, and storage cost dropped 30 %. The pattern looks like:
[Meter] → Redis Stream (XADD) → Aggregator Worker (Go) → Batch INSERT (PostgreSQL)