Measures how the counting rules for coding-agent token usage differ on a real corpus, so a tool that reports totals can be checked against data instead of against its own source code.
Tools that read ~/.claude/projects disagree about three things, none of which
is documented: how to combine several records that share one dedup key, whether
usage.iterations is already aggregated by its parent block, and which models
exist. Codex sessions add a fourth: a running total sitting next to a per-turn
delta under near-identical names. Each disagreement moves the reported number.
ccledger applies every rule to the same corpus and prints the distance between
them.
No dependencies outside the standard library. It reads and prints; it writes nothing.
python ccledger.py # whichever corpora exist on this machine
python ccledger.py /some/other/path # a different Claude Code corpus
python ccledger.py --codex /some/path # a different Codex corpus
With no arguments it reports on ~/.claude/projects and ~/.codex/sessions,
skipping whichever is absent.
Python 3.9 or later. On Windows, py -3 ccledger.py.
Abridged, from a 201-file corpus:
corpus: C:\Users\you\.claude\projects
JSONL files: 201 | lines with usage: 9,425 | unreadable lines: 0
groups (requestId, message.id): 3,623
==============================================================================
check 1 usage.iterations: does the parent block aggregate the iterations?
==============================================================================
records with iterations : 8,922
records with more than one: 23
of those, mixed models : 23
for records with more than one iteration, which rules explain the parent.
parent == last 23 100.00%
explained by exactly one rule: 23 (100.00%)
tokens in iterations that are not the final one, by model.
model input output cache_creation cache_read
claude-fable-5 90,186 8,297 152,481 1,436,662
==============================================================================
check 2 deduplication: how far apart the counting rules land
==============================================================================
class groups multi-line differing multi-file non-monot.
main 3,237 2,598 0 207 0
subagents 386 279 229 0 0
ratio against max_record, which is 1 by construction:
rule input output cache_creation cache_read
no_dedup 2.0952x 2.8655x 2.7227x 2.6307x
sum_distinct 1.4074x 1.0003x 1.0725x 1.0150x
max_record 1.0000x 1.0000x 1.0000x 1.0000x
max_field 1.0000x 1.0000x 1.0000x 1.0000x
first_line 1.0000x 0.9511x 1.0000x 1.0000x
last_line 1.0000x 1.0000x 1.0000x 1.0000x
==============================================================================
check 3 models present in the corpus
==============================================================================
model records input output
claude-fable-5 5,616 4,536,794 7,264,717
claude-opus-5 2,250 13,121 3,889,457
[...]
==============================================================================
check 4 codex: is total_token_usage a running total?
==============================================================================
sessions carrying tokens : 182
never decreasing : 182 (100.0%)
reading total tokens
sum of total_token_usage, line by line 30,074,985,351
final total_token_usage, once per session 509,045,554
sum of last_token_usage (per-turn deltas) 511,449,773
summing the running total inflates by 59.08x
the deltas rebuild the final total to within 0.47%
arithmetic of the nested fields, over 4,903 per-turn records:
input + output == total 4,846 ( 98.84%)
cached_input <= input 4,903 (100.00%)
reasoning_output <= output 4,903 (100.00%)
What this particular corpus says, as an example of how to read it:
- The parent block equals the last iteration and, since every one of the 23 records is explained by exactly one rule, it is not the maximum. The tokens of the attempt that did not serve belong to a different model and are attributed to none.
- Only the output column moves between the sensible dedup rules. Input and cache counts are known when a message starts; the output is not, so keeping the first line keeps a snapshot taken before the message finished, and loses 4.9%.
max_fieldandlast_lineboth land exactly onmax_record, so on this corpus those three rules cannot be told apart by their totals.non-monot.is zero, which is why.- Every differing group is under
subagents/: none of the 3,237 top-level groups has rows that disagree. A corpus without subagent traffic would show no difference at all between the rules.
The output totals in check 1 and check 2 differ because they count different things: check 1 sums every record carrying iterations, check 2 sums one entry per dedup key.
1. usage.iterations. Some records carry an array of complete usage objects
alongside the parent block. A tool that summed them on top of the parent would
double count; one that ignored them would lose tokens if the parent does not
aggregate them. The check reports which rule explains the parent, and counts how
many records are explained by exactly one rule: when several rules match, the
corpus is not telling them apart rather than confirming all of them.
It also reports the tokens carried by iterations that are not the final one,
grouped by their own model field. The parent block carries a single
message.model, so those tokens are attributed to no model by any tool that
reads that field alone.
2. Deduplication. Claude Code writes a streaming message as several lines
sharing one (requestId, message.id) key, and the earlier lines carry a partial
usage snapshot. The check applies six rules to every group and prints the totals
side by side, with the ratio against max_record:
| rule | what it does |
|---|---|
no_dedup |
sums every line |
sum_distinct |
sums the distinct usage fingerprints |
max_record |
keeps the record with the highest total |
max_field |
takes the maximum of each field separately |
first_line |
keeps the first line seen |
last_line |
keeps the last line seen |
Results are split between top-level transcripts and subagent transcripts,
because the rules can agree on one and disagree on the other. The non-monot.
column counts groups where a later record in the same file carries a lower total
than an earlier one: when it is zero, last_line and max_record coincide on
that corpus, and a tool cannot be distinguished by its totals alone.
3. Models. Lists the models the corpus actually contains, to compare against a tool's price table. A model missing from that table is usually priced at zero without any error being raised.
4. Codex running totals. Codex session files put two near-identical objects
side by side under payload.info: total_token_usage is a running total for the
whole session, last_token_usage is the delta for that turn. The names do not
say so. The check measures whether either one ever decreases across a session,
which is what tells a running total apart from a per-turn measure, and reports
what summing the wrong one costs. On the corpus this was written against, that
is a 59x overcount.
It also checks the arithmetic of the nested fields, since cached_input_tokens
and reasoning_output_tokens are subsets of their parents rather than additions,
and reports records whose total is not explained by input plus output, which a
tool adding up only the detail fields would lose.
A corpus grows while it is being measured, because the session running the
measurement writes to its own transcript. ccledger fixes each file's size
before reading it, so two consecutive runs over the same state produce identical
output, and reports how many files changed underneath. Numbers meant to be
quoted should come from a run that reports no growth.
python -m unittest discover
The rules agree on almost all real data, so the tests build corpora on purpose
to make them disagree: a group whose last record drops below its own maximum, a
group whose per-field maxima sit on different records, iterations that grow on
every field and therefore cannot distinguish last from max. A test passing
on data where two rules coincide would not show that the tool tells them apart.
It does not price anything and it does not tell you which rule is correct. Which records are actually billed is not visible in a transcript: it is visible on an invoice. The tool reports how far apart the rules land, and the choice between them stays an argument about the protocol, not about this corpus.
MIT.