Install
You need two things: rote, and a python3 of at least 3.9. Nothing else — no pip install, no node, no lockfile.
curl -fsSL https://getrote.dev/install | sh
Then sign in when it asks. That's it; the Plays pull themselves on first run.
Already have rote? Check you're on 0.78 or newer with rote --version. The Plays declare their tool requirements in deps.toml, and rote play inspect <uri> will tell you before you run anything whether this machine satisfies them.
Quick start
Run it against the synthetic logs bundled inside the Play first. They are real logs in structure and completely fabricated in content, so you can watch the whole thing work without pointing it at anything of yours:
rote play run https://play.modiqo.ai/rajkaria/comped \
claude_dir=resources/fixtures/claude \
codex_dir=resources/fixtures/codex \
out_dir=comped-demo
Eight steps, about two seconds, and a card. Then the real thing:
rote play run https://play.modiqo.ai/rajkaria/comped
Ten seconds or so on a month of heavy use. Everything lands in ~/comped/. Run it again tomorrow and the card grows a line telling you what moved.
Reading the card
Top to bottom, the card says:
- The total. What the window's tokens would have cost at API list prices. Not a bill — you're on a subscription and you paid what you paid.
- The multiplier. That total divided by your plan, prorated across the window by days ÷ 30.4375. You do not type the plan: see what you're running.
- Spend per model, largest first, with a bar. If one model is quietly eating your month, this is where you see it.
- Cache-read share. The fraction of your input tokens served from cache. High is good and normal — most agent traffic is re-reading the same context. A number near zero means something at the front of your prompt keeps changing and the whole prefix is being re-billed.
- Active days and sessions, so a big total can be read as either heavy use or a heavy week.
- The delta, once there's a previous run to compare against.
- Repeat offenders. Jobs you've asked for repeatedly, with what the repeats cost, and the command to turn the top one into a Play so you stop paying for it.
- The Rote dividend. What those repeats would have cost as a Play instead, at Modiqo's stated 98% reduction and at a more conservative 80%.
- Detected. Which providers your window actually used — Claude, GPT/Codex, Kimi, GLM and the rest — with their share and the harnesses they came through. None of it typed.
- If you're on… every subscription those providers sell, priced against this window at once, with the assumed row marked. Your tier is the one thing your logs do not record, so the card shows all of them rather than asking.
What you're running, worked out for you
Asking you which AI you use makes no sense: your logs already say. Every usage record carries the model id the harness sent, and that id names the provider behind it. So plan defaults to auto and the run works the rest out itself.
- The harnesses. Claude Code, Codex CLI, Pi, OpenCode. A directory that isn't there is a shrug, and the card names the ones it didn't find so you know what wasn't counted.
- The provider. Read off the model id, after gateway and region prefixes are stripped —
us.anthropic.claude-opus-5,bedrock/anthropic.claude-sonnet-5andclaude-opus-5are one provider, not three. Point Claude Code at Moonshot or Z.ai and the ids give that away too, which is the whole point. - The tier — the one thing no log records. A Pro session and a Max session are the same bytes. So instead of asking, every subscription the detected providers sell is priced against your window at once and you read your own row. The one the card assumes is deliberately the least flattering: the most expensive plan that fits, which is the smallest multiplier you could honestly claim.
- Anything else. A provider with no subscription in the table (Kimi, GLM, DeepSeek and friends) is named, its spend stays in the total, and the card says plainly that nothing in the plan cost covers it. If you pay for one,
plan=usd:29prices it without the table having to know.
Overriding is still one word — plan=claude-pro-20, or several separated by commas — and it wins over the inference.
| Provider | What you call it | Model ids that name it | Subscriptions priced |
|---|---|---|---|
| Anthropic | Claude | claude, anthropic… | claude-pro-20, claude-max-100, claude-max-200 |
| OpenAI | GPT / Codex | gpt, o1, o3, o4, o5, codex, chatgpt, davinci… | chatgpt-plus-20, chatgpt-pro-200 |
| Moonshot | Kimi | kimi, moonshot… | — |
| Z.ai | GLM | glm, zai, zhipu, chatglm… | — |
| DeepSeek | DeepSeek | deepseek… | — |
| Gemini | gemini, gemma, palm, bison… | — | |
| xAI | Grok | grok… | — |
| Alibaba | Qwen | qwen, qwq… | — |
| MiniMax | MiniMax | minimax… | — |
| Mistral | Mistral | mistral, codestral, devstral, magistral, ministral, pixtral… | — |
| Meta | Llama | llama, meta-llama… | — |
| Amazon | Nova | nova-, titan… | — |
| Cohere | Command | command… | — |
Detection reads nothing new. It looks at records the ledger already parsed, and at which of the four log directories existed. A model id nobody recognises is reported by name as unknown rather than assigned to a provider by guess.
The three Plays
comped
The card: what your window cost at list price, the multiplier against your plan, and the asks you keep repeating.
rote play run https://play.modiqo.ai/rajkaria/comped
Steps. read_claude, read_codex, read_pi, read_opencode (in parallel) → merge_ledger → price_ledger → find_repeats → render_card
Parameters. Every one has a default, so the bare command above works. Pass them as name=value after the URI.
| Parameter | Type | Default | What it does |
|---|---|---|---|
days_back | integer | 30 | Filter on each record's own timestamp. |
out_dir | string | ~/comped | Created if missing. Everything this Play writes goes here and nowhere else. |
claude_dir | string | ~/.claude/projects | Set to resources/fixtures/claude for a demo run on synthetic logs. |
codex_dir | string | ~/.codex/sessions | Set to resources/fixtures/codex for a demo run on synthetic logs. |
pi_dir | string | ~/.pi/agent/sessions | Best-effort adapter; the source note says so. |
opencode_dir | string | ~/.local/share/opencode/storage | Best-effort adapter; the source note says so. |
include_subagents | string | true | Claude Code subagent transcripts. true or false. |
redact | string | true | true stores a 120-character truncation plus sha256; false keeps full text locally, never in a card. |
plan | string | auto | Leave it. auto reads the model ids already in your logs, names the providers behind them (Claude, GPT/Codex, Kimi, GLM, DeepSeek, Gemini, Grok, Qwen and the rest) and prices every tier those providers sell, marking the least flattering one as assumed -- you read your own row instead of typing it. Override with a comma-separated list of ids, or usd:<amount> for a subscription this table does not carry. This Play never opens your OAuth files to find out. |
repeat_threshold | integer | 3 | Minimum asks for a repeat offender; also needs 2 sessions and 2 days. |
rates_path | string | (empty) | Path to a prices.json that replaces the bundled table. |
handle | string | (empty) | Your rote handle, used only to print the /play settle command. |
card_theme | string | dark | dark or light. |
What it writes, all of it under out_dir and nowhere else:
| File | What's in it |
|---|---|
comped-report.md | The whole run in Markdown: card, per-model table, sources, repeats, dividend, delta, unpriced models, methodology, privacy, and every path written. |
comped-card.svg | The shareable card, 1200×675. |
comped-card-square.svg | The same card on a square canvas. PNG renderers fit thumbnails into a square box and would otherwise crop the wide one. |
comped-card.png | Rendered from the square SVG when this machine has rsvg-convert or macOS qlmanage. Absent, with a note, when it doesn't. |
comped-explain.txt | One line per model showing tokens × rate = dollars, plus the plan arithmetic and one line per source. |
comped-share.txt | A post you can paste, with the numbers already in it. |
comped-baseline.json | Totals and repeat labels from this run, so the next one can show you the delta. |
ledger.jsonl | The full ledger. Same file session-ledger produces. |
ledger-summary.json | Counts per source, including what could not be read and why. |
session-ledger
The deduplicated ledger everything else is built on. Nothing priced, nothing judged.
rote play run https://play.modiqo.ai/rajkaria/session-ledger
Steps. read_claude, read_codex, read_pi, read_opencode (in parallel) → merge_ledger → summarize
Parameters. Every one has a default, so the bare command above works. Pass them as name=value after the URI.
| Parameter | Type | Default | What it does |
|---|---|---|---|
days_back | integer | 30 | Filter on each record's own timestamp. |
out_dir | string | ~/comped | Created if missing. Everything this Play writes goes here and nowhere else. |
claude_dir | string | ~/.claude/projects | Set to resources/fixtures/claude for a demo run on synthetic logs. |
codex_dir | string | ~/.codex/sessions | Set to resources/fixtures/codex for a demo run on synthetic logs. |
pi_dir | string | ~/.pi/agent/sessions | Best-effort adapter; the source note says so. |
opencode_dir | string | ~/.local/share/opencode/storage | Best-effort adapter; the source note says so. |
include_subagents | string | true | Claude Code subagent transcripts. true or false. |
redact | string | true | true stores a 120-character truncation plus sha256; false keeps full text locally, never in a card. |
What it writes, all of it under out_dir and nowhere else:
| File | What's in it |
|---|---|
ledger-<harness>.jsonl | One partial ledger per harness read — the reads run in parallel and each writes its own. |
ledger.jsonl | The merged, deduplicated, turn-attributed ledger. |
ledger-summary.json | Record, message and tool counts, sessions, subagent records, and a per-source report. |
wrong-turns
Recurring mistakes, what recovering from them cost, and a drafted rule for each.
rote play run https://play.modiqo.ai/rajkaria/wrong-turns
Steps. read_claude, read_codex (in parallel) → merge_ledger → classify_turns → draft_rules
Parameters. Every one has a default, so the bare command above works. Pass them as name=value after the URI.
| Parameter | Type | Default | What it does |
|---|---|---|---|
days_back | integer | 14 | Filter on each record's own timestamp. |
out_dir | string | ~/comped | Created if missing. Everything this Play writes goes here and nowhere else. |
claude_dir | string | ~/.claude/projects | Set to resources/fixtures/claude for a demo run on synthetic logs. |
codex_dir | string | ~/.codex/sessions | Set to resources/fixtures/codex for a demo run on synthetic logs. |
include_subagents | string | true | Claude Code subagent transcripts. true or false. |
min_recurrence | integer | 3 | How many times a mistake class must recur before it is reported; it also needs 2 sessions. |
show_snippets | string | true | true shows one redacted evidence line per class; false replaces it with (snippets hidden). |
rules_target | string | both | claude, agents or both. Drafts only; nothing is ever written to your rules files. |
What it writes, all of it under out_dir and nowhere else:
| File | What's in it |
|---|---|
wrong-turns-report.md | A table of recurring mistake classes: kind, confidence, tool, signature, count, sessions, recovery cost, evidence. |
wrong-turns-rules.md | The drafted rules, one block per class, ready to paste into CLAUDE.md or AGENTS.md. Nothing is applied for you. |
ledger.jsonl | The ledger it classified. |
What it tracks, field by field
Three record types come out of your logs. This is all of them — the table below is generated from the code that defines them, so it cannot drift.
Usage records — one per API call
| Field | Meaning |
|---|---|
harness | Which tool wrote the line: claude-code, codex, pi, opencode. |
session_id | The harness's own session identifier. |
record_id | The dedup key. For Claude Code that is (message.id, requestId) — the pair that collapses streaming duplicates. |
timestamp | The record's own timestamp, in UTC. Windowing uses this, never the file's mtime. |
model | As written by the harness, before alias resolution. |
input_tokens | Uncached input tokens. |
cache_write_tokens | Tokens written to the prompt cache, billed at a premium. |
cache_read_tokens | Tokens served from cache, billed at a discount. Usually most of your traffic. |
output_tokens | Generated tokens. |
reasoning_tokens | Thinking tokens, reported separately and billed as output. |
project | The working directory the session ran in. |
is_subagent | True for subagent and sidechain traffic, which is easy to forget and expensive to ignore. |
turn_id | The message that started this turn — how cost gets attributed to what you asked. |
Human messages — one per message in the user role
These exist for one reason: to attribute cost to what you asked. Without them a month of agent work is an undifferentiated wall of API calls.
| Field | Meaning |
|---|---|
harness | Which tool the message came from. |
session_id | The session it belongs to. |
message_id | Stable id, used as the turn id. |
timestamp | UTC. |
text | Truncated to 120 characters by default. Set redact=false and full text stays local. |
text_sha256 | Hash of the normalised text, so identical asks can be matched without keeping them. |
project | Working directory. |
origin | human, unknown or automated. Harness-generated messages arrive in the user role and are labelled, not counted as yours. |
Tool events — one per tool call
| Field | Meaning |
|---|---|
harness | Which tool. |
session_id | Session. |
event_id | Stable id. |
timestamp | UTC. |
tool_name | Bash, Edit, exec_command, and so on — resolved from the call that named it. |
input_summary | One short line: the command, path or query. Never the full input. |
is_error | Whether the call came back as an error. |
error_text | Up to 300 characters of the error, for errors only. |
turn_id | The turn this happened in. |
What is never collected: file contents, tool outputs beyond a 300-character error snippet, prompt text beyond the 120-character truncation, and anything at all from a credential, keychain or token file. There is no identifier for you, no machine id and no run id that leaves your disk, because nothing leaves your disk.
The arithmetic
Pricing
Per record, in exact decimal arithmetic — never floating point, which is how cent-level errors get into totals:
usd = uncached_input × in_rate
+ cache_write × cache_write_rate
+ cache_read × cache_read_rate
+ output × out_rate
Reasoning tokens are already counted inside output, because that is how they are billed. Rounding happens once, at display time.
Deduplication
Claude Code writes a line per content block, so the same API call appears several times with the same message.id and requestId. On real logs about four in ten usage lines are duplicates. They are collapsed on that pair, and the count of what was dropped appears in the source report. Codex has the opposite shape: its counters are cumulative totals, so each record is the difference from the previous snapshot, and a counter that goes backwards starts a new baseline rather than producing a negative.
Windows and the multiplier
A record is in the window if its own timestamp is, never the file's modification time. Plan cost is prorated by days_back ÷ 30.4375 — the mean month — so a 14-day window is compared against 14 days of subscription, not a whole month of it.
Repeat offenders
Messages are normalised (lowercased, paths, URLs, numbers and hashes replaced by placeholders, stop-words dropped), turned into 2-word shingles, and clustered when their Jaccard similarity is 0.5 or higher. A cluster qualifies when it has at least repeat_threshold asks across two or more sessions on two or more days — one frustrated afternoon of retries is not a repeated job. Its repeat cost is the cluster's total minus its cheapest single solve: what you paid to ask again.
Harness-generated text is kept out: session-continuation preambles, injected reminders, observer prompts and anything in the automated origin class. They stay in the ledger, because they cost real money, but they are not things you asked for.
Wrong turns
Three signals, with honest confidence labels. Tool errors (high confidence): the call returned an error, and the error's first line is stripped of paths and numbers to make a signature that clusters across sessions. Corrections (medium): your next message matched a correction phrase — "no,", "revert", "that's not", "undo". Reverts (high): a destructive git command ran. A class is reported when it recurs at least min_recurrence times across two or more sessions. Recovery cost is the signalling turn plus the next one — what it took to get back on track.
Prices and plans
The price table is a snapshot, bundled with the Play, that carries its own provenance: the source URL, the upstream file's sha256, and the date it was taken. It is never fetched at runtime.
| Source | https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json |
|---|---|
| As of | 2026-09-04 |
| Models | 78 |
MiniMax-M2, MiniMax-M2.1, MiniMax-M2.5, MiniMax-M3, claude-fable-5, claude-fable-5-1, claude-haiku-4-5, claude-opus-4-1, claude-opus-4-5, claude-opus-4-6, claude-opus-4-7, claude-opus-4-8, claude-opus-5, claude-sonnet-4-5, claude-sonnet-4-6, claude-sonnet-5, codestral-latest, codex-mini-latest, deepseek-chat, deepseek-reasoner, deepseek-v3.2, deepseek-v4-flash, deepseek-v4-pro, devstral-medium-latest, gemini-2.5-flash, gemini-2.5-pro, gemini-3-flash-preview, gemini-3-pro-preview, glm-4.5, glm-4.5-air, glm-4.6, glm-4.7, glm-4.7-flash, glm-5, glm-5-code, glm-5.1, glm-5.2, glm-5.3, glm-5.3-flash, gpt-5, gpt-5-codex, gpt-5-mini, gpt-5-nano, gpt-5.1, gpt-5.1-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2, gpt-5.2-codex, gpt-5.2-pro, gpt-5.3-codex, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.4-pro, gpt-5.5, gpt-5.5-pro, gpt-5.6, grok-4, grok-4.3, grok-4.6, grok-code-fast-1, kimi-k2-0905-preview, kimi-k2-thinking, kimi-k2-thinking-turbo, kimi-k2-turbo-preview, kimi-k2.5, kimi-k2.6, kimi-k2.7-code, kimi-k3, kimi-latest, mistral-large-latest, o3, o4-mini, qwen-max, qwen-plus, qwen3-coder, qwen3-coder-plus
A model that is not in this list is reported under "unpriced" with its token counts, and no dollar figure is invented for it.
Plans
Public list prices as of 2026-09-03. You normally pass none of these: plan=auto prices every row a detected provider sells and marks the one it assumed. Pass one id, or several separated by commas, to override — or usd:<amount> for a subscription this table does not carry.
| Plan id | Label | Monthly |
|---|---|---|
auto | Detected from your logs | — |
claude-pro-20 | Claude Pro | $20 |
claude-max-100 | Claude Max 5x | $100 |
claude-max-200 | Claude Max 20x | $200 |
chatgpt-plus-20 | ChatGPT Plus | $20 |
chatgpt-pro-200 | ChatGPT Pro | $200 |
api | API pay-as-you-go | — |
unknown | Unknown plan | — |
Your tier is inferred from the model ids in your own logs, never from your account. The tool will not read ~/.claude.json or ~/.codex/auth.json to discover it, because a tool that reads your OAuth files to be convenient is a tool you should not run.
Privacy, and how to check it
The claims are on the front page. Here is how you verify them rather than believing them:
- No network.
python3 -m unittest tests.test_no_networkfails if the core importsurllib,http,socket,requestsorssl, if anything but the PNG renderer mentionssubprocess, or if any source line references a credential path. - No surprises in what it writes. Every run lists every path it wrote, in the report and in its JSON output.
- Determinism. Pin
--nowand two runs produce byte-identical output. The suite proves it with the PATH emptied, which also proves the pipeline needs no external binary. - Read the code. It is a few thousand lines of standard-library Python with no dependencies. github.com/rajkaria/comped.
Running it without rote
The Plays are a thin wrapper around a Python package with no dependencies. If you would rather run it directly, clone the repo and use the module. Every subcommand prints one JSON object as its last line; a missing log directory is a warning and exit 0, bad arguments exit 2, and nothing ever prints a traceback.
git clone https://github.com/rajkaria/comped && cd comped
python3 -m comped_core ledger --days-back 30 --out-dir ~/comped
python3 -m comped_core price --out-dir ~/comped # --plan auto by default
python3 -m comped_core repeats --out-dir ~/comped --repeat-threshold 3
python3 -m comped_core card --out-dir ~/comped
The full set:
| Subcommand | Options |
|---|---|
ledger | --out-dir ~/comped --claude-dir ~/.claude/projects --codex-dir ~/.codex/sessions --pi-dir ~/.pi/agent/sessions --opencode-dir ~/.local/share/opencode/storage --days-back 30 --include-subagents true --redact true --now --only |
merge | --out-dir ~/comped |
price | --out-dir ~/comped --plan auto --rates-path --days-back 0 --now |
repeats | --out-dir ~/comped --repeat-threshold 3 --handle |
card | --out-dir ~/comped --card-theme dark |
wrongturns | --out-dir ~/comped --min-recurrence 3 --show-snippets true |
rules | --out-dir ~/comped --rules-target both |
explain | --out-dir ~/comped |
verify | --out-dir ~/comped |
sources | --claude-dir ~/.claude/projects --codex-dir ~/.codex/sessions --pi-dir ~/.pi/agent/sessions --opencode-dir ~/.local/share/opencode/storage |
summary | --out-dir ~/comped |
verify is worth knowing about: it re-prices the ledger from scratch and confirms the total in your report still reproduces.
Troubleshooting
"no log directory found"
Expected, not an error, if you don't use that harness — the step exits 0 with a warning and the run continues. If you do use it and it's missing, point the parameter at the right path: claude_dir, codex_dir, pi_dir, opencode_dir.
The total looks too low
Check days_back — it defaults to 30 — and check the unpriced list at the bottom of the report. A model missing from the price table contributes tokens but no dollars, on purpose.
No repeat offenders
The bar is deliberately high: three asks, two sessions, two days. Try repeat_threshold=2. If you work on one thing at a time in long sessions, you may genuinely not repeat yourself across days.
No PNG
Install rsvg-convert, or use macOS where qlmanage is built in. The SVG is always written, and it uploads to LinkedIn as-is.
The numbers moved and I didn't change anything
You ran it on a different day: the window slid. Pin it with --now on the CLI if you need two runs to be comparable.