A public leaderboard of the tokens AI builders burn with Claude Code, Codex, opencode, pi, Cursor and Grok Bot. Pick a username, paste a couple of snippets, and your coding sessions self-report their token usage to the board. No email, no PII — just token counts.
It's a TanStack Start + React app on Cloudflare Workers with a Hono JSON/API layer, backed by one D1 database.
These tools don't hand token counts to hooks — they only point at local session
files. So the installed reporter (reporter/src/* → bundled
tokentally.mjs, a zero-dependency Node script published on npm as
tokenmaxer) reads those files, sums
usage per model, and POSTs the totals — the same files
ccusage parses.
What leaves the machine: per-session token counts, model names, session ids,
and timestamps — never prompts, code, file paths, or credentials. Append
--dry-run to any reporter command to print the exact payloads instead of
sending them.
- Claude Code —
~/.claude/projects/**/<session>.jsonl→message.usage.* - Codex —
~/.codex/sessions/**/rollout-*.jsonl→ lasttoken_countevent - opencode —
~/.local/share/opencode/storage/message/<session>/*.json→tokens.*on each assistant message - pi —
~/.pi/agent/sessions/**/*.jsonl→usageon each assistant record (deduped byid, since pi stores a branching tree) - Cursor — dashboard API fetch via local auth (reporter calls
cursor-sync) - Grok Bot — same Cursor dashboard fetch; its
grok-bot-*rows are stored as their own source
Each row is your usage for one tool, session, model and day, where day is the
calendar day on the machine that reported it. Leaderboard windows are whole
calendar days (today is your local date; 7d is that date plus the six
before it), so a long-running session contributes to every day it touched
instead of booking its lifetime total to the day it opened.
Reporting fires on Claude Code / Codex SessionStart / SessionEnd hooks (no cron, no daemon). opencode and pi have no shell hooks, so a small shell wrapper function runs the reporter each time they exit. Each row is scoped to one user, tool, session, model, and day, and re-reporting a session replaces every row it owns rather than adding to them, so reporting the same session twice never double-counts — which is what makes combining start + end (and the start-only catch-ups) safe.
Token counts are self-reported — this is an honor system with light guardrails
(bearer auth, rate limits, sanity caps). See /about.
src/
server.ts # Worker entry: Hono (API/agent/OG) vs TanStack Start (HTML)
index.tsx # Hono app: JSON API, agent markdown, OG, reporter, cookie routes
routes/ # TanStack file routes (React pages)
pages/ # Shared React page components
api/ # Hono JSON/API route modules
core/api/ # createServerFn loaders for TanStack routes
lib/ # auth, pricing, ratelimit, validate, aggregate, format
drizzle/ # D1 migrations
__tests__/ # vitest unit tests
reporter/src/ # reporter modules (strict TS; esbuild → tokentally.mjs for npm + /tokentally.mjs)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/register |
— | {username} → {id, username, token} |
| POST | /api/token/rotate |
Bearer | rotate your token |
| GET | /api/whoami |
Bearer | {username} for the token |
| POST | /api/ingest |
Bearer | upsert {source, sessions[], replace_sessions?} (live reporting) |
| POST | /api/history |
Bearer | bulk backfill {source, sessions[], replace_sessions?} (past history) |
| POST | /api/profile |
Bearer | set/clear {url} (https public profile link) |
| GET | /api/leaderboard |
— | ?window=&metric=&source=&model=&limit= |
| GET | /api/u/:username |
— | profile totals + breakdown |
| GET | /api/health |
— | {name, version} |
window ∈ today|7d|30d|all, metric ∈ total|input|output|cached|cost, source ∈ claude_code|codex|opencode|pi|cursor|grok_bot.
Non-browser clients (e.g. curl) get Markdown by default on /, /about,
/start, and /u/:username. Browsers still get HTML. Explicit twins:
/llms.txt,/llms-full.txt/index.md,/about.md,/start.md,/u/:username.md
/start.md requires the invite session cookie when INVITE_KEY is set.
pnpm install
pnpm db:migrate:local # apply migrations to local D1
pnpm dev # Vite + TanStack Start (Cloudflare plugin) on :3000
pnpm test # vitest
pnpm typecheck
pnpm build && pnpm preview # production build + Workers previewwrangler d1 create tokentally # paste database_id into wrangler.toml
wrangler kv namespace create RATE_LIMIT # paste id into wrangler.toml
pnpm db:migrate # apply migrations to remote D1
# optionally set PUBLIC_BASE_URL in wrangler.toml [vars]
pnpm deployUsername claims are invite-only via a shared invite link at /invite?token=<KEY> (sets a session cookie, then redirects home; claim the username at /start).
After claiming a username at /start, users get a personalized version of:
npm install -g tokenmaxer && \
mkdir -p ~/.tokenmaxer && \
printf '%s' '{"apiBase":"https://<host>","token":"tt_..."}' > ~/.tokenmaxer/config.jsonClaude Code — merge into ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"type": "shell",
"command": "tokenmaxer claude-sessionstart"
}
],
"SessionEnd": [
{
"type": "shell",
"command": "tokenmaxer claude-sessionend"
}
]
}
}Codex — add to ~/.codex/config.toml (Codex has no SessionEnd hook, so the latest
session reports on the next launch):
[[hooks.session_start]]
type = "command"
command = "tokenmaxer codex-sessionstart"opencode — opencode has no shell hooks, so add a wrapper to ~/.bashrc/~/.zshrc
that reports your latest sessions each time opencode exits:
opencode() { command opencode "$@"; tokenmaxer opencode-sessionstart; }pi — same idea for pi:
pi() { command pi "$@"; tokenmaxer pi-sessionstart; }The token lives only in ~/.tokenmaxer/config.json, never in shared settings files.
(Legacy ~/.tokentally/config.json and TOKENTALLY_* env vars still work as fallbacks.)
The hooks only report sessions going forward (SessionStart catch-up scans the last
TOKENMAXER_DAYS, default 3). To load everything you ran before installing tokenmaxer,
run the one-time backfill — it computes token-count summaries from all local
Claude Code / Codex / opencode / pi / Cursor transcripts and uploads only those summaries:
tokenmaxer backfill --dry-run # print payloads, send nothing
tokenmaxer backfill # all tools
tokenmaxer backfill claude # Claude Code only (same for codex|opencode|pi|cursor)
tokenmaxer set-profile-url https://github.com/YOU # optional public link on /u/YOU
tokenmaxer set-profile-url --clear
tokenmaxer rotate # replace your token (needs the current one)
tokenmaxer whoami # print the username for that tokenBackfill posts to a dedicated POST /api/history endpoint (Bearer auth) rather than
/api/ingest. It's a separate route with its own rate-limit bucket and a larger
per-request cap, so a big one-time upload doesn't eat into the live reporting budget.
Uploads use the same idempotent upsert as /api/ingest — each row scoped to one user,
tool, session, model, and day — so it's safe to run backfill while the hooks are active
and safe to re-run.
If you were reporting before local-day attribution shipped, run tokenmaxer backfill
once — existing data keeps its old day attribution until you do. Cursor backfill only
reaches back 90 days (reporter/src/commands.ts), so Cursor days older than that can't
be re-derived at all.
Estimated USD cost uses a hardcoded per-model table in src/lib/pricing.ts — update
it and redeploy when prices change. Values are estimates, not billing truth.