How a token counted on a laptop becomes a percentage on the dashboard, and which file owns each hop.
Claude Code / Desktop / Pi
└─ ~/.claude/projects/**/*.jsonl one line per streamed segment
└─ cli: scanner → tailer → parser read from stored byte offset
└─ POST /api/v1/usage bearer device token, ≤1000 records
└─ usage_event rows deduped on (userId, uuid)
└─ cli: claude-limits oauth/usage (sub, free) or 1-token ping (api)
└─ POST /api/v1/limits Anthropic's OWN utilization numbers
└─ claude_account + limit_sample keyed by the local login
dashboard: per account: fold rows → cost share per group → split the official pct
Two independent legs. The usage leg is ours (tokens, models, cost estimates); the limits leg is Anthropic's own truth about how full the account is. The dashboard's headline numbers come from the limits leg — the usage leg only decides whose usage filled it.
Everything downstream of the limits leg is per Anthropic account: a fleet split over two Claude subscriptions gets two independent budgets, and each device counts against whichever one it is signed into.
TanStack Start (SSR + server functions), React 19, Tailwind v4, shadcn, Drizzle
- Postgres, better-auth (email/password, Google, GitHub) with its Stripe plugin.
Routing is file-based under src/routes:
_dash.tsx— authenticated layout; its loader's server fn callsrequireUser()(redirecting to/login) to guard the shell. Every child route's own server fns still callrequireUser()themselves — loaders run independently, so the layout guard is UX, not the security boundary._dash/{dashboard,devices,groups,settings,billing,admin}.tsx— the app.admin.tsxre-checksrequireAdmin()server-side.api/v1/{usage,limits}.ts— the collector's ingest API (device-token auth).api/dashboard.ts— the browser's live poll; returnstoDashboardDTO(...).api/auth/$.ts— better-auth handler.index.tsx,login.tsx,reset-password.tsx,privacy.tsx,terms.tsx,robots[.]txt.ts,sitemap[.]xml.ts— public surface.
Data access is layered so pages stay thin:
| Layer | File | Job |
|---|---|---|
| session | lib/session.ts |
requireUser() / requireAdmin() (env ADMIN_EMAILS) |
| reads | lib/data.ts |
every dashboard/history query + the group split |
| writes | lib/actions.ts |
createServerFn mutations (groups, devices, settings) |
| math | lib/usage/* |
pure, unit-tested — see usage-math.md |
| money | lib/billing.ts, lib/plans.ts, lib/stripe-prices.ts |
entitlement |
| guards | lib/rate-limit.ts, lib/device-auth.ts, lib/device-token.ts |
trust boundary |
lib/promise-cache.ts collapses concurrent identical loads (in-process only).
lib/flags.ts gates optional features by env presence (Stripe, OAuth).
user/session/account/verification/subscription— better-auth generated (db/auth-schema.ts), regenerate rather than hand-edit.groups— a named bucket of devices, owned by a user, with the twoblock_on_*_limittoggles the prompt guard enforces.devices— one collector install.token_hash(SHA-256, unique) + a display prefix,revoked,last_seen_at,collector_version, optionalgroup_id, andclaude_account_id(stamped from its limits posts, null until the first).claude_account— one Anthropic subscription the fleet draws on, keyed(user_id, ext_id)withext_id= theoauthAccount.accountUuidthe collector reads locally.ext_id IS NULLis the bucket for devices whose login can't be identified (API-key setups, collectors older than multi-account);NULLS NOT DISTINCTkeeps it to one per user. Holds the latest reported utilization (five_hour_pct,seven_day_pct, resets,model_limitsjsonb) — Anthropic meters each subscription separately, so this is per account and not per user.calibrationjsonb holds this account's fitted token-bucket weights and meter lag when its own history beats list prices on held-out data, and null when it does not (seeusage-math.md).usage_event— one raw JSONL segment. Unique on(user_id, uuid); indexed on(user_id, ts)and(device_id, ts). Never aggregate without folding.user_settings— plan preset, token limits, week reset weekday/hour (a grid fallback for the past-windows card only — the live weekly window anchors on Anthropic's reported reset), cache TTL, admin free-device grant. Its*_pct/model_limitscolumns are dead as of migration 0017, which moved them toclaude_account.limit_sample— peak utilization per(claude_account, window, window_start). Claude only reports the open window, so this is the only record of how full a closed one got; the past-windows card reads it instead of guessing.usage_eventtimestamps are stamped by the reporting machine, so ingest shifts each batch onto server time before any window or attribution decision, using the minimumreceivedAt - sentAtover an hour-long window held ondevices.clock_offset_ms/clock_offset_at(lib/usage/clock.ts).limit_change_point— one row per reading that raised a window's official pct, per(claude_account, window, model, at)—modelis''for a whole window and the model id for a per-model limit. Nothing is thinned on write: this is the full-resolution log, and readings closer together than one poll cycle are merged at read time instead (mergePoints), so the fit and the split can walk the same series. Pruned past the longest window on write. The live group split attributes each rise to the groups active in its interval (delta attribution — seeusage-math.md). Identified accounts only.
All three endpoints: x-api-key or Authorization: Bearer <device token> →
authenticateDevice() (hash lookup, revoked check) → plan re-check. The two
POSTs then read an arktype-validated body through readJsonCapped; the GET has
no body. A device outside the plan gets 402 from all three, which the guard
treats as open like any other non-OK. last_seen_at is touched by the two POST
handlers, and on the 402 path so a parked device still shows as alive.
POST /api/v1/usage—{ records: [...], sentAt }, ≤1000 per batch. Responds with accepted/duplicate/skipped counts.sentAtis the collector's own clock at send time and is load-bearing: the server derives the device's clock offset from it (see theusage_eventnote above), so it must keep being stamped per batch. At-least-once by design: the collector only commits a file offset after the server acknowledges, and the unique index absorbs the replay. Records older thandevices.created_atare dropped (skipped): a fresh collector tails the machine's whole log history, and usage from before the token existed is not this device's to report. Records dated in the future are clamped to arrival time, not dropped — a machine with a fast clock would otherwise inflate month and all-time spend permanently, and rejecting them instead would be silent data loss, since the collector commits its file offsets on any 200 and never readsskipped. Clamped rows are counted back asclamped, because nothing else in the response would reveal a machine whose clock is wrong. Each token field must be an integer 0..500,000,000 — the columns are int4, and a record outside that is a400the collector bisects down to and drops, so it is the one wire bound a third-party client has to know.osis also accepted asother(freebsd/sunos/…); the column is a display-only enum, so an unlabelled box keeps whatever it had rather than forcing a migration.POST /api/v1/limits— the parsed limits reading (oauth/usage for subscriptions, rate-limit headers for API keys), plus the optionalaccountthe collector read from~/.claude.json. Upsertsclaude_account, stamps the device with it, upserts the peak intolimit_sample, and — unless the account is the unidentifiedext_id = NULLbucket — appends alimit_change_pointper window whose pct rose.GET /api/v1/limits— whatusagefleet guardasks before a prompt. Returns{ group, sessionPct, weeklyPct, blocked, blockedWindow, blockedUntil, reportedAt }; the guard readsblockedto decide andblockedWindow/blockedUntilto word the refusal.blockedis true only when the device's own blocking toggle is on (devices page, per machine), its group has the matching switch on, that window is at 100% of the group's budget slice, and the last reported utilization is younger thanLIMITS_STALE_MS(15 min). A stale reading never blocks: the limits leg can die while upload keeps working, and nothing decays the stored percentage.
Status codes carry meaning to the CLI: 401 revoked/unknown token (stop),
402 device outside the plan's device limit (park, keep data), 400
malformed records (the collector splits the chunk and retries), 429 backoff.
Sessions are better-auth cookies. Device tokens are a separate, long-lived
credential — issued once in /devices, shown once, stored hashed.
accountPlan(userId) in lib/billing.ts is the single source of device caps:
the newest subscription row in ENTITLING_STATUSES (active, trialing,
past_due — a failed charge shouldn't yank a fleet mid-dunning), else free.
planDevices() maps plan + Stripe seat quantity to a number; only custom
varies with seats. Prices live in Stripe, never in the catalog.
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db for
Postgres on localhost:5432, then bun run dev. Point DATABASE_URL at that
local instance: a .env aimed at the deployed database makes every local signup
and every agent-driven experiment a production write.
Schema changes are always a committed migration: bun run db:generate, then
bun run db:migrate locally (the container runs it at boot). Commands live in
package.json; env in .env.example.