Analytics dashboard for Claude Code that aggregates usage across multiple machines into a single view. Track sessions, token costs, model usage, active coding time, and more.
A lightweight client runs on each machine (via cron or launchd), pushing session events to a central server. The server deduplicates, stores everything in SQLite, and serves a Bauhaus-styled dashboard.
- Aggregate token data from every device into one dashboard
- Captures Claude Desktop session titles, MCP toggle state, and lifecycle metadata (macOS only)
- Per-model, per-day, per-project cost breakdown with live LiteLLM pricing
- Active time tracking: thinking time, tool execution, main sessions, subagents
- Per-machine stats for prompts, tokens, and costs
- Weekly budget utilization gauge via Anthropic's OAuth API
- Personal provider limit groups for Claude, Codex, OpenCode Go, and observed OpenCode Zen spend
- 24-hour hourly activity heatmap
- Stdlib-only Python client, no pip install needed
- Message content is stripped before sending; only metadata and token counts are transmitted
git clone https://github.com/jaedync/tokenfold.git
cd tokenfold
cp .env.example .env
# Edit .env - set STATS_API_KEY to a random secret
docker compose up -dThe dashboard is now at http://localhost:5000.
Copy client/claude-stats-push.py to each machine you use Claude Code on.
Linux (cron):
# Copy the script
cp client/claude-stats-push.py ~/tokenfold-push.py
# Add to crontab (runs every 5 minutes)
(crontab -l 2>/dev/null; echo "*/5 * * * * TOKENFOLD_URL=https://your-server.example.com TOKENFOLD_API_KEY=your-secret python3 ~/tokenfold-push.py") | crontab -macOS (launchd — recommended):
macOS note: always use launchd, not cron. macOS
cronruns with a minimal PATH that resolvespython3to Apple’s stock/usr/bin/python3, which is Python 3.9 on current macOS versions. The script is written for Python 3.9+ viafrom __future__ import annotations, so stock Python works fine — but launchd is the conventional macOS user-agent runner and survives reboots cleanly.
# Copy the script
cp client/claude-stats-push.py /usr/local/bin/tokenfold-push.py
# Copy and edit the plist
cp client/com.tokenfold.push.plist ~/Library/LaunchAgents/
# Edit ~/Library/LaunchAgents/com.tokenfold.push.plist: set TOKENFOLD_URL and TOKENFOLD_API_KEY
# Load (starts immediately since RunAtLoad=true)
launchctl load ~/Library/LaunchAgents/com.tokenfold.push.plist
# Tail the log to verify
tail -f /tmp/tokenfold-push.log| Variable | Default | Description |
|---|---|---|
TOKENFOLD_URL |
(required) | Server URL (e.g. https://your-server.example.com) |
TOKENFOLD_API_KEY |
(required) | API key matching the server's STATS_API_KEY |
TOKENFOLD_MACHINE |
hostname | Display name for this machine |
TOKENFOLD_VERBOSE |
0 |
Set to 1 for debug logging to stderr |
TOKENFOLD_CURSOR |
~/.tokenfold-cursor.json |
Cursor file path (tracks sync progress) |
Legacy CLAUDE_STATS_* env vars are still supported as fallbacks.
| Variable | Default | Description |
|---|---|---|
STATS_API_KEY |
(required) | API key for authenticating client pushes |
TOKENFOLD_READ_API_KEY |
(empty) | Optional read-only key for GET /api/ha only (display hosts). Every write route refuses it. Empty disables it |
TZ |
America/Chicago |
Timezone for daily bucketing |
DB_PATH |
/app/data/tokenfold.db |
SQLite database path |
STATS_OWNER |
(empty) | Display name shown on the dashboard |
NOTIFY_TOKEN |
(empty) | Auth token for /api/notify (falls back to STATS_API_KEY) |
HA_URL |
(empty) | Home Assistant URL (e.g. http://homeassistant.local:8123) |
HA_TOKEN |
(empty) | Home Assistant long-lived access token |
HA_DEVICES |
(empty) | Comma-separated HA notify targets (e.g. mobile_app_phone) |
If a Meridian proxy with a Claude Pro or Max login runs on the server host, the quota-collector service keeps Claude quota current at all times. Without it, quota arrives only while a Pi session reports it.
# .env
COMPOSE_PROFILES=meridian
docker compose up -d --build
docker logs tokenfold-quota-collector # logs only health changesThe service polls MERIDIAN_URL (loopback only, default http://127.0.0.1:3456) every 120 seconds and posts to POST /api/usage/claude with STATS_API_KEY. Anthropic rate-limits the OAuth usage endpoint for each token. A 60-second poll got HTTP 429 on every second request, so 120 seconds gives the same freshness without rejected requests. The service uses host networking to reach Meridian on loopback, publishes no port, and runs as an unprivileged user on a read-only filesystem. COLLECTOR_MACHINE (default: the host name) labels the reports. COLLECTOR_INTERVAL_S (30 to 3600) changes the cadence. TOKENFOLD_URL must use https unless it is a loopback address.
Tokenfold includes an optional notification relay that forwards Claude Code hook events to Home Assistant as mobile push notifications. Get notified when Claude finishes a response, needs permission, or asks a question.
Set these environment variables (or add to .env):
NOTIFY_TOKEN=a-random-secret # Auth token for hook scripts
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your-long-lived-access-token
HA_DEVICES=mobile_app_phone1,mobile_app_phone2If these are left empty, the /api/notify endpoint still works but silently skips the HA relay.
On each machine you want notifications from:
# Save your Tokenfold server URL and notify token
echo "https://your-tokenfold-server.example.com" > ~/.config/notify-relay-url
echo "your-notify-token" > ~/.config/notify-relay-token
# Copy the hook script
cp client/notify-relay.sh ~/notify-relay.shThen add to ~/.claude/settings.json:
{
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "command": "bash ~/notify-relay.sh", "timeout": 10 }] }],
"Notification": [{ "hooks": [{ "type": "command", "command": "bash ~/notify-relay.sh", "timeout": 10 }] }]
}
}# Copy the hook script
cp client/codex-notify-relay.sh ~/codex-notify-relay.shAdd to ~/.codex/config.toml:
notify = ["bash", "~/codex-notify-relay.sh"]Claude Code CLI -> ~/.claude/projects/**/*.jsonl
|
client/claude-stats-push.py (cron, every 5min)
|
POST /api/ingest (X-API-Key auth, Claude JSONL batches)
POST /api/ingest/pi (X-API-Key auth, typed Pi normalized batches)
POST /api/provider-usage (X-API-Key auth, best-effort quota snapshots)
|
app/ingest.py (privacy-safe normalization and ID namespacing)
|
SQLite (WAL mode, events + tool_uses tables)
|
app/aggregator.py (cached rebuild on invalidation)
|
GET / (dashboard; GET /classic: the old one)
GET /api/stats (JSON API)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/ |
No | Dashboard: Runway, Ledger, History, Lab (/next is an alias) |
GET |
/classic |
No | The old dashboard |
GET |
/api/stats |
No | Full stats JSON |
GET |
/api/stats/version |
No | Cache version (for polling) |
POST |
/api/ingest |
X-API-Key |
Ingest Claude Code JSONL session events |
POST |
/api/ingest/pi |
X-API-Key |
Ingest typed, privacy-scrubbed Pi Agent events; provider-aware reported costs and required dotfleet work/personal account class |
POST |
/api/desktop-metadata |
X-API-Key |
Ingest Claude Desktop session metadata (macOS) |
POST |
/api/usage |
X-API-Key |
Push Anthropic OAuth usage/quota data (Claude-only; not Pi) |
POST |
/api/usage/claude |
X-API-Key |
Personal Claude quota observed through Meridian (Pi reporter and quota-collector) |
POST |
/api/provider-usage |
X-API-Key |
Merge best-effort Codex/OpenCode quota snapshots from Pi, filed under the reporter's account_class (work = enterprise scope, personal = personal scope) |
POST |
/api/notify |
Bearer |
Notification relay to Home Assistant |
GET |
/health |
No | Health check |
pip install -r requirements.txt
STATS_API_KEY=test uvicorn app.main:app --reload --host 127.0.0.1 --port 5000MIT
