A public leaderboard of the tokens AI builders burn with Claude Code, Codex, opencode, pi and Cursor. 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)
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. Every session is keyed by its id and the server upserts rather than adds, so re-reporting the same session 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 |
| POST | /api/ingest |
Bearer | upsert {source, sessions[]} (live reporting) |
| POST | /api/history |
Bearer | bulk backfill {source, 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.
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 --clearBackfill 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 are the same idempotent upsert as /api/ingest — keyed by session id — so it's
safe to run backfill while the hooks are active and safe to re-run.
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.