Know what your AI agents cost at API rates. Analyze Claude Code usage locally and see where your tokens and dollars are going.
$ npx contextbill
$14,482.81 at API rates across 72,891 turns in 571 sessions
$11,250.61 projected per 30 days
124,518 median tokens loaded before you type — $2,834.25 of that total
9 long session(s) wrote no file — see the report
15 run(s) look like they died on startup
unpriced models excluded: <synthetic>, claude-fable-5-1
96,785 streamed rewrite(s) collapsed — usage is counted once per message
1,965 message(s) copied forward by a resumed session merged across 39 transcript(s) —
$1,250.33 that would otherwise be counted twice
3 message id(s) span 17 transcripts with no shared line id —
nothing proves they are the same request, so they are NOT merged (about $1.25 counted twice)
Figures are API-equivalent — what this usage would cost metered at the
published Anthropic API rates. A subscription bills a flat fee instead.
report -> ./contextbill-report.htmlThose are real figures from one developer machine, not a projection: 2,424 transcripts and 1,527,997,880 bytes, measured 2026-09-09 against a frozen copy of the corpus. The third line is the one that tends to surprise people: 19.6% of that total was context nobody typed — the system prompt, tool catalog, connector definitions and instruction files that load before every session and get re-read on every subsequent turn.
Two of those lines are corrections rather than findings, and they are printed because a total that has been corrected should say so. The streamed-rewrite collapse counts each assistant message once instead of once per line it was written on. The resumed-session merge counts each request once across the whole corpus rather than once per file it was copied into. What could not be proved a copy stays counted and is named, which is why the last line quotes a residue rather than hiding it.
What the dollars mean. contextbill counts the tokens in your transcripts and prices them at Anthropic's published first-party API rates. If you are on a subscription you paid a flat monthly fee, so these figures are not your invoice: they value the usage, which is what makes one session comparable to another and a habit comparable to the habit you replaced it with. Nothing here needs an API key to be true, and nothing here is a bill.
Every figure is documented. METHODOLOGY.md is the full account of how a directory of transcripts becomes a dollar total: the deduplication rule and the measured inflation factors it corrects, how a turn is counted, how rates are looked up, what the cache-TTL default assumes, how the startup-prefix model is built and why it is capped, and a blunt list of what contextbill cannot know. It is written so you can check the numbers rather than take them.
No signup. No account. No API key. No network calls.
Per-seat billing gives you one blended number. It cannot tell you which sessions were expensive, which produced nothing, which scheduled runs died before doing any work, or how much of the bill is fixed overhead you could delete rather than optimize.
That information is already on your disk, in the transcripts. contextbill reads it.
npx contextbillOr from source:
git clone https://github.com/MohammedAlkindi/contextbill.git
cd contextbill && npm install && npm run build
node dist/cli.js| Flag | Default | Meaning |
|---|---|---|
--root <dir> |
~/.claude/projects |
Claude Code transcript root (see below) |
--source <claude|codex|all> |
claude |
Which agents to read (see below) |
--codex-root <dir> |
~/.codex/sessions |
Codex rollout root. Read only when --source includes codex |
--out <file> |
./contextbill-report.html |
Report path |
--cache-ttl <5m|1h> |
5m |
Cache-write billing rate |
--plan <id> |
— | Value usage against a subscription fee, over whole months only |
--project <slug> |
— | Only projects whose slug contains this text |
--top <n> |
20 |
Rows in the most-expensive-sessions table |
--show-paths |
off | Show full project directory slugs |
--json |
— | Print JSON to stdout, write no file |
--statusline |
— | Print one short line, write no file (see below) |
--scope <session|today> |
session |
What --statusline measures |
--limits |
— | Report subscription rate-limit windows (see below) |
Running contextbill from inside a repo does not scope it to that repo. --root
always defaults to ~/.claude/projects, so the corpus is the same wherever you run
from. The only thing that follows your working directory is where the HTML report
lands. That is deliberate — spend is worth seeing whole — but it is easy to mistake
for a bug when two directories report the same number.
To measure one project, point --root at its directory under ~/.claude/projects:
npx contextbill --root ~/.claude/projects/C--Users-you-work-acmecontextbill notices that shape and says so, because the figure it produces then covers one project rather than everything. Note that transcript folders are named after the literal working directory a session started in, so work started from a parent folder is filed under that parent, not under the repo you were editing.
--source all also reads OpenAI Codex CLI rollouts from ~/.codex/sessions
(--codex-root to point elsewhere). It is a separate flag from --root because the
two agents keep their transcripts in different places and in different layouts.
Codex turns are priced in dollars. prices.json gained OpenAI rates on
2026-09-07, so a Codex turn is priced from a published OpenAI rate exactly as a Claude
Code turn is priced from a published Anthropic one. datedByProvider carries a
separate reading date per vendor, and every OpenAI entry repeats its own source URL
and reading date; a test fails if one does not.
Only the models Codex rollouts actually name are priced, which today means
gpt-5.6-sol and gpt-5.6-terra. gpt-5.5 is deliberately left out: OpenAI charges
a cache-write premium on GPT-5.6 and later and none before it, and the cache-write
multiplier in prices.json is one global value with no per-model override, so pricing
gpt-5.5 would assert a premium nobody charges and nothing would fail. Left out, its
tokens are counted, named in unpriced models, and kept out of every dollar figure.
With --source given the CLI prints a per-source table, so you can always see which
part of the corpus a dollar figure covers. Adding Codex never changes what your Claude
Code usage costs.
--statusline prints one line and writes nothing:
$0.94 API-equiv · 16 turns · 2.7M tok · session
When the payload carries a rate-limit window, the line carries it too:
$0.94 API-equiv · 16 turns · 2.7M tok · session · 7d 63% used · ~2d1h to limit (projected)
Add it to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "npx -y contextbill --statusline"
}
}npx re-resolves the package on every prompt, so install it once and point at the
binary directly if you want the fast path:
{
"statusLine": {
"type": "command",
"command": "contextbill --statusline"
}
}Claude Code writes a JSON payload to the command's stdin; contextbill reads
transcript_path from it and prices that one transcript. Run by hand with no payload,
it falls back to the most recently modified transcript under --root.
It is scoped because it has to be fast. Claude Code runs the command on every
prompt, and a full corpus scan is not a per-prompt operation. Session scope reads one
transcript, --scope today reads the files modified since local midnight, and an
unscoped run reads everything — so the three differ by roughly the amount of data each
one opens, which is the only part of this that is a property of the tool rather than
of a machine.
There is no timing table here, and that is deliberate. One stood here until 2026-09-09 with five rows and a stated method, and it has now failed to reproduce twice on the machine it was measured on. Re-measured 2026-09-09 against a frozen 2,424-transcript, 1,527,997,880-byte corpus, median of five to seven runs:
| Row | Published | Re-measured | |
|---|---|---|---|
node -e "" floor |
0.17 s | 0.09 s | 1.9x faster |
| session scope, median transcript on stdin | 0.28 s | 0.15 s | 1.9x faster |
| session scope, largest transcript on stdin | 0.67 s | 0.46 s | 1.5x faster |
| session scope, no stdin | 0.41 s | 0.39 s | 1.05x, over 29% more files |
| full report | 20.1 s | 14.8 s | 1.36x faster, on a 17% larger corpus |
--scope today |
2.19 s | not measurable | a frozen copy has no files modified today |
The floor is the row that settles it. node -e "" runs no contextbill code at all,
so it cannot have got faster because the tool changed. A machine-level constant moving by
1.9x means the published table describes a machine state rather than a program, and every
row above it inherits that. For the record, the re-measurement was itself taken on a busy
machine — 26 node processes, 17 Claude Code processes, 48% CPU — so it is an upper bound
and not the quiet-machine number either.
A benchmark nobody can reproduce is worse than no benchmark, so it is gone rather than restated with today's equally circumstantial figures.
Measure it on your own hardware instead, which is the only number that answers the question you actually have:
# one transcript, the shape a statusline actually runs
echo '{"transcript_path":"/path/to/session.jsonl"}' | \
node -e 'const t=Date.now();require("child_process").execFileSync(process.execPath,["dist/cli.js","--statusline"],{input:require("fs").readFileSync(0)});console.error(Date.now()-t+"ms")'Run it several times and take the median, compare it against node -e "" on the same
machine in the same minute, and read the difference rather than the absolute — Node's
own startup is the floor no CLI written in it beats, and it moves with load just as
everything above it does.
--scope today is labelled active today in the output rather than today,
because it filters on file modification time: a session that started yesterday and
continued this morning is counted in full. It is a bound on today's spend, not a
measurement of it.
Two behaviours differ from every other mode, both on purpose:
- It exits non-zero and prints nothing when it cannot answer. Claude Code hides a
statusline whose command fails, so a bad
--rootcosts you the line rather than putting an error message in your prompt. API-equivis not droppable. These are the dollars this usage would cost metered at published API rates. If you are on a subscription you paid a flat fee instead, and a bare dollar figure sitting in a prompt reads as a bill.
--limits reports how much of each subscription rate-limit window is gone, when it
resets, and the pace that implies:
Rate-limit windows, read from the statusline payload
measured 42% of the 5-hour session limit used — resets in 1h58m (2026-09-09 22:43 UTC)
projected 13.9 points/hour since that window opened; at that pace the window resets before it fills
measured 63% of the 7-day limit used — resets in 3d10h (2026-09-13 07:44 UTC)
projected 0.7 points/hour since that window opened
100% around 2026-09-11 22:39 UTC — 2d1h from now, before the reset
measured $0.00 API-equivalent across 0 turn(s) in 0 transcript(s) touched
since that window opened — $0.00/hour
That block is a verbatim capture, not an illustration: it is what
contextbill --limits --root fixtures/projects printed on 2026-09-09 with a sample
payload on stdin. The local-spend row reads $0.00 because the bundled fixture corpus
has no file modified inside the window — against a real corpus it carries the
API-equivalent figure for the transcripts touched since the window opened.
These are the only numbers in contextbill that are not derived from a transcript,
and that is the point. Claude Code reads the anthropic-ratelimit-unified-* response
headers and writes the resulting percentages to a statusline command's stdin. So a
window that is present is the vendor's own count of what is gone, not a reconstruction
of one. Everything labelled projected is a straight line through that count — the
pace so far, extended — and usage is bursty, so read a projection as a pace and never
as a promise.
Wire it up the same way as --statusline:
{
"statusLine": { "type": "command", "command": "contextbill --limits" }
}What it cannot see, stated plainly. It never infers a window from your transcripts. A locally inferred window cannot see usage from another machine, from the web app, or from a session whose transcript was deleted, and it cannot see how the vendor weights a request — which is exactly why locally inferred windows disagree with the limits people actually hit. So:
- No payload, no answer. Run by hand with nothing on stdin and it says it cannot see a window rather than guessing one.
- There is no window at all on an API-key, Bedrock or Vertex session, and none for a subscriber until the first API response of the session.
- The dollar figures beside a window are not what the percentage measures. A plan window is weighted by rules this tool cannot see. They sit next to the official numbers, never as a check on them.
--scope todaycarries no window, because only session scope reads stdin.
Writes no file. Combines with --json.
If you are building a statusline, a dashboard or a CI check, import the analysis instead of shelling out and parsing text. Text output is a presentation choice and it changes; these functions and their types are the stable surface.
npm install contextbillimport { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { aggregate, cacheWriteMultiplier, parseTranscript, priceAll, usd } from 'contextbill';
import type { PriceTable, SessionStat } from 'contextbill';
// The price table ships with the package. `createRequire` rather than an import
// attribute, because `import ... with { type: 'json' }` needs node 20.10+ and
// contextbill supports 18.
const table = createRequire(import.meta.url)('contextbill/prices.json') as PriceTable;
const text = readFileSync('/path/to/session.jsonl', 'utf8');
const stat: SessionStat = parseTranscript(text, {
id: 'session-a',
project: 'my-project',
isSubagent: false,
bytes: text.length,
});
console.log(stat.turns, usd(priceAll(stat.byModel, table, '5m').cost.total));
// Many transcripts at once: the whole report the HTML is rendered from.
const report = aggregate([stat], {
table,
ttl: '5m',
cacheWriteMult: cacheWriteMultiplier(table, '5m'),
topN: 20,
});| Export | What it does |
|---|---|
parseTranscript(text, opts) |
Claude Code transcript text → one SessionStat |
parseCodexRollout(text, opts) |
The same for an OpenAI Codex CLI rollout |
priceAll(entries, table, ttl) |
Per-model usage → dollars, plus what it could not price |
priceModelUsage(entry, table, ttl) |
One model's usage → dollars, or null if unpriced |
cacheWriteMultiplier(table, ttl) |
The 5m/1h cache-write rate multiplier |
aggregate(stats, opts) |
Many SessionStats → the full Report |
monthlyProjection(report) |
The 30-day projection |
planUtilization(report, plans, id) |
Value against a subscription's monthly fee |
classify(toolName) |
Tool name → spend category |
redactProject(slug, home) |
Strips the OS username out of a project slug |
renderReport(report) |
Report → the self-contained HTML string |
usd(n) |
The dollar formatting the CLI uses |
Every interface these accept or return — SessionStat, Report, PriceTable,
CostBreakdown, RawUsage and the rest — is exported as a type from the same entry.
Nothing reachable from the library entry point touches the filesystem, so it
bundles for a browser. Reading transcripts off disk is your half of the job, which
keeps the parser identical on both surfaces — this repo's own web app parses in the
browser through the same parseTranscript. scan.ts, which owns node:fs, is
deliberately not exported; a test walks the import graph and fails if a node: import
ever appears in it.
Two things worth knowing before you trust a number:
priceAllreturnsunpriced. A model with no entry in the price table contributes zero dollars and real tokens. Surface that or your totals quietly under-report — contextbill's own output names the model ids rather than folding them in.- Deduplication happens in two places, and only one of them is
parseTranscript. A streamed assistant message is written to the transcript several times, each restating that message's cumulative usage, soparseTranscriptkeeps the maximum per field permessage.id. Summing raw lines inflated the priced total by 2.39x on the corpus this was measured against. The second correction is inaggregate: a resumed session copies its parent's history into a new file, so the same request sits in two transcripts, andaggregatemerges those copies across the whole corpus. CallingparseTranscripton many files and adding the results yourself gets the first correction and not the second — on this corpus that is a further $1,250.33, or 7.9% of the pre-merge total.
One self-contained HTML file with no external references:
- Cost by category — shell, file reads, browser automation, connectors, model output, and fixed startup overhead, in dollars.
- The startup prefix — your median pre-input token load and what re-reading it across every turn of every session is worth at API rates.
- Sessions that wrote no file — long runs that never produced an artifact.
Offered as a prompt to go look, not as a verdict; read-only research is real work,
and a
Bashcommand that writes a file does not count toward the flag. - Runs that died on startup — a scheduled agent that exits before working is indistinguishable from one that never fired. Transcript size separates them.
- Cost by project — what each directory you work from actually costs. The rows
sum to the total exactly, so a project's share is a real share and not a
re-estimate. Use
--project <slug>to report on one of them alone. - Per-model spend — including fast-mode turns at their own rate.
- Token classes — input, cache writes, cache reads, output.
Other tools read the same transcripts — ccusage and Claude Code Usage Monitor are the ones
I am aware of, and Claude Code ships a built-in /cost. I have not audited any of them, so
this section makes no claim about what they do or how they do it. A comparison table written
by one project about its alternatives is marketing, not evidence.
What contextbill commits to instead:
- Accuracy you can check. METHODOLOGY.md states every rule with the file
that implements it. The suite pins an exact end-to-end dollar figure worked out by hand from
prices.json, so a pricing regression fails loudly.scan.tsandclassify.tscan be differential-checked against the reference script they were ported from. The streamed-rewrite correction is stated with its measured factors, not asserted: across 2,424 transcripts and 459,783 lines, 171,974 usage-bearing lines collapse to 75,196 messages, and the priced total moves from $37,655.25 to $15,733.14 on the same text in one pass. - Per-MCP-connector attribution. Tool names arrive as
mcp__<server>__<tool>, so spend is attributable per connector, priced off the same content pool and the same denominator as the category rows. Servers that were called and returned nothing still appear with their call count — a chatty connector and a silent one are different findings. The limits of that attribution are documented too: it is an apportionment by result bytes, and a connector that loaded but was never called cannot be seen. - Plan utilization.
--plan <id>values usage against a subscription fee, over complete months only, and returns nothing rather than a multiple when no month is complete. A metered plan gets no multiple at all, because there is no flat fee to divide by.
Questions worth putting to any usage tool, this one included. The answers are what separate them:
- Does it collapse streamed message rewrites, and by which rule — sum, last line, or maximum per field?
- What does it do when a resumed session copies its parent's history into a new file?
- Does it price last quarter's usage at last quarter's rates or at today's?
- Where do its rates come from, and do they carry the date they were read?
- What does it assume about cache-write TTL, which no transcript records, and which direction does that assumption err in?
- Does it call the result an invoice?
Transcript files, opened read-only. Nothing is written back into any directory it reads.
| Read | When | Default |
|---|---|---|
*.jsonl under --root |
always, unless --source codex |
~/.claude/projects |
Codex CLI rollouts under --codex-root |
only when --source is codex or all |
~/.codex/sessions |
The Codex tree is opt-in and is read only when you ask for it. A default run
(--source claude) never opens ~/.codex/sessions or any path outside --root.
From each transcript it reads token counts, model ids, timestamps, tool names, and the byte size of tool results. It does not read conversation content into the report.
No. The shipped build imports four Node built-ins — fs, os, path, url — and
nothing else. There is no HTTP client, no socket, no telemetry, and there are zero
runtime dependencies. A test asserts the generated report contains no external
reference; the report renders offline with system fonts.
Two related protections:
- Project paths are redacted by default. Transcript folders are named after the
literal working directory (
C:\Users\jdoe\work\acmebecomesC--Users-jdoe-work-acme), which encodes your OS account name and directory tree. Since the report is built to be shared, that is stripped towork-acme.--show-pathsis an explicit opt-in. - Generated reports are gitignored, so one cannot be committed by accident.
The short version. METHODOLOGY.md is the long one, including the deduplication rule, dated rate periods, the category and connector apportionment, and what none of it can know.
Every dollar figure derives from prices.json, which carries the date it was
compiled and is cited in every report footer. It holds first-party rates from two
vendors — Anthropic's for Claude Code usage and OpenAI's for Codex — each with its
own reading date under datedByProvider, because the two were read on different days
and a single dated field would have to lie about one of them.
First-party only, in both cases. Amazon Bedrock, Google Vertex AI and Azure OpenAI are partner-operated and priced separately; contextbill does not model them, and a transcript produced through any of them is priced as if it had gone to the first-party API.
Three details that are easy to get wrong, and how contextbill handles them:
- Dated model ids. Transcripts record the id actually sent, including snapshots
like
claude-haiku-4-5-20251001. A direct table lookup misses those and prices a real model at zero. The suffix is stripped before lookup. - Fast mode.
usage.speed === "fast"bills at roughly double. Ignoring it understates every fast session. - Cache-write TTL is not recorded in transcripts. Writes bill at 1.25× at the
5-minute TTL and 2× at one hour, and the transcript carries a single number with no
TTL field. contextbill cannot infer it and does not guess: it defaults to the cheaper
rate — so it understates rather than overstates — names the assumption in the report
footer, and offers
--cache-ttl=1hto see the other figure.
Models with no entry in the price table are counted and reported separately rather than folded into the total as free.
The startup-prefix figure is a model, not a billed line item: prefix tokens priced across observed turns, capped at non-output spend. It is labelled as such in the report.
Zero runtime dependencies, Node built-ins only. Every module is pure except
cli.ts, so the logic is testable without touching a disk.
| File | Purpose |
|---|---|
src/types.ts |
Shared interfaces, defined before consumers |
src/scan.ts |
Walks transcripts, extracts per-session token and tool stats |
src/classify.ts |
Tool name → spend category |
src/cost.ts |
Token counts + price table → dollars |
src/aggregate.ts |
Session stats → whole-corpus report |
src/waste.ts |
Waste and fleet-health heuristics |
src/privacy.ts |
Strips machine-identifying path data |
src/report.ts |
Report → self-contained HTML |
src/index.ts |
The published library entry. Re-exports the pure modules only |
src/cli.ts |
Arg parsing and file I/O. The only impure module |
npm test # vitest
npm run lint # eslint
npm run build # tscTests run against hand-built fixture transcripts in fixtures/ — never real ones,
which contain private data. The suite pins an exact end-to-end dollar total that is
worked by hand in a comment, so a pricing regression fails loudly rather than drifting.
Differential check. scan.ts and classify.ts are ports of a reference
measurement script, so they can be validated against it rather than only against
themselves. Point both at the same frozen corpus and compare turns and the
median startup prefix — they must agree exactly. Freeze it first: running against a
live transcript directory produces off-by-one differences whenever another agent
session appends a turn mid-comparison, which looks exactly like a port bug and is not
one.
None. contextbill needs no API key, no account and no configuration, because it never calls an API. If a future version ever requires a credential, that will be a breaking change announced in the changelog.
Issues and pull requests welcome. CONTRIBUTING.md has the full guide — setup, the constraints that will fail review, and how to update prices. The short version, before opening one:
npm test,npm run lintandnpm run buildall pass.- New behaviour comes with a test. Do not weaken an existing test to make a suite green.
- Do not add a runtime dependency without a stated reason —
npx contextbillstaying instant and auditable is a product property, not an implementation detail. - Do not add a network call. Zero egress is the product; there is a test asserting it.
- Price changes in
prices.jsonmust come with an updateddatedfield and a source. - A change to how any number is computed is a change to METHODOLOGY.md. Update it in the same pull request.
MIT © 2026 Mohammed Alkindi