How to track LLM costs per customer

Updated August 20, 2026.

"$14,300 this month" answers nothing. Which customer drove it? Which feature? Is your biggest account profitable at its current plan? For AI products — where inference is a real cost of goods sold and usage-based pricing is winning — per-customer cost attribution has become table stakes. Industry surveys put it bluntly: fewer than half of teams can attribute AI spend to a customer at all.

Provider dashboards can't do it. OpenAI's and Anthropic's usage APIs report org-level daily dollars — useful for reconciling an invoice, useless for knowing that acme-corp consumed 45% of yesterday's spend.

The recipe#

Attribution means capturing four things at every LLM call site, at the moment the response arrives:

  1. The provideropenai, anthropic, gemini, … It namespaces the price lookup.
  2. The response-reported model — not the alias you requested. Responses echo dated snapshots (gpt-4o-2024-08-06), and snapshots price differently.
  3. The usage object, as the provider sent it — token counts, including cache and reasoning details.
  4. Your attribution fields — at minimum customer and feature; teams running agents add a run/session id, multi-tenant products add end-user.

Cost is then tokens × a price table, computed server-side, and stored frozen at ingest — never recomputed when prices change later.

The traps (each one is a real, documented bug class)#

  • Inclusive vs exclusive input tokens. Anthropic's input_tokens excludes cached tokens; OpenAI's prompt_tokens includes them. Naive cross-provider math double-bills cached tokens — errors up to 2x have shipped in popular open-source trackers.
  • Streaming loses usage. OpenAI streams only report usage if you set stream_options: {"include_usage": true}, on a final extra chunk. Anthropic sends cumulative totals in the last event — summing deltas overcounts. Capture at end-of-stream, from the provider's own numbers.
  • Unknown models must never be $0. A new model or a custom fine-tune that isn't in your price table yet should land as unpriced, visibly — a silent zero corrupts every average built on top of it.
  • Stale price tables. Providers reprice quietly. Several corrections on our own pricing pages this month came from the community price catalog lagging an official change — a maintained, synced table is part of the job.

Build vs buy#

The capture side is genuinely small — one call after each LLM response. The grind is everything after: maintaining prices for thousands of model variants, normalizing five usage dialects, handling batch/cache/tier modifiers, and keeping a queryable store. That second half is what Marginal sells:

marginal.track({
  provider: "openai",
  model: response.model,        // response-reported, snapshot included
  usage: response.usage,        // as-is; dialects normalized server-side
  fields: { customer: "acme-corp", feature: "support-bot" },
});

One buffered, fire-and-forget call per response. Cost is computed at ingest against a daily-synced catalog (with your custom prices for anything private), unknown models land flagged as unpriced, and the dashboard slices spend by any field you registered — customer, feature, model, whatever your margins depend on.

Start tracking, or read the docs for the full event shape.

Know your cost per customer this week.

One track() call per LLM response. Register your fields, deploy, and the next request starts answering which customer, which feature, which model.