tokenarchive
by ddyy
README.md
# tokenarchive
Daily snapshots of your AI coding usage, kept forever.

[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:

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:

## 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
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues