Quickstart: meter an AI app

Wrap your model call, take the token count from the provider's response, record it in Usage Lab using the provider's response id as the Idempotency-Key, then read the customer's monthly total. No SDK.

Before you start

curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/meters \
  -H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
  -d '{"key":"llm_tokens","name":"LLM tokens","aggregation":"sum"}'

subscription is whatever id you bill on (your customer id, or a Stripe cus_… id if you'll push to Stripe later).

curl (+ jq)

# needs curl + jq. env: OPENAI_API_KEY, USAGE_LAB_KEY
USAGE_LAB=https://usage-lab.marc-cf2.workers.dev
CUSTOMER=cus_123

# 1. Model call; keep the full response
RESP=$(curl -s https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Say hi"}]}')

# 2. Read the provider's id and token count from the response
RESP_ID=$(echo "$RESP" | jq -r '.id')
TOKENS=$(echo "$RESP" | jq -r '.usage.total_tokens')

# 3. Record it; the response id is the Idempotency-Key, so re-running this line never double-counts
curl -s -X POST $USAGE_LAB/api/v1/usage \
  -H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $RESP_ID" \
  -d "{\"meter\":\"llm_tokens\",\"subscription\":\"$CUSTOMER\",\"value\":$TOKENS}"; echo

# 4. This month's total for the customer
curl -s "$USAGE_LAB/api/v1/meters/llm_tokens/rollups?month=$(date -u +%Y-%m)&subscription=$CUSTOMER" \
  -H "Authorization: Bearer $USAGE_LAB_KEY" | jq '.total_quantity'

Node 18+ (fetch, no dependencies)

// meter-ai.mjs — Node 18+. env: OPENAI_API_KEY, USAGE_LAB_KEY
const USAGE_LAB = "https://usage-lab.marc-cf2.workers.dev";
const LAB_HEADERS = {
  "Authorization": `Bearer ${process.env.USAGE_LAB_KEY}`,
  "Content-Type": "application/json",
};

// 1. Your normal model call (OpenAI Chat Completions shown)
async function callModel(messages) {
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: { "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "gpt-4o-mini", messages }),
  });
  if (!res.ok) throw new Error(`model call failed: ${res.status}`);
  return res.json(); // { id: "chatcmpl-…", usage: { prompt_tokens, completion_tokens, total_tokens }, … }
}

// 2. Record usage AFTER a successful call; the provider response id is the Idempotency-Key
async function recordTokens(customerId, completion) {
  const res = await fetch(`${USAGE_LAB}/api/v1/usage`, {
    method: "POST",
    headers: { ...LAB_HEADERS, "Idempotency-Key": completion.id },
    body: JSON.stringify({
      meter: "llm_tokens",
      subscription: customerId,
      value: completion.usage.total_tokens,
      properties: { model: completion.model },
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
  return body; // { event: { status: "created" | "duplicate", id, idempotency_key, meter, month }, duplicate }
}

// 3. Read this month's total for the customer
async function monthlyTokens(customerId, month = new Date().toISOString().slice(0, 7)) {
  const url = `${USAGE_LAB}/api/v1/meters/llm_tokens/rollups?month=${month}&subscription=${encodeURIComponent(customerId)}`;
  const res = await fetch(url, { headers: LAB_HEADERS });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);
  return body.total_quantity;
}

export async function chatAndMeter(customerId, messages) {
  const completion = await callModel(messages);
  await recordTokens(customerId, completion);
  return completion;
}

const customer = "cus_123";
const completion = await chatAndMeter(customer, [{ role: "user", content: "Say hi" }]);
console.log("tokens this call:", completion.usage.total_tokens);
console.log("retry:", (await recordTokens(customer, completion)).duplicate); // true: not counted twice
console.log("month total:", await monthlyTokens(customer));

We ran both against the live API with a stubbed model response: the first POST returns 201 with "status": "created", the replay returns 200 with "duplicate": true, and the monthly total counts the call once. See the live demo for real captured responses.

Using Anthropic instead

Call POST https://api.anthropic.com/v1/messages. Use message.id (msg_…) as the Idempotency-Key and message.usage.input_tokens + message.usage.output_tokens as value. Or create two meters (input_tokens, output_tokens) and post a batch, giving each event its own idempotency_key such as msg_…:in and msg_…:out.

Rules that keep the bill right