Three Plays, one core
comped is three rote Plays on one dependency-free Python package, comped_core. The package is bundled byte-identical into each Play (a sync check in CI enforces it), so the three share every adapter, the ledger, the price table and the renderers. Every step is one python3 invocation printing a 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.
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 name on the gotcomped.com leaderboard, and in the /play settle command. Blank posts anonymously (anon-xxxx). gotcomped.com/run.sh fills in your rote handle. |
card_theme | string | dark | dark or light. |
leaderboard | string | true | true posts your score to gotcomped.com after the card is written and prints your rank: the multiplier, tier, list-price total, plan, detected providers, days, and your handle. Nothing else, ever; the exact payload is saved to out_dir/comped-rank.json. false sends nothing, and the run is then entirely offline. |
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, the line to post, a link that draws the card as a downloadable picture, 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 pointing at the card page, when it doesn't: that page draws the same card in your browser and downloads it as a PNG. |
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 the logs. The tables are generated from the dataclasses that define them, so they cannot drift from the code.
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 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. |
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: cumulative counters, 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. CI checks the per-model Claude Code totals against ccusage, an independent parser.
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. Its repeat cost is the cluster's total minus its cheapest single solve: what you paid to ask again. Harness-generated text: continuation preambles, injected reminders, observer prompts: stays in the ledger, because it costs real money, but is never counted as something you asked for.
Wrong turns
Three signals with honest confidence labels. Tool errors (high): the call returned an error; its first line, stripped of paths and numbers, is the signature. 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.
Detection
Nothing on the machine records which subscription you pay for, and the one place it is written is a file this tool refuses to open. So plan defaults to auto and the run infers what it can:
- 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.
- 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. Claude Code pointed at Moonshot or Z.ai gives itself away the same way. - The tier: the one thing no log records. A Pro session and a Max session are the same bytes. So every subscription the detected providers sell is priced against the window at once; the assumed row is deliberately the least flattering: the most expensive plan that fits, i.e. the smallest multiplier you could honestly claim: and the rest are one glance away.
- Anything else. A provider with no subscription in the table is named, its spend stays in the total, and the card says nothing in the plan cost covers it.
plan=usd:<amount>prices one the table lacks;plan=<id>overrides the inference outright.
| 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 table above is generated from comped_core/detect.py.
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. Where several upstream keys map to one model: the vendor's own and a reseller's: the vendor's wins.
| 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 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. auto is not a price: it means infer the provider and price every row it sells.
| 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 | - |
The tier is inferred from model ids in the logs, never from an 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.
Leaderboard API
Two endpoints on this origin, both JSON, both stdlib Python on Vercel (api/score.py, api/leaderboard.py), both thin: each calls one Postgres function through PostgREST with a publishable key. The SQL functions are the trust boundary. The table is closed to the API role; every bound is enforced in SQL; the device id that keys a row is never returned by either call, so nothing you can read lets you write someone else's row.
POST /api/score
{"device": "<uuid>", "handle": "priya", "multiplier": 12.99, "comped_usd": 2560.98, "plan_usd": 197.13,
"tier": "All-you-can-eat", "plan": "Claude Max 20x", "plan_id": "claude-max-200", "plan_source": "auto",
"providers": ["anthropic"], "harnesses": ["claude-code"], "days_back": 30, "active_days": 22,
"sessions": 99, "cache_share": 0.98, "client": "comped/0.1.5"}
Reply 200: {"ok": true, "rank": 7, "of": 312, "percentile": 2.2, "eligible": true, "held": false, "reason": null, "handle": "priya", "url": "…/leaderboard.html#priya", "board": "…"}. 400 names the first bad field; 429 is the same device inside 15 seconds; 502 is storage. The server recomputes multiplier = comped_usd / plan_usd and refuses a mismatch above 2%. Handles are [A-Za-z0-9][A-Za-z0-9_.-]{0,31}. Over 2,000× or $250,000 is stored with held: true and not shown.
GET /api/leaderboard?sort=multiplier|comped_usd&limit=100
One row per handle (the latest run) or per anonymous device, ranked; up to 500. Each row carries what the table above shows plus plan_id, plan_source, runs, first_seen and updated_at, and a rules object restates the thresholds. Cached for 30 seconds at the edge. CORS is open: embed it where you like.
Posting without the Play
python3 leaderboard/post_score.py --out-dir ~/comped --handle you after a card run does exactly what the Play's last step does; --url points it elsewhere and COMPED_LEADERBOARD_URL does the same for the Play. A machine whose python cannot verify TLS certificates (a python.org build on a Mac that never ran Install Certificates) falls back to the system CA bundle, then to curl with a fixed argv.
Verifying the privacy claims
- No network in the core.
python3 -m unittest tests.test_no_networkfails ifcomped_coreimportsurllib,http,socket,requestsorssl, if anything but the PNG renderer mentionssubprocess, or if any source line references a credential path. A second test proves the only file in any Play package that can open a socket ispost_score.py, the leaderboard poster, and that it is bundled intocompedalone. The site is served withconnect-src 'self': the page can ask this origin for the board and nobody else for anything. - What the poster sends is one JSON object, built in one function from the priced summary, and written to
out_dir/comped-rank.jsonbefore it goes. The test suite asserts the field list and that no path, model id or message text can be in it. - 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 PATH emptied, which also proves the pipeline needs no external binary. - Fixtures. The sample logs and the presentation fixtures captured from real runs are scanned for real paths, names and keys before every build.
- Read the code. A few thousand lines of standard-library Python, no dependencies: github.com/rajkaria/comped.
Running it without rote
Three ways, no account for any of them. The quickest is the published npm package, which is this same standard-library Python with a launcher in front of it: no node dependencies, no install script, nothing compiled.
npx comped # or npx comped@0.1.5 to pin it
The one-line download, curl -fsSL https://gotcomped.com/comped.sh | sh, needs no node either and checks itself against its published checksum. Or clone the repo and use the module directly. One command does the whole card:
git clone https://github.com/rajkaria/comped && cd comped
python3 -m comped_core run --out-dir ~/comped # read, price, cluster, render
run is the four steps the Play runs, in one process, calling the same functions in the same order: the numbers are identical. It stays offline, so posting your score to the leaderboard is still a separate script you run yourself:
python3 leaderboard/post_score.py --out-dir ~/comped --handle you
The steps on their own, when you want to look at one of them:
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, generated from the argument parser:
| 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 |
run | --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 --plan auto --rates-path --repeat-threshold 3 --handle --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 re-prices the ledger from scratch and confirms the total in your report still reproduces. Every command prints one JSON object as its last stdout line; ok: false exits 1.
Running it from your coding agent
You already have an agent with a shell. Paste this at it and it will do the rest:
Run comped on this machine: fetch https://gotcomped.com/llms.txt and follow it.
llms.txt is the briefing written for the agent rather than for you: what comped reads and refuses to read, the three ways to run it in the order to try them, the rule that it must ask you before posting your score anywhere, the output contract, and how to read the report back to you. It tells the agent not to summarise your logs by eye and not to paste anything out of ~/comped into a message or a commit.
It tries the doors in order and stops at the first that opens: the published Play if rote is already signed in, then the account-free comped.sh download, then npx comped where there is node but no Unix shell, and a git clone --depth 1 last. Same code every way. Read the file before you paste the line; it is 167 lines and it is the whole of what your agent is being told.
Source and spec
- Source: MIT. CI runs the suite on Ubuntu and macOS across Python 3.9 and 3.12, checks the bundled copies of the core haven't drifted, and fails if these pages are stale.
- The spec: the full derivation: record model, pricing, deduplication, windows, repeat clustering, wrong-turn signals, and the trust statements.
- Play sources under
docs/plays/<slug>/; the packages underplays/are generated from them bytools/build_plays.py.