kibble

Kibble Blog

Estimated vs billed: why a coding-agent cost dashboard must never add the two

There are two ways to know what your team's coding agents cost, and they give different answers. One is computed on the laptop from the tokens the agent logged, at list price. The other is what the vendor actually charged, read from its Admin API. The first mistake a dashboard can make is to add them together. The second is to show only one.

Two numbers, two sources

The estimate comes from the transcript files the agent writes on each machine (here is where they live). Sum the four token fields per turn, multiply by the model's list rates, and you have a per-day, per-engineer, per-repo figure with fine grain and no authority. It is available for every agent and every seat, including the ones nobody is billing per token.

The billed figure comes from the vendor. Anthropic's Admin API exposes the organization's usage and cost report by day, workspace and API key, read with an Admin key the organization owns. It is the invoice, broken down, and it has authority and no grain: it knows the key, not the laptop, not the repo, not the person if a key is shared.

They should agree. They will not, and the reasons are worth listing because each one is a thing a lead will otherwise ask about in a meeting.

Six reasons they disagree

1. Seats do not bill per token

An engineer on a Claude Max, Team or Enterprise seat is not charged per token at all. Their transcript still records tokens, so the estimate still produces a number, and that number is "what this would have cost at API rates". Useful for deciding whether the seat pays for itself; nothing to do with the invoice. The billed report for those seats is a flat fee, or nothing.

2. The rate card moves

List prices change, new models arrive with new rates, and a pricing table embedded in a tool is exactly as current as its last release. The vendor prices each request at the rate in force when it ran. An estimate made at today's rates for last quarter's tokens is wrong by whatever changed in between.

3. Discounts and tiers

Batch pricing, negotiated rates, committed spend, long-context surcharges above a token threshold, a one-hour cache tier that costs more to write than the five-minute one: every one of these is on the invoice and none of them are in the log. The log has a token count and a model name; it does not know what deal your company has.

4. Transcripts get cleaned up

Claude Code deletes session files older than its retention window, 30 days by default. A collector that started in June has no estimate for May, and none will ever appear. The billed report for May is still there. A dashboard that sums the two would show May as cheap.

5. Days end at different times

The transcript timestamp is UTC; the vendor's daily bucket is UTC too, but the invoice's month is a billing period, and a session that spans midnight lands on one day in the log and, request by request, on two in the report. Per day the difference is noise; per week it evens out; on the last day of a month it can look like a discrepancy until someone checks the hour.

6. The same request, counted twice

An agent run on a laptop and an agent run in CI can share an API key. The billed report puts both on the key. The local estimate puts the laptop's tokens on the laptop and, if the collector also runs in CI, the runner's tokens on the runner. That is the whole reason to have the estimate. It is also why the two sets of rows describe the same dollars from two directions, and adding them counts every request twice.

The rule

Label both. Let billed win the day where it exists. Never sum them.

In Kibble that rule is a merge on the day grain. A day's local rows are estimates; a day's vendor rows are billed. When both exist for an organization, model and day, the billed row is the day's total and the local rows become the breakdown underneath it: which engineer, which repo, which machine. When only the estimate exists (a seat, a day before the Admin key was added, an agent the vendor does not report), the estimate stands, marked as one. Every rollup in the product reads that merged relation and none of them touch the raw table, so there is no page that can accidentally show estimate plus billed as one figure.

Two practical consequences fall out:

What to do with the gap

The gap between estimated and billed is not an error to eliminate; it is the second most useful number on the dashboard. Consistently 20% under means the rate card is stale or a tier is not modelled. Suddenly 60% under means a machine stopped pushing, or a runner was added without the collector. Over 100% means two collectors on one machine, which the opaque session ids will de-duplicate if the collector sends them, and will not if it does not.

None of that is visible on a dashboard that shows one number. All of it is visible on one that shows two and refuses to add them.