Skip to main content
Glama
ddyy

tokenarchive

by ddyy
README.md
# tokenarchive

Daily snapshots of your AI coding usage, kept forever.

![The dashboard: per-device health, totals, and daily cost over time](docs/dashboard.png)

[ccusage](https://github.com/ryoppippi/ccusage) can tell you what Claude Code,
Codex, Gemini and friends cost you — but only as far back as the logs on your
machine go, and those logs rotate. tokenarchive snapshots the ccusage report
once a day, keeps every snapshot as an append-only JSON archive, and renders a
dashboard from it. Optionally, a Cloudflare Worker keeps a copy of the archive
off your machine, behind a passkey.

Two principles shape everything here:

- **Nothing is ever erased.** Snapshots are the archive; every database is a
  derived copy that can be rebuilt from them.
- **Absence is never reported as zero.** A day with no snapshot says so. $0.00
  would claim we looked and you spent nothing.

## Quickstart (macOS)

```sh
npx tokenarchive
```

The installer prompts for a device name and an optional Worker URL, puts the
runtime under `~/.local/share/tokenarchive/app`, schedules the daily job
(23:50 plus every login — the script is idempotent), lets `security` prompt
for the ingest token so it never crosses the installer's hands, and proves
the whole thing with one real run. Re-run it any time to reconfigure;
`--uninstall` removes everything except your archive.

Snapshots land in `~/.local/share/tokenarchive/{device}/{date}.json`. To try
it once without installing anything:

```sh
git clone https://github.com/ddyy/tokenarchive && cd tokenarchive && ./snapshot.sh
```

Other platforms: everything except the scheduling glue is portable. `sync.js`,
the importer and the dashboard are plain Node; port `snapshot.sh` to a systemd
timer or cron job and the rest follows.

## The dashboard

`generate-dashboard.js` builds a single self-contained HTML file — no
framework, no server, no external requests. It imports new snapshots into a
local SQLite read model first (`import-snapshots.js`), so regeneration is
incremental.

Every panel is a filter: click or drag across models, devices, agents, days,
or billing windows and the rest of the page follows (⌘ adds to a selection,
⇧ extends it). Filters compose across dimensions with AND, and live in the
URL fragment, so a view can be bookmarked.

<table>
<tr>
<td width="50%"><img src="docs/models.png" alt="Cost by model"></td>
<td width="50%"><img src="docs/projects.png" alt="Cost by project"></td>
</tr>
<tr>
<td><img src="docs/devices.png" alt="Cost by device"></td>
<td><img src="docs/blocks.png" alt="Recent billing windows"></td>
</tr>
</table>

A **Display as** switch — above the totals, and again in the sticky bar once
they scroll away — measures the whole page in dollars or in tokens: the
chart and its axis, the coverage shading, the billing windows, and every
list, which reorders to match. Hovering a row gives the full name it had to
truncate, its cost, tokens, the implied rate per million, its share of what
is on screen, and — for projects — the session count and the span they
cover:

![Row hover detail](docs/hover.png)

The coverage strip shows one cell per calendar day, so a gap is visible as a
gap — days with no snapshot are never drawn as zero-usage days:

![Coverage strip](docs/coverage.png)

## Multiple devices

The archive is partitioned by device from the filesystem up: snapshots live
in `{data}/{device}/{date}.json`, every database row carries the device, and
importing is upsert-only. Machines therefore never merge or clobber each
other, and anything that lands both machines' files under one data directory
gives you a combined archive — a Worker, a synced folder, or a copied
directory all work the same way.

Health is judged per device: each gets its own green/amber/red dot, its own
last-snapshot date, and its own missing-days list, so one current machine
cannot hide another that has gone quiet. A device that stops reporting keeps
all of its history — the archive never erases; it just shows the gap.

The Worker below is the way to do this without moving files by hand.

## The Worker (optional, recommended)

The `worker/` directory is a Cloudflare Worker that accepts snapshot pushes
and serves the same dashboard from D1, behind WebAuthn passkeys. The raw
snapshots are kept verbatim in R2 — that is what makes it a backup rather
than a view: `/admin/replay` can rebuild D1 from the bucket without your
laptop. See [worker/README.md](worker/README.md) for the five-command deploy.

Once deployed, register a passkey at `/auth/` — the first passkey claims the
archive; adding another device's passkey requires being signed in. Then
generate a per-device ingest token at `/admin` (the token reveal includes
setup instructions) and the daily snapshot job pushes automatically. A
machine that never syncs has lost nothing: the files on disk are the
archive, and the next successful sync catches up. Stale pushes cannot
clobber newer data — ingest keeps whichever snapshot is newest.

`/admin` manages the rest: ingest tokens and read-only MCP tokens (shown
once, revocable per device), session-key rotation, a snapshot inventory,
and `/admin/replay`, which rebuilds D1 from the R2 originals.

To add a second machine, run the installer there with the same Worker URL
and mint it its own ingest token — a token only ingests as the device it was
minted for, so one machine cannot write as another and revoking one never
touches the others.

## MCP

Ask your agent about your own usage. Two servers, same four read-only tools
(`usage_summary`, `usage_by_project`, `usage_by_day`, `archive_health`),
one shared implementation:

- **Local**, over the SQLite archive on this machine:

  ```sh
  claude mcp add tokenarchive node /path/to/tokenarchive/mcp-server.js
  ```

- **Remote**, served by the Worker at `/mcp` for machines that do not hold
  the archive. Mint an `mcp` token at `/admin`, then:

  ```sh
  claude mcp add --transport http tokenarchive https://YOUR-WORKER/mcp \
    --header "Authorization: Bearer YOUR-TOKEN"
  ```

Read-only by construction: there is no tool that writes, deletes or rewrites
anything.

## Configuration

Everything is overridable by environment variable, nothing requires it:

| Variable | Default |
| --- | --- |
| `TOKENARCHIVE_DATA` | `~/.local/share/tokenarchive` |
| `TOKENARCHIVE_DB` | `{data}/tokenarchive.db` |
| `TOKENARCHIVE_DEVICE` | short hostname |
| `TOKENARCHIVE_URL` | unset — no sync |
| `TOKENARCHIVE_TOKEN` | unset — read from the login keychain (`tokenarchive-ingest`) |

## Tests

```sh
bash test/run.sh
```

Most suites are hermetic. Three (`static`, `interaction`, `tooltip`) run
against the dashboard you last generated at
`~/.local/share/tokenarchive/index.html`, so they need a real archive on the
machine — run the snapshot once first.

## License

MIT