Session Cost Report Script
Price your Claude Code transcripts at real API rates, and the six rules that decide whether the number is right
What it does
Claude Code writes a JSONL transcript for every session under ~/.claude/projects/, and every assistant turn in it carries a usage object: input, output, thinking, cache writes split by TTL, and cache reads. That is enough to price the work at published API rates without an account, an API key, or a network call.
Releve is that, as three Python scripts and a dashboard. The scripts are standard library only.
Download
# one number curl -O https://releve.neorgon.com/scripts/releve-mini.py python3 releve-mini.py --days 30 # the full scan, which writes the file the dashboard loads curl -O https://releve.neorgon.com/scripts/releve-scan.py curl -O https://releve.neorgon.com/scripts/releve_cost.py python3 releve-scan.py --days 30 --out releve.json
Requires Python 3.8+. No external dependencies. Each script prints the date of the rate card it used.
Basic usage
python3 releve-mini.py --days 7 # last week python3 releve-mini.py --days 1 # today only
Releve · 7 days · 22 sessions · 13,621 turns API-equivalent $2,659.18 excluded 113 turns, never billed rates as of 2026-08-27. A counterfactual at list rates, not an invoice.
The six rules
Reading a transcript naively is wrong in six specific ways. Each of these is a real defect the first attempt shipped.
| Rule | |
|---|---|
| 1 | Resolve the rate per turn, from that turn's own message.model. A session that switched models is not priced by whichever model appears last in the file. |
| 2 | Never substitute another model's rates. Exact id, then longest declared prefix, then *unpriced*. An unpriced model is named and counted in its own bucket, never folded into a total and never rendered as zero. |
| 3 | Split the cache writes. A 5-minute write bills at 1.25x base input, a 1-hour write at 2x. A turn carrying only the flat counter is an assumption, and should be flagged as one. |
| 4 | Honour usage.speed. Fast mode bills double, so a fast session must not be priced as standard. |
| 5 | A non-empty usage.iterations replaces the top-level counters rather than adding to them. Adding double-counts; ignoring it under-reports every multi-iteration turn. |
| 6 | Count each response once, keyed on message.id. A response is written one entry per content block and every entry repeats the same usage, so a turn that thought, spoke and called a tool appears three times and is billed once. |
Rule 6 decides whether the answer is right or roughly triple. On one real machine, 116,402 transcript entries are 52,078 responses, and counting entries reports 30.0B cache-read tokens against a true 12.9B.
Rule 3 is the one people skip, and the cache is usually most of the bill: cache reads at 0.1x base input are cheap per token and enormous in aggregate.
The whole tree, not one directory deep
Transcripts nest three ways, and a single-level glob misses the third:
~/.claude/projects/<project>/*.jsonl ~/.claude/projects/<project>/<session>/*.jsonl ~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl
Subagent spend lives in that last tier. releve-scan.py walks all three; --no-subagents excludes them deliberately, which is different from missing them.
Budget alerts
--budget exits 1 when the window is over the threshold, so it works as a cron guard:
python3 releve-scan.py --days 7 --budget 100 --quiet
--notify sends a desktop notification with the total (macOS osascript, Linux notify-send).
Automate with cron
crontab -e
0 9 * * * /usr/bin/python3 /path/to/releve-scan.py --days 7 --budget 100 --notify --quiet
On macOS, launchd survives reboots. Create ~/Library/LaunchAgents/com.releve.costs.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.releve.costs</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/path/to/releve-scan.py</string>
<string>--days</string>
<string>7</string>
<string>--budget</string>
<string>100</string>
<string>--notify</string>
<string>--quiet</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>
Load it:
launchctl load ~/Library/LaunchAgents/com.releve.costs.plist
Reading the breakdown
releve-scan.py --out releve.json writes aggregates only, no prompt or response text. Drop that file on releve.neorgon.com and the same total is cut by model, project, skill, effort, git branch, main thread against subagent, service tier and Claude Code version. Nothing is uploaded: the file is read in the browser.
Add --anonymize before sharing one. It hashes project labels and drops cwd, gitBranch, title and slug.
The rate card is editable on the page, so you can reprice the whole window against a rate you have verified yourself rather than trusting the one shipped.
Limitations
- Not an invoice. Every figure is tokens multiplied by a published list rate. Nothing here reads a bill or the Admin API.
- Subscription users see what the work would have cost through the API. The plan number is whatever you type in.
- Bedrock and Vertex rates differ and are left null rather than guessed. A wrong rate presented confidently is worse than a stated gap.
- Web search (\$10 per 1,000 searches) and code execution time are not counted.
- Thinking tokens are already inside
output_tokens. Report them as a label, never add them to cost.