← docs
docs · v0.1

Token Dashboard — Local Telemetry for AI Spend

v0.1 · last updated 2026-07draft

DRAFT — pending Harsh's verification.

This page documents a local telemetry dashboard Harsh built to track his own AI usage across the coding assistants he actually runs. It is a side tool, not part of Memex or any employer system — a personal observability surface, run entirely on his laptop. The design is interesting precisely because the problem is widespread and almost nobody instruments it: AI spend is real money and real context, but each assistant reports it in a different place, in a different shape, with different fidelity, and most users have no unified view of it at all.

1. The problem

A working engineer in 2026 doesn't use one AI assistant; they use a stack: a terminal coding agent (Codex), a different terminal coding agent (Claude Code), an in-editor assistant (GitHub Copilot), plus chat products for planning and research (Claude chat, ChatGPT). Each one bills (or burns context) independently. The result is a classic observability gap:

This is the same shape as every metrics problem Harsh has spent his career on (see Build Failure Triage and Release Engineering): make the thing visible first, then behavior follows. The dashboard exists to make AI spend visible.

2. The design: one local board over a scrubbed daily-burn file

The system is two pieces, and the split is deliberate:

  1. An importer (npm run import:daily) that reads local logs from each source, normalizes them, and writes a single static file: data/daily-burn.json.
  2. A Next.js dashboard that reads that static file and renders it — totals by source, a heat calendar, weekly trend, a driver breakdown, and a "what to do next" nudge.
flowchart LR
  subgraph logs["local logs · exact where counted"]
    codex[Codex telemetry db]
    claude[Claude Code sessions]
    copilot[VS Code Copilot logs]
  end
  importer["import:daily\nscrubbed · estimates where not"]
  board["daily-burn.json\ncounts only · static read"]
  dashboard[Next.js dashboard]

  codex --> importer
  claude --> importer
  copilot --> importer
  importer --> board
  board --> dashboard

The central design decision is a fidelity ladder: use exact local telemetry where the logs expose real token counters, and fall back to labeled estimates derived from request counts where they don't — never guessing a number and labeling it exact. Chat products, which expose nothing local, are written as 0 and intentionally excluded, rather than fabricated. The row carries an evidence note on every line so a reader always knows which is which.

The normalized daily row (as documented in the dashboard README) is:

{
  "date": "YYYY-MM-DD",
  "codex_tokens": 0,
  "claude_code_tokens": 0,
  "claude_code_calls": 0,
  "github_copilot_est": 0,
  "github_copilot_requests": 0,
  "claude_chat_est": 0,
  "chatgpt_est": 0,
  "total": 0,
  "driver": "planning",
  "evidence": "Codex exact local telemetry and Claude Code exact local usage"
}

total is the sum of the token fields. The two _est chat fields are structural placeholders — present so the schema stays stable when those sources are eventually wired, but always zero today.

Source fidelity

FieldFidelityWhy
codex_tokensexactread from local Codex completed-response telemetry
claude_code_tokensexactread from local Claude Code session logs
claude_code_callsexactcount of assistant responses with usage, from the same logs
github_copilot_estestimatedVS Code Copilot logs expose request events, not token counters
github_copilot_requestsexact countsuccessful Copilot request events found in the logs
claude_chat_estexcludednothing local to read; written as 0
chatgpt_estexcludednothing local to read; written as 0

Reusable pattern: when sources have different fidelity, build a fidelity ladder — exact where you can, conservatively estimated where you must, explicitly excluded where you can't — and stamp each row with an evidence note. A number labeled "estimate" is useful; a guess labeled "exact" is a lie that breaks trust in the whole board.

3. Per-source importers as separate modules feeding one row

The importer is one script with three per-source modules — importCodex, importClaudeCode, importGithubCopilot — each responsible for a single source. They don't know about each other. They all write into one shared date → row map, lazily creating a row for any date they touch; a row that ends up with a zero total is dropped at the end.

flowchart LR
  codex[importCodex] --> rows["shared rowsByDate map\ndate -> codex_tokens,\nclaude_code_tokens,\ngithub_copilot_est,\ndriverTokens, …"]
  claude[importClaudeCode] --> rows
  copilot[importGithubCopilot] --> rows
  getRow["getRow(date) lazily creates the row\ntotal == 0 dropped at write time"] --> rows

The split keeps each source's parsing logic isolated, so adding a fourth source is "write a new importer that calls getRow(date)," not "refactor a monolith." Each module is also free to be as simple or as defensive as its source needs:

Copilot estimation weights

The Copilot weights are design constants documented in the dashboard README, not measured usage. They are the importer's stated conservative per-request token estimates:

Request kindEstimated tokens
agent / edit request20,000
conversation summary8,000
search / subagent6,000
small wrapper1,500
inline completion300
other successful request4,000

A wrinkle the README calls out explicitly: newer VS Code Copilot logs sometimes expose only markdown request records with no success/channel line. In that case the importer counts one fallback request per minute after the last day it saw a real success event, weighting each at the 4,000 "other" figure and keeping it labeled as an estimate. The dashboard never presents these as exact.

4. Pacific-day bucketing

Every timestamp is bucketed to a Pacific calendar day (America/Los_Angeles) before it enters a row. This is not a manual UTC offset — the importer formats each timestamp through the timezone-aware Intl.DateTimeFormat so daylight saving is handled by the platform tz database, not by hand. All three importers route through the same localDate helper, so a Codex epoch-seconds event and a Claude Code ISO timestamp land in the same day bucket consistently.

Why a local day and not UTC? Because the question the dashboard answers is "What did I burn on my Tuesday?" — and a Tuesday defined by a 3am Pacific boundary is the wrong answer for someone whose workday is Pacific. Bucketing to the operator's local calendar day is the choice that matches the question the board is meant to answer.

Reusable pattern: *bucket telemetry to the calendar day of the person *who reads the board, using the platform timezone database — never a hardcoded *UTC offset. Off-by-one days from naive UTC bucketing are the most common bug in any "by day" dashboard.

5. Privacy stance: everything local, counts only

The dashboard's trust posture is the same one this whole site is built around (see the PRD's agent trust rules) — except here it's applied to the operator's own data. The rule is simple: content never leaves the machine; only counts do.

Concretely, the importer reads local files and writes a row that contains:

It deliberately does not write raw prompts, message content, file paths, client names, request IDs, or session titles. The driver is derived from local metadata and then the metadata is discarded — so the row says driver: shipping without ever recording which session or which file. The README's deploy instruction makes this a gate, not a suggestion: deploy only after confirming daily-burn.json is scrubbed.

The chat-product fields are the strongest expression of this stance. Claude chat and ChatGPT have no local log the importer can read, so rather than guess, the importer writes 0 and marks them intentionally excluded. The board undercounts total spend on purpose before it would invent a number.

6. The DORA-dashboards connection

This dashboard is not a one-off. It is the same instinct that produced Harsh's release-engineering metrics work — and it is worth naming the through-line because it is the actual point.

DORA-style metrics (deployment frequency, lead time, change failure rate, mean time to restore) are valuable not because the numbers themselves are interesting but because putting them on a board changes how the org behaves. The Release Engineering arc — moving from bi-weekly to daily releases at a sub-1% rollback rate across a large multi-repo footprint — only became tractable once delivery became legible: visible on a dashboard, instead of folklore in someone's head.

The token dashboard is the same move, applied to a personal cost surface:

DORA made delivery legible and shifted how teams ship. This board makes AI spend legible, by source and by driver, with the same goal: behavior change driven by a visible signal.

7. What it demonstrates

Honest gaps

A few things this page does not claim:

Sources

This page was distilled from the token-dashboard project's own documentation and source — primarily the dashboard's README (which documents the row schema, the source-fidelity table, the Copilot estimation weights, the Pacific-day bucketing, and the driver-classification rules) and the importer script and dashboard app, read for architecture only. No source code, no real usage numbers, and no local file paths from the operator's machine appear here; the design is described, not copied, and every architectural claim above traces to that material.