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.

  1. 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.
  2. The provider. Read off the model id, after gateway and region prefixes are stripped — us.anthropic.claude-opus-5, bedrock/anthropic.claude-sonnet-5 and claude-opus-5 are one provider, not three. Point Claude Code at Moonshot or Z.ai and the ids give that away too, which is the whole point.
  3. 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.
  4. 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:29 prices 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.

ProviderWhat you call itModel ids that name itSubscriptions priced
AnthropicClaudeclaude, anthropic…claude-pro-20, claude-max-100, claude-max-200
OpenAIGPT / Codexgpt, o1, o3, o4, o5, codex, chatgpt, davinci…chatgpt-plus-20, chatgpt-pro-200
MoonshotKimikimi, moonshot…
Z.aiGLMglm, zai, zhipu, chatglm…
DeepSeekDeepSeekdeepseek…
GoogleGeminigemini, gemma, palm, bison…
xAIGrokgrok…
AlibabaQwenqwen, qwq…
MiniMaxMiniMaxminimax…
MistralMistralmistral, codestral, devstral, magistral, ministral, pixtral…
MetaLlamallama, meta-llama…
AmazonNovanova-, titan…
CohereCommandcommand…

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.

ParameterTypeDefaultWhat it does
days_backinteger30Filter on each record's own timestamp.
out_dirstring~/compedCreated if missing. Everything this Play writes goes here and nowhere else.
claude_dirstring~/.claude/projectsSet to resources/fixtures/claude for a demo run on synthetic logs.
codex_dirstring~/.codex/sessionsSet to resources/fixtures/codex for a demo run on synthetic logs.
pi_dirstring~/.pi/agent/sessionsBest-effort adapter; the source note says so.
opencode_dirstring~/.local/share/opencode/storageBest-effort adapter; the source note says so.
include_subagentsstringtrueClaude Code subagent transcripts. true or false.
redactstringtruetrue stores a 120-character truncation plus sha256; false keeps full text locally, never in a card.
planstringautoLeave 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_thresholdinteger3Minimum asks for a repeat offender; also needs 2 sessions and 2 days.
rates_pathstring(empty)Path to a prices.json that replaces the bundled table.
handlestring(empty)Your rote handle, used only to print the /play settle command.
card_themestringdarkdark or light.

What it writes, all of it under out_dir and nowhere else:

FileWhat's in it
comped-report.mdThe whole run in Markdown: card, per-model table, sources, repeats, dividend, delta, unpriced models, methodology, privacy, and every path written.
comped-card.svgThe shareable card, 1200×675.
comped-card-square.svgThe same card on a square canvas. PNG renderers fit thumbnails into a square box and would otherwise crop the wide one.
comped-card.pngRendered from the square SVG when this machine has rsvg-convert or macOS qlmanage. Absent, with a note, when it doesn't.
comped-explain.txtOne line per model showing tokens × rate = dollars, plus the plan arithmetic and one line per source.
comped-share.txtA post you can paste, with the numbers already in it.
comped-baseline.jsonTotals and repeat labels from this run, so the next one can show you the delta.
ledger.jsonlThe full ledger. Same file session-ledger produces.
ledger-summary.jsonCounts 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.

ParameterTypeDefaultWhat it does
days_backinteger30Filter on each record's own timestamp.
out_dirstring~/compedCreated if missing. Everything this Play writes goes here and nowhere else.
claude_dirstring~/.claude/projectsSet to resources/fixtures/claude for a demo run on synthetic logs.
codex_dirstring~/.codex/sessionsSet to resources/fixtures/codex for a demo run on synthetic logs.
pi_dirstring~/.pi/agent/sessionsBest-effort adapter; the source note says so.
opencode_dirstring~/.local/share/opencode/storageBest-effort adapter; the source note says so.
include_subagentsstringtrueClaude Code subagent transcripts. true or false.
redactstringtruetrue 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:

FileWhat's in it
ledger-<harness>.jsonlOne partial ledger per harness read — the reads run in parallel and each writes its own.
ledger.jsonlThe merged, deduplicated, turn-attributed ledger.
ledger-summary.jsonRecord, 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.

ParameterTypeDefaultWhat it does
days_backinteger14Filter on each record's own timestamp.
out_dirstring~/compedCreated if missing. Everything this Play writes goes here and nowhere else.
claude_dirstring~/.claude/projectsSet to resources/fixtures/claude for a demo run on synthetic logs.
codex_dirstring~/.codex/sessionsSet to resources/fixtures/codex for a demo run on synthetic logs.
include_subagentsstringtrueClaude Code subagent transcripts. true or false.
min_recurrenceinteger3How many times a mistake class must recur before it is reported; it also needs 2 sessions.
show_snippetsstringtruetrue shows one redacted evidence line per class; false replaces it with (snippets hidden).
rules_targetstringbothclaude, 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:

FileWhat's in it
wrong-turns-report.mdA table of recurring mistake classes: kind, confidence, tool, signature, count, sessions, recovery cost, evidence.
wrong-turns-rules.mdThe drafted rules, one block per class, ready to paste into CLAUDE.md or AGENTS.md. Nothing is applied for you.
ledger.jsonlThe 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

FieldMeaning
harnessWhich tool wrote the line: claude-code, codex, pi, opencode.
session_idThe harness's own session identifier.
record_idThe dedup key. For Claude Code that is (message.id, requestId) — the pair that collapses streaming duplicates.
timestampThe record's own timestamp, in UTC. Windowing uses this, never the file's mtime.
modelAs written by the harness, before alias resolution.
input_tokensUncached input tokens.
cache_write_tokensTokens written to the prompt cache, billed at a premium.
cache_read_tokensTokens served from cache, billed at a discount. Usually most of your traffic.
output_tokensGenerated tokens.
reasoning_tokensThinking tokens, reported separately and billed as output.
projectThe working directory the session ran in.
is_subagentTrue for subagent and sidechain traffic, which is easy to forget and expensive to ignore.
turn_idThe 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.

FieldMeaning
harnessWhich tool the message came from.
session_idThe session it belongs to.
message_idStable id, used as the turn id.
timestampUTC.
textTruncated to 120 characters by default. Set redact=false and full text stays local.
text_sha256Hash of the normalised text, so identical asks can be matched without keeping them.
projectWorking directory.
originhuman, unknown or automated. Harness-generated messages arrive in the user role and are labelled, not counted as yours.

Tool events — one per tool call

FieldMeaning
harnessWhich tool.
session_idSession.
event_idStable id.
timestampUTC.
tool_nameBash, Edit, exec_command, and so on — resolved from the call that named it.
input_summaryOne short line: the command, path or query. Never the full input.
is_errorWhether the call came back as an error.
error_textUp to 300 characters of the error, for errors only.
turn_idThe 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.

Sourcehttps://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
As of2026-09-04
Models78

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 idLabelMonthly
autoDetected from your logs
claude-pro-20Claude Pro$20
claude-max-100Claude Max 5x$100
claude-max-200Claude Max 20x$200
chatgpt-plus-20ChatGPT Plus$20
chatgpt-pro-200ChatGPT Pro$200
apiAPI pay-as-you-go
unknownUnknown 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_network fails if the core imports urllib, http, socket, requests or ssl, if anything but the PNG renderer mentions subprocess, 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 --now and 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:

SubcommandOptions
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.