Local-first Claude Code usage & cost tracker.
A Hono server ingests OTLP HTTP/JSON metrics & logs (and JSONL transcripts) into SQLite; a React + Vite dashboard renders cost, token, session, person, and model breakdowns with live SSE updates. Everything runs on your machine — no data leaves your laptop.
The fastest way to get running locally is the finius CLI. You need Node 22.5+ (Finius uses the
built-in node:sqlite module; developed on Node 24).
npx @cliftonc/finius # first run: installs finius globally, then walks you through setup
finius serve # start the server + dashboard at http://localhost:8787
finius import all # optional: import old Claude Code + Codex sessionsThen launch Claude Code in a new terminal and start coding — the dashboard at http://localhost:8787 updates live as telemetry arrives.
That's it. Three things just happened:
npx @cliftonc/finiusinstalledfiniusglobally and ranfinius setup, which saved~/.finius/config.jsonand — with your consent — edited~/.claude/settings.jsonto add the OTLP env vars plus aSessionEnd+PreCompacthook (finius hook) that uploads each session transcript.finius servestarted a single process exposing the API and the dashboard on one port. Its data lives under~/.finius(override withFINIUS_DB_PATH/FINIUS_BLOB_DIR).- Any Claude Code session you run now reports usage to that local server. If you ran
finius import all, Finius also backfilled historical Claude Code and Codex transcripts already on disk. Usefinius import claudeorfinius import codexto import only one agent.
Re-run finius setup any time to reconfigure, or finius doctor to diagnose telemetry that isn't
arriving.
Prefer to hack on Finius itself? Clone the repo and run the dev servers.
npm install
npm run devnpm run dev runs the API and UI together (via concurrently):
- UI → http://localhost:5173 (Vite dev server; proxies
/api,/otlp,/eventsto the API) - API → http://localhost:8787
Run them separately if you prefer: npm run dev:server (API only) or npm run dev:client (UI only).
In the shell where you launch Claude Code, use the bundled helper — it checks the server is up,
exports the OTLP env vars, then runs claude:
./scripts/run-claude.sh # forwards any extra args to `claude`Or export the variables manually:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:8787/otlp/v1/metrics
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://localhost:8787/otlp/v1/logs
claudeRun a Claude Code session and the dashboard updates live (SSE) as telemetry arrives.
npm run build # tsc -> dist + vite build -> dist/client
npm start # node dist/server/index.js, serves the built UI from dist/client on :8787When a build exists, npm start serves the UI and API from the single port http://localhost:8787.
- Home — KPIs (cost, tokens, cache, lines, edits, sessions, people), tokens/cost/lines/edits charts over time, plus Models / Users / Sources breakdowns.
- Sessions, People, Models — lists ranked by recency / cost. Click any row (or any Home breakdown row) to drill into the Home view filtered to that session, person, or model.
- Filters — time range, source, user, and model selects apply everywhere. All view and filter state lives in the URL query string, so any view is shareable/bookmarkable and back/forward works.
Besides live OTLP telemetry, you can backfill from JSONL transcripts:
POST /api/import/jsonl— body{ content, source?, sessionId? }(or raw JSONL text).POST /api/import/claude-hook— body{ transcript_path, session_id?, cwd? }; reads a local Claude Code transcript file (restricted to~/.claude/projectsor the givencwd).
Imports are idempotent — re-sending the same file (matched by content hash) is detected and skipped.
The original transcript is stored as a file and can be viewed from the session drill-down
(GET /api/sessions/:id/transcript).
Data is stored in data/finius.sqlite by default. Override with FINIUS_DB_PATH=/path/to/db.sqlite.
Override the API port with PORT. Imported transcript files live under <db-dir>/transcripts
(override with FINIUS_BLOB_DIR).
Dashboard reads are served from a pre-aggregated hourly metric_rollup; raw metric_points keep
the full-resolution data for the live view and drill-downs.
The full OTLP payload of each ingest batch is kept in raw_batches only to allow replaying history
into new metric classifications. Control it with:
-
FINIUS_RAW_PAYLOADS=retain(default) |off—offkeeps only the dedup hash, not the payload. -
FINIUS_RAW_RETENTION_DAYS=7(default) — age cutoff used by the prune endpoint below. -
FINIUS_CRON_TOKEN=<secret>— enablesPOST /api/maintenance/prune-raw-batches. Without it the endpoint is disabled (returns 503). Wire a cron to it:curl -fsS -X POST -H "Authorization: Bearer $FINIUS_CRON_TOKEN" \ http://127.0.0.1:8787/api/maintenance/prune-raw-batches
npm test # vitest run
npm run typecheck # tsc --noEmit (strict)