codex-token-tracker
v0.4.0
Published
Menu bar / system tray app and headless agent that tracks OpenAI Codex token usage, cache hits, model mix and API-equivalent cost — and syncs it to your team dashboard.
Maintainers
Readme
codex-token-tracker
Menu bar / system tray app and headless agent that tracks your OpenAI Codex usage — tokens, cache hit rate, model mix, tokens per second of the running session, subscription rate limits and the API-equivalent cost in dollars — and syncs it to your team's Codex Tracker dashboard (Next.js + Clerk + Convex).
- macOS menu bar app and native Windows tray app (Electron)
codex-token-tracker agentheadless mode for WSL2, Linux and servers- Nothing to install:
npx codex-token-trackerfetches the newest version every time it starts; global installs self-update withcodex-token-tracker updateor the in-app "Update" button - Reads local usage metadata for current Codex-OAuth agents (Codex, pi, oh-my-pi, Cline, Kilo, Hermes, OpenClaw and DeepSeek Harness) — no agent API keys or proxies
- Real-time: today's totals, the live session's generation speed (tokens/s), context window use, weekly / 5-hour rate limits
- Activity heatmap merged from local data + your other devices (realtime database)
- English / 中文, follows your OS language and can be switched and persisted
- Light / dark follows the system theme
Screenshots:
docs/(coming with the first release).
Install
There is nothing to install — run it with npx (Node.js 20+):
npx codex-token-tracker login # the first time on this computer: sign in and approve the device in the browser that opens
npx codex-token-tracker # every day after: start the menu bar app (agent mode where there is no display)npx resolves the newest published version every time it starts, so there is nothing to keep up to
date; turn on Launch at login from the tray menu and the command is never typed again. Prefer a
permanent install?
npm install -g codex-token-tracker
codex-tracker login # then just `codex-tracker`That puts two equivalent commands on your PATH: codex-token-tracker and the shorter alias
codex-tracker, upgraded with codex-token-tracker update, which installs the newest version with
whichever package manager you used (npm / pnpm / yarn / bun). Every example below uses the short form;
read codex-tracker <command> as npx codex-token-tracker <command> if you did not install globally.
Electron is downloaded on first launch, not during install.
npm install -gnever runs Electron's install script, so the install succeeds on locked-down servers and headless machines download nothing. The firstcodex-trackerrun on a desktop fetches the runtime (~100 MB, once) into~/.codex-tracker/electron/<version>/, honouringELECTRON_MIRROR(e.g.https://npmmirror.com/mirrors/electron/) andHTTPS_PROXY. A globally installedelectron(npm i -g electron) is used instead when present.
Node.js 20+ is required. Electron is not an npm dependency: if the runtime cannot be downloaded (locked-down servers, WSL without a desktop), the package still installs and runs in agent mode.
Stuck on Node 16? Install
codex-token-tracker-nodejs16instead — the same core app and version, built from these same sources for Node 16.8+. Current SQLite-backed sources neednode:sqlite, so a Node 16/20 headless process uses the available text/rollout/legacy fallbacks instead. Use one package or the other on a machine, not both, since they install the same commands.
Quick start
codex-tracker login # connect this device to the dashboard (Google / GitHub via Clerk)
codex-tracker # start the menu bar app (agent mode when there is no display)
codex-tracker status # terminal summary — handy in WSL / over SSHBy default the tool connects to https://codex.chenli.dev; other teams pass their own dashboard with --dashboard <url> (remembered afterwards). login prints a short code and the approval link https://<dashboard>/cli-auth?code=XXXX-XXXX, opens it in your browser when this machine has one, and otherwise prints a QR code of the link — scan it with your phone, or open the link on any other computer, sign in and approve. The tracker then receives a device token. Local tracking works without signing in; signing in enables uploads and the multi-device heatmap.
$ codex-tracker login
Connecting this device to https://codex.chenli.dev
Your code: RHF7-DWW8
Open this link on any device — this computer, another one, or your phone — sign in and approve:
https://codex.chenli.dev/cli-auth?code=RHF7-DWW8
The code expires in 15 min.
█▀▀▀▀▀█ ▄▀ █▄ ▀▀▀ █▀▀▀▀▀█ ← scan with a phone camera
█ ███ █ ▀▄▀ ▄ ██▀ █ ███ █
…
Waiting for approval…
Connected as Chen Li. Uploads are enabled.--qr always prints the QR code (also on a desktop), --no-qr never does, and --no-browser only prints the link and the code.
One machine, one device. Logging in more than once from the same computer — the tray app and codex-tracker agent, or a re-login — does not create a second device: the login carries a hashed hardware id (the platform UUID / MachineGuid / /etc/machine-id, SHA-256'd, never the raw value), the dashboard attaches it to the machine's existing device, and its usage is counted once. Inside WSL the Windows MachineGuid is used, so a WSL agent and the Windows tray app on the same PC count as one machine too.
Self-hosted dashboard
codex-tracker login --dashboard https://tracker.your-company.com
# or
codex-tracker config set dashboardUrl https://tracker.your-company.comThe tracker discovers the Convex deployment through <dashboard>/api/config.
Commands
| Command | What it does |
| --- | --- |
| codex-tracker | Menu bar app; falls back to agent when no display / no Electron |
| codex-tracker menubar [--background] | Start the tray app (detached with --background) |
| codex-tracker agent [--interval <sec>] [--once] | Headless tracking + uploads; --once runs one cycle and exits |
| codex-tracker login [--dashboard <url>] [--qr\|--no-qr] [--no-browser] | Device-code login; prints the link and a QR code for phones / other computers |
| codex-tracker logout | Forget the device token (local data stays) |
| codex-tracker status [--json] | Today / 7d / 30d usage, live session, rate limits, models |
| codex-tracker sync | Full sync: rescan every agent and re-upload this device's whole history |
| codex-tracker paths | Detected session directories for every enabled source |
| codex-tracker config get [key] / config set <key> <value> | Settings (see below) |
| codex-tracker lang <en\|zh\|auto> | Display language |
| codex-tracker update [--check] | Install the newest published version; --check only reports it |
| codex-tracker --version / --help | |
Menu bar app
- Tray title shows today's tokens (
12.4k) —config set trayTitle tokens|cost|none - Click: popover with Today, Live session (tokens/s, context window, rate limits), Activity heatmap, Models, Account
- Right-click: Open dashboard, Sign in/out, Language, Launch at login, Refresh, Sync now, Check for updates, Quit
- A banner appears at the top of the popover when a newer version is published; Update installs it and the app asks you to restart
- Launch at login uses a LaunchAgent on macOS and the registry run key on Windows
Sync
The tracker uploads continuously in the background: every 60 s it sends the hour buckets and sessions that changed since the last push. Sync does the full version instead — use it when the dashboard's numbers for this machine look wrong, after you install a new agent, or after an update that changes the pricing table.
Press the ⟳ button in the popover header (or Sync now in the Account card and the tray menu, or run
codex-tracker sync). It:
- re-reads the config, so agents enabled since the app started are picked up;
- re-discovers every session directory and re-parses every transcript from scratch — Codex plus
the coding agents running on your Codex subscription (pi, oh-my-pi, Cline, Kilo, Hermes, OpenClaw,
retained OpenCode / Roo readers) and any
extraSessionDirsyou configured — instead of skipping files whose size and mtime are unchanged; - recomputes all aggregates with the current pricing table;
- re-uploads every still-present bucket and session, not just what changed. The API uses idempotent upserts, so this refreshes current local records but does not delete older remote rows whose source file is no longer present;
- pulls the other devices' rows and the live rate limits back down.
A banner reports the phase while it runs and then what it found (Synced codex, pi · 75 files ·
75 sessions · 70 hours re-uploaded). Signed out, steps 1-3 still run and nothing leaves the machine.
A full sync re-sends your whole history, so it costs more bandwidth than a normal push — it is a manual action, never on a timer.
Windows and WSL2
Native Windows: npx codex-token-tracker login, then npx codex-token-tracker, in PowerShell. The tray app also scans every WSL distro (\\wsl$\<distro>\home\*\.codex\sessions) so sessions run inside WSL are counted.
Inside WSL2: Electron cannot show a Windows tray from WSL, so run the agent:
codex-tracker login # prints the URL, the code and a QR code — approve from your Windows browser or your phone
codex-tracker agent # keep running (tmux / nohup / systemd --user)The agent also scans /mnt/c/Users/*/.codex/sessions, so one agent covers both sides. WSLg can display the Electron window but tray support is limited; the Windows tray app is the recommended UI. Running both the Windows tray app and a WSL agent on one PC is fine since 0.3.0: they identify as the same machine and the dashboard counts it once.
Example systemd --user unit (~/.config/systemd/user/codex-tracker.service) for a global install — with
npx use ExecStart=/usr/bin/npx codex-token-tracker agent (needs the registry reachable at start):
[Service]
ExecStart=%h/.npm-global/bin/codex-tracker agent
Restart=always
[Install]
WantedBy=default.targetConfiguration
Config lives in ~/.codex-tracker/ (override with CODEX_TRACKER_HOME):
| Key | Default | Notes |
| --- | --- | --- |
| dashboardUrl | https://codex.chenli.dev | Your team dashboard (self-hosters: codex-tracker login --dashboard <url>) |
| language | auto | en, zh or auto (OS language) |
| uploadIntervalSec | 60 | Push interval |
| heartbeatIntervalSec | 15 | Live status interval |
| extraSessionDirs | [] | Extra session folders (comma-separated in config set) |
| launchAtLogin | false | macOS / Windows |
| trayTitle | tokens | tokens, cost or none |
| checkUpdates | true | Ask the npm registry (once per 6 h) whether a newer version exists |
| trackAllProviders | false | Parse other providers for local source diagnostics; priced and uploaded totals remain Codex-OAuth-only |
| sources.* | all available sources on | Per-source switches such as sources.openclaw, sources.kilo and sources.cline |
Source-specific location overrides include CODEX_HOME; PI_CODING_AGENT_DIR, PI_CONFIG_DIR and
PI_CODING_AGENT_SESSION_DIR; CLINE_SESSION_DATA_DIR, CLINE_DATA_DIR and CLINE_DIR; KILO_DB and
XDG_DATA_HOME; HERMES_HOME; OPENCLAW_STATE_DIR; and DSH_HOME.
Updates
Started with npx, the tracker is the newest version every time it launches — update and the in-app
button then only ask you to quit and run npx codex-token-tracker again. For global installs,
checkUpdates (default on) asks registry.npmjs.org for the package's latest dist-tag at most once
every 6 hours and caches the answer in ~/.codex-tracker/update.json. Nothing else is sent — the request
carries no usage data and no identifiers. Turn it off with codex-tracker config set checkUpdates false;
codex-tracker update still works on demand. Set CODEX_TRACKER_REGISTRY (or npm_config_registry) to use
a mirror.
Global installs can fail for reasons the app cannot fix — a root-owned npm prefix, a proxy, a read-only volume. When that happens the exact command is shown so you can run it yourself.
Pricing
Costs are "API-equivalent" — standard OpenAI list prices
per 1M tokens (input, cached input, cache writes, output; reasoning tokens are billed as output). Models
with a long-context tier (GPT-6 Astra, GPT-5.6, GPT-5.5, GPT-5.4) bill the whole request at the higher rate —
2× input and cache rates, 1.5× output — when its prompt exceeds 272K tokens. -codex variants are priced at
their base model's rate.
Codex only. Some supported sources (Cline/Roo/Kilo, OpenCode, DeepSeek Harness) can also drive Anthropic, Google or local models. That usage is not counted: this tool reports Codex consumption, and pricing a Claude request against an OpenAI table would be meaningless.
Models missing from the built-in table are priced by family and marked est. Override or add prices in
~/.codex-tracker/pricing.json (cacheWrite is optional and defaults to input; an override is one flat
rate with no long-context tier):
{
"gpt-5.7-nova": { "input": 1.75, "cachedInput": 0.175, "output": 14 }
}Sources
The tracker reads the local transcripts of every agent that can use a Codex subscription (ChatGPT login) and
attributes usage to an agent (shown as "Sources" chips in the popover, a Sources line in codex-tracker status,
and as a tag on live sessions / model rows). Priced, displayed and uploaded totals always require an exact
Codex-OAuth provider signal and an OpenAI model. trackAllProviders only lets source parsers retain other
providers for local diagnostics; it never broadens those totals — see Pricing.
| Source | Where it looks | Current Codex-OAuth accounting |
|---|---|---|
| codex – Codex CLI / Codex Desktop | $CODEX_HOME or ~/.codex/{sessions,archived_sessions}; rollout-*.jsonl and .jsonl.zst | Current token_usage_record rows are de-duplicated by response id; legacy cumulative counters remain supported. Usage counts only when the current auth.json proves ChatGPT login. Also carries rate limits and context-window size |
| dsh – DeepSeek Harness | $DSH_HOME/sessions or ~/.dsh/sessions; nested session.jsonl and session.jsonl.zstd | Folds final and failed-attempt usage samples without duplication. By default it requires exact openai-codex model provenance plus current OAuth route metadata. Input/cache buckets are combined once; output already includes reasoning. Concatenated independent Zstandard frames are decoded one by one |
| pi – pi coding agent | $PI_CODING_AGENT_DIR or ~/.pi/agent/sessions/<project>/*.jsonl | Counts assistant usage whose provider/API identifies Codex OAuth; API-key providers require trackAllProviders for local parsing |
| omp – oh-my-pi | ~/.omp/agent/sessions, profiles, $XDG_DATA_HOME/omp/sessions, and $PI_CODING_AGENT_SESSION_DIR; $PI_CONFIG_DIR replaces ~/.omp | pi-compatible assistant messages plus current model_usage / reasoningTokens records, tagged omp. A shared $PI_CODING_AGENT_DIR is scanned once and tagged pi |
| cline – Cline | $CLINE_SESSION_DATA_DIR, otherwise $CLINE_DATA_DIR/sessions, $CLINE_DIR/data/sessions or ~/.cline/data/sessions; current/legacy data/tasks; VS Code-family globalStorage | Parses current v1 *.messages.json assistant usage. Default filtering requires exact modelInfo.provider === "openai-codex"; openai-codex-cli is always excluded to avoid duplicating native Codex rollouts |
| kilo – Kilo Code | $KILO_DB; otherwise %LOCALAPPDATA%\kilo (Windows), ~/Library/Application Support/kilo (macOS), or $XDG_DATA_HOME/kilo / ~/.local/share/kilo (Linux), with auth at that data root's auth.json; legacy VS Code-family globalStorage | Parses current SQLite assistant messages. providerID: openai counts only when the current OpenAI auth type is OAuth; legacy Cline-format tasks remain supported |
| hermes – Hermes Agent | $HERMES_HOME or <user-home>/.hermes on every OS (including %USERPROFILE%\.hermes on Windows and Windows homes visible under /mnt/c/Users from WSL), including profiles; state.db and legacy sessions/**/*.json\|jsonl | Reads current session_model_usage rows whose billing_provider is Codex OAuth, including cache-write/reasoning/request totals; legacy usage objects remain supported |
| openclaw – OpenClaw | $OPENCLAW_STATE_DIR, ~/.openclaw or ~/.clawdbot; each agents/<id> runtime's SQLite DB, managed codex-home rollouts, or legacy sessions | Current transcript DB records and attributable legacy records are tagged openclaw. Managed rollouts are discoverable only for local diagnostics because their in-memory OAuth token is not persisted alongside them |
| opencode – OpenCode | $XDG_DATA_HOME/opencode or ~/.local/share/opencode/storage/ (Windows: %LOCALAPPDATA%\opencode, %APPDATA%\opencode) | Retained best-effort reader: an openai message counts when auth.json identifies OAuth |
| roo – Roo Code | VS Code-family <globalStorage>/rooveterinaryinc.roo-cline/tasks | Retained best-effort legacy Cline-format reader |
| custom | extraSessionDirs | {"path": "~/.myagent/logs", "agent": "myagent", "format": "generic"} (formats: codex, dsh, pi, generic, opencode, cline) |
All available sources are on by default. Turn one off with codex-tracker config set sources.openclaw false (or
config set sources '{"pi":false}'); codex-tracker paths shows which roots were found. On Windows the WSL
distros' homes are scanned too, and inside WSL the Windows user profiles under /mnt/c/Users.
The current formats for Codex, pi, oh-my-pi, Cline, Kilo, Hermes, OpenClaw and DeepSeek Harness were audited against their
public implementations. OpenCode, Roo and older layouts remain best-effort compatibility readers. If usage
is missing, please open an issue with one anonymised usage-only sample and the output of codex-tracker paths.
Other audited agents
| Agent | Decision |
|---|---|
| Claude Code | Its public first-party login is for Anthropic services; it has no Codex / ChatGPT OAuth provider |
| Zazen (Freebuff fork) | No separate source: Freebuff Desktop can run a locally installed Codex with the existing provider account, whose native rollouts are already tracked as codex. Freebuff exposes no distinct durable OAuth attribution for a zazen tag |
Source limitations
- SQLite runtime: current Kilo, Hermes and OpenClaw databases are read through built-in
node:sqlite, available from Node 22.5 and in the current Electron runtime. A Node 16/20 headless process skips those databases and uses any attributable legacy JSON/JSONL or VS Code task fallback; SQLite-only history is unavailable there. - Codex authentication: native rollouts count only when the current
$CODEX_HOME/auth.jsoncontains explicitauth_mode: "chatgpt"or a structurally valid legacy OAuth token bundle. A keyring-only or ephemeral login leaves no readable proof in the rollout, so it is deliberately excluded. Since rollouts also lack a per-request auth marker, switching the current Codex login between ChatGPT and API-key auth reclassifies historical rollouts on the next scan. Tokens are never retained, logged or uploaded. - Kilo attribution: only the current
auth.jsonentry'stypediscriminator is used; credential values are not retained, logged, or uploaded. SQLite message rows do not record which auth method served each historical request, so switching OpenAI between OAuth and API-key auth can reclassify olderproviderID: openairows on the next sync. - DeepSeek Harness attribution: local
.credentials.yamlandsettings.yamlare parsed, but the tracker uses only theopenai-codexrecord kind and whether anapiKeyEnvoverride exists; credential values are not retained, logged or uploaded. Session events do not preserve each request's auth method, so current route configuration can reclassify historical usage. For an alternate session root, selectformat: "dsh"; its attribution still uses the current sidecars under$DSH_HOMEor~/.dsh. - Hermes time buckets:
session_model_usageis an aggregate, not an hourly event stream. Its usage is assigned to the row'slast_seenhour. Session/model totals are retained, but hourly charts are approximate. - Compressed sessions: Codex/OpenClaw
.jsonl.zstand DeepSeek Harness.jsonl.zstdprefer native Zstandard when available and otherwise use the bundled decoder, so compressed history works across supported runtimes. Plain.jsonlremains supported. - DeepSeek Harness privacy: session records are JSON-decoded locally to obtain usage and model provenance. Prompt and response content is never retained, logged or uploaded by the tracker.
- OpenClaw managed harness: the supported transcript DB contains the exact Codex-OAuth route marker. The managed harness instead injects
chatgptAuthTokensin memory and deliberately does not write a managedCODEX_HOME/auth.json; its rollout files can be parsed only withtrackAllProvidersfor diagnostics and are always removed from priced/uploaded OAuth totals.
Rate limits
The popover and codex-tracker status show your account's rate-limit windows (e.g. weekly / 5-hour) live from
https://chatgpt.com/backend-api/wham/usage, the same endpoint the official Codex client uses. The request is
made with the access token from your local Codex login (~/.codex/auth.json); the token is read fresh each time,
never written, never refreshed by the tracker, and is sent only to chatgpt.com – never to the dashboard.
Because every Codex-subscription consumer (pi, oh-my-pi, Cline, Kilo, Hermes, OpenClaw, DeepSeek Harness, …) draws from the same account, this is the only
accurate number; the values inside Codex logs are just snapshots from Codex's own last request.
- Refreshed every
usageRefreshSec(default 60 s) and ~10 s after new local usage is seen. - If the request fails (offline, expired token, API-key login) the card falls back to the latest values from Codex logs and is labelled From logs · as of with the reason.
- Disable with
codex-tracker config set liveRateLimits false.
What gets uploaded
Only aggregates: token counts per UTC hour and model, per-session totals, the model name, the project folder name and a SHA-256 of its path, plus a heartbeat (tokens/s, today's totals) that carries a SHA-256 of the machine's hardware id (so one computer maps to one device however often it logs in). Prompts, code, file paths, session contents and the raw hardware id never leave your machine. Timestamps are stored in UTC; the app and dashboard display them in your local time zone.
Development
pnpm install # from the repo root
pnpm --filter codex-token-tracker build
node packages/menubar/bin/codex-tracker.js status
CODEX_TRACKER_DEBUG=1 node packages/menubar/bin/codex-tracker.js menubar # verbose logs
CODEX_TRACKER_DEVTOOLS=1 ... # open DevTools
pnpm --filter codex-token-tracker build:icons # regenerate tray iconsDev builds vs published builds
A build knows which environment it belongs to, so a local test run can never write into production.
scripts/build.mjs stamps the channel at bundle time: only --release produces a prod build, which
is what prepack runs — so every tarball and every npm publish is prod, and every pnpm build,
pnpm dev and watch-mode rebuild is dev.
| | dev build (pnpm build) | published build (npx codex-token-tracker / npm i -g) |
| --- | --- | --- |
| Dashboard default | http://localhost:3000 | https://codex.chenli.dev |
| Convex deployment | whatever the local dashboard's /api/config advertises — the dev one | production |
| Config, device token, upload state | ~/.codex-tracker-dev | ~/.codex-tracker |
| checkUpdates default | false (update refuses to run) | true |
| App name / Electron userData | Codex Tracker (dev) | Codex Tracker |
| macOS LaunchAgent | dev.codex-tracker.menubar.dev | dev.codex-tracker.menubar |
| Popover | orange DEV badge in the header | — |
Because the two differ in app name and config directory, a dev build and an installed one can run at the same time, each with its own tray icon, device token and upload state.
To test against a dev environment, start the dashboard and Convex, then run the local build:
cd apps/dashboard && npx convex dev # terminal 1
pnpm dev # terminal 2 — http://localhost:3000
pnpm --filter codex-token-tracker build # terminal 3
node packages/menubar/bin/codex-tracker.js login # → localhost:3000, no --dashboard needed
node packages/menubar/bin/codex-tracker.js menubarBoth defaults are only defaults: --dashboard <url> and config set dashboardUrl <url> still point
either build anywhere, and CODEX_TRACKER_HOME still overrides the config directory.
Publishing (from the repo root): pnpm release:menubar — prepublishOnly typechecks and prepack
makes the release build, so a published tarball is never accidentally a dev build. postpack restores
the dev build in dist/ afterwards, so your working copy keeps pointing at localhost.
License
MIT
Troubleshooting
- "Electron download failed" / tray app does not start – the CLI downloads the Electron runtime on first launch from GitHub releases into
~/.codex-tracker/electron/<version>/. Behind a firewall setELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/(orHTTPS_PROXY) and runcodex-tracker menubaragain; a half-finished download can be cleared by deleting that directory. A manually installednpm i -g electronis also picked up. The headlesscodex-tracker agentandcodex-tracker statuswork without Electron. - Nothing is tracked – run
codex-tracker pathsto see which enabled sources were found. Confirm that your agent has written one of the supported stores above, or add a compatible location withcodex-tracker config set extraSessionDirs '["/path/to/sessions"]'. - Uploads fail with BAD_TOKEN – the device was revoked in the dashboard; run
codex-tracker loginagain. - No browser on this machine (WSL2, a server, SSH) –
codex-tracker loginprints the approval link and a QR code; scan it with your phone or open the link on any computer where you can sign in.--qrforces the QR code on a desktop too. - This machine shows up twice on the dashboard – it logged in twice with a version before 0.3.0. Restart the tracker (tray app or agent) on 0.3.0; within a few minutes its first heartbeats merge the two entries, and the Devices page shows one device with a 2 logins badge.
