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.

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 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_themestringdarkdark or light.
leaderboardstringtruetrue 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:

FileWhat's in it
comped-report.mdThe 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.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 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.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 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

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

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:

  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.
  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. Claude Code pointed at Moonshot or Z.ai gives itself away the same way.
  3. 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.
  4. 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.
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 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.

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

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_network fails if comped_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. A second test proves the only file in any Play package that can open a socket is post_score.py, the leaderboard poster, and that it is bundled into comped alone. The site is served with connect-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.json before 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 --now and 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:

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
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 under plays/ are generated from them by tools/build_plays.py.