Every AI Coding Agent Miscounts Tokens. Here's How Each One Does It.
Four AI coding agents, four different ways to miscount tokens — none of them documented. What I found reading the logs they already write to disk.
I wanted one number: what my AI coding agents cost me last month.
Four agents, four log files already sitting on disk. Add up the token counts, multiply by the rate, done. An afternoon's work.
It took considerably longer than an afternoon — because every one of those log formats miscounts, and each does it differently. None of it is documented. All of it is discoverable if you sit and read real output instead of the docs.
Here is what I found, in the order it cost me time.

1. Claude Code writes the same message up to seven times
Open a Claude Code transcript and you will find the same assistant message repeated — sometimes twice, sometimes seven times. The payloads are byte-identical. Every copy carries the full usage block.
{"requestId":"req_011CSx…","uuid":"3f9a…","message":{"id":"msg_01ABC…",
"usage":{"input_tokens":4,"output_tokens":187,"cache_read_input_tokens":42310}}}
{"requestId":"req_011CSx…","uuid":"7c2e…","message":{"id":"msg_01ABC…",
"usage":{"input_tokens":4,"output_tokens":187,"cache_read_input_tokens":42310}}}
{"requestId":"req_011CSx…","uuid":"b8d1…","message":{"id":"msg_01ABC…",
"usage":{"input_tokens":4,"output_tokens":187,"cache_read_input_tokens":42310}}}message.id, identical usage, different uuid — and 561 output tokens where 187 were spent.Sum them naively and you overcount by roughly 2.5×.
The fix is a dedup key. message.id is unique per API response, and requestId scopes it:
key = (requestId, message.id)Check your own transcripts
# total usage-bearing lines
jq -r 'select(.message.usage) | .message.id' ~/.claude/projects/*/*.jsonl | wc -l
# how many are actually distinct
jq -r 'select(.message.usage) | .message.id' ~/.claude/projects/*/*.jsonl | sort -u | wc -lIf the first number is roughly two and a half times the second, you have just reproduced the bug that every naive token counter ships with.
2. Subagents hide in separate files — and get recorded mid-stream
Claude Code subagents do not write into the parent transcript. They get their own files:
~/.claude/projects/*/*/subagents/agent-*.jsonlMiss that glob and every subagent turn is invisible. Your total is too low, and you will never notice, because nothing errors.
There is a second edge underneath it: a message can be written while it is still streaming. The first copy holds only part of the output. Dedup by ID alone and you might keep the partial one and discard the complete one — an undercount that looks exactly like a correct count.
3. Codex restates its totals, so summing turns double-counts
Codex writes two things: a per-turn figure, and total_token_usage — a running total restated on every event. It also repeats its final usage event.
Sum the per-turn figures and the repeats inflate the result.
turn 1 last_token_usage: 12,400 total_token_usage: 12,400
turn 2 last_token_usage: 8,100 total_token_usage: 20,500
turn 2 last_token_usage: 8,100 total_token_usage: 20,500 ← repeated event
turn 3 last_token_usage: 6,300 total_token_usage: 26,800last_token_usage gives 34,900. Deltas of the running total give 26,800 — which is exactly the final total_token_usage.The fix is nicer than deduplication. Take deltas of the running total instead:
| Approach | What a repeated event does |
|---|---|
| Sum per-turn figures | Adds the turn again — silent overcount |
| Delta of running total | Yields zero — self-correcting |
A repeated event contributes nothing, and the deltas sum to the final total by construction. That last property is free test coverage: after parsing, the deltas must reconcile exactly with the last total_token_usage in the file. If they ever disagree, the parser is wrong and the check says so on every scan.
4. Kimi Code writes each number three times
Kimi Code logs a usage.record entry per turn — that one is billable. It also writes two token_counting records restating the same figure.
Count all three and every number roughly doubles. There is no flag distinguishing them; you have to know which record type means "this happened" and which means "here is that number again".
5. Ollama reports what it read, not what you sent
This one is not a logging quirk. It is the model.
Ollama truncates any prompt larger than its context window and reports the tokens it actually processed. Send roughly 8,000 tokens against the default 4,096-token window and you get back 2,050.
The number is not wrong. It is answering a different question than the one you assumed. No agent can report what its provider never told it — so if you are measuring local inference, raise num_ctx before you trust the figure.
6. A rate of $0 almost never means free
The last trap is the one with real consequences, and it is not in any log file.
When a pricing table lists a model at 0, that usually means included in your plan. On Claude Max or a ChatGPT plan you are not billed per token at all. Rendering "$0.00" implies you were charged nothing. Rendering "$204" implies a bill that does not exist.
Both are lies. The honest version needs three separate states:
| Situation | Shown as |
|---|---|
| Subscription plan | API-equivalent — what this would cost at API rates, never a bill |
| No known rate for the model | —, never $0.00 |
| Running on your own hardware | not metered — a different thing from free |
And if you are on a subscription, the dollar figure is not even the number that matters. The one that stops your work is the rate-limit percentage.
The thing that surprised me most
None of this is in the documentation. Every trap above came from reading real output and noticing a number that did not reconcile with something else.
Which is why the tool I built verifies each adapter against actual output from the agent before it ships, never from docs alone. Codex's per-turn accounting looked entirely reasonable on paper — and it was wrong.
The other lesson: prefer designs where being wrong is loud. The Codex delta approach is not just more accurate, it is checkable — deltas either reconcile with the running total or they do not. A per-turn sum has no such property. It is silently wrong forever.
What I did about it
All of this went into burn-o-meter — one meter for every agent, in both GUI and CLI. It reads the logs your agents already write, handles each format's quirks, and reports tokens, cost, cache hit rate and rate limits in one place. Nothing leaves your machine: no account, no telemetry.
pipx install burn-o-meter
burn-o-meter scan && burn-o-meter todayIt currently reads Claude Code, Codex CLI, OpenCode and Kimi Code — and because OpenCode and Kimi Code route to any provider, that also covers DeepSeek, GLM, Qwen, MiniMax and local models.
There is a fuller walkthrough of the reports, the macOS menu bar app and the install options in the burn-o-meter overview. The code is on GitHub.
It is alpha, and honestly the log formats were the interesting part. If you run an agent I have not covered yet — Copilot CLI especially — a few rows of its usage output would help me write that adapter.
FAQ
Why does summing Claude Code's logs overcount tokens?
Claude Code writes the same assistant message up to seven times, each copy carrying the full usage block. The payloads are byte-identical, so a naive sum counts the same tokens repeatedly — roughly 2.5× too high. Deduplicate on (requestId, message.id).
How do I track Claude Code token usage accurately?
Deduplicate by message ID, include the subagent transcripts under subagents/agent-*.jsonl, and prefer complete messages over copies captured mid-stream. Reconcile your total against a figure the agent reports itself, so an error is visible rather than silent.
Why is my Codex token count too high?
Codex repeats its final usage event and restates each turn. Summing per-turn figures counts repeats. Use deltas of total_token_usage instead — a repeated event then produces a delta of zero, and the deltas reconcile exactly with the final total.
Does a $0 rate mean the model is free?
Usually not. A published rate of zero normally means the usage is included in your subscription, not that it costs nothing. Subscription usage is best shown as API-equivalent value, and a model with no known rate should show as unpriced rather than $0.00.
Why does Ollama report fewer tokens than I sent?
Ollama truncates prompts larger than its context window and reports the tokens it actually processed. Around 8,000 tokens against the default 4,096-token window is reported as 2,050. Raise num_ctx if you need the full prompt measured.