Drop-in LLM token metering (and a margin calculator)
One file, no dependencies: usage-lab.js (MIT). Wrap any model call and it records the call's total tokens for a customer in Usage Lab. The provider's response id is the Idempotency-Key, so a retried request, a re-run job or a double-fired webhook never bills twice. Server-side only (Node 18+, Bun, Deno, Cloudflare Workers): your Usage Lab key must stay secret.
Setup: get a key, then create a sum meter called llm_tokens (see the API docs). Metering errors are logged and swallowed by default, so a metering hiccup never breaks your app; pass { strict: true } to throw instead.
OpenAI
// npm i openai (download usage-lab.js next to this file)
import OpenAI from "openai";
import { createMeter } from "./usage-lab.js";
const openai = new OpenAI();
const meter = createMeter({ apiKey: process.env.USAGE_LAB_KEY }); // meter "llm_tokens" (sum)
const completion = await meter.wrap("cus_123", () =>
openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Say hi" }] })
);
// Posted: { meter: "llm_tokens", subscription: "cus_123", value: completion.usage.total_tokens }
// with Idempotency-Key: completion.id ("chatcmpl-...")
Anthropic
import Anthropic from "@anthropic-ai/sdk";
import { createMeter } from "./usage-lab.js";
const anthropic = new Anthropic();
const meter = createMeter({ apiKey: process.env.USAGE_LAB_KEY });
const msg = await meter.wrap("cus_123", () =>
anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 512, messages: [{ role: "user", content: "Say hi" }] })
);
// value = input_tokens + output_tokens (+ cache tokens if present); Idempotency-Key = msg.id ("msg_...")
Vercel AI SDK
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createMeter } from "./usage-lab.js";
const meter = createMeter({ apiKey: process.env.USAGE_LAB_KEY });
const result = await meter.wrap("cus_123", () =>
generateText({ model: openai("gpt-4o-mini"), prompt: "Say hi" })
);
// value = result.usage.totalTokens; Idempotency-Key = result.response.id
// streamText: record once the stream finishes
// streamText({ ..., onFinish: (r) => meter.record("cus_123", r) })
The raw request
No library needed if you'd rather call the API yourself:
# What the helper sends (one request per model call)
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/usage \
-H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: chatcmpl-abc123" \
-d '{"meter":"llm_tokens","subscription":"cus_123","value":123,"properties":{"model":"gpt-4o-mini"}}'
# 201 {"event":{"status":"created",...},"duplicate":false} retry: 200 "duplicate":true changed value: 409 idempotency_conflict
API
createMeter({ apiKey, meter = "llm_tokens", baseUrl, keyPrefix = "", onError })meter.wrap(customer, fn, opts): runsfn, records its usage, returns the model result unchanged.meter.record(customer, result, { strict, idempotencyKey, timestamp }): record a result you already have (e.g. inonFinish). Returns the API body, ornull.meter.monthTotal(customer, "YYYY-MM"): the customer's monthly total from the rollups endpoint.extractUsage(result): readsusage.total_tokens(OpenAI),input_tokens + output_tokens(Anthropic) orusage.totalTokens(Vercel AI SDK), and the response id.
Tested 2026-10-14 against the live API: create, retry (duplicate), changed payload (409), Anthropic and Vercel shapes, and the monthly total all behave as documented. Try the rules without a key in the playground.
LLM cost and usage-pricing calculator
Will per-token pricing cover your model bill? Enter your numbers (any currency). Runs in your browser; nothing is sent.
Model prices change; check your provider's current pricing page. The defaults are only an example.
Once the price works, bill it: meter tokens per customer with the helper above, read monthly totals from /rollups, and push them to your biller. See how to meter LLM token usage per customer and Usage Lab pricing (free tier: 100k events/month).
- Playground: validate a usage event in your browser
- Live end-to-end demo
- API docs
- Quickstart: meter an AI app
- Code examples
- Metering usage for Stripe usage-based billing
- Deduping usage events with idempotency keys
- Backfilling late usage events: replays, late windows and adjustments
- Usage Lab vs UsageBox: an honest comparison
- Metronome and Orb alternatives for small SaaS teams
- How to meter LLM token usage per customer for billing
- Stripe Billing Meters vs a usage ledger: when you need both