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

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).