Tokens Worker Checking collector status

Self-hosted backend for the tokens CLI, running on a single Cloudflare Worker. Every machine reports its AI-agent token usage here; D1 keeps the full matrix and one precomposed endpoint feeds lkwplus.com/tokens.

Tokens
Cost
Active days
Last report

Architecture

One Worker, three bindings. Writes are events — submissions, plus one reported snapshot per subscription — so they drive everything downstream and there is no cron. Reads never queue behind writes, and never touch D1 at all in the steady state: the dashboard payload is precomposed into KV and served with an ETag so browsers revalidate instead of guessing.

Write · every 30 minutes

tokens CLI POST /api/submit merge engine D1 · usage matrix

Each machine runs tokens serve and POSTs a full rescan of its local session logs. The payload is validated with the official server’s consistency checks, then merged day by day (semantics below).

Quota · every 30 minutes, one host

ChatGPT / Anthropic OracleARM POST /api/quota/:plan KV · one key per plan

How much of each subscription’s rate-limit window is spent — a number no session log contains. The OAuth credentials stay on the collecting host, which reports finished percentages; this Worker holds no vendor secret and runs no scheduled job. Each vendor’s body is narrowed by hand into the payload’s own shape, which is where account identity is dropped. The two legs are separate reports to separate keys, each dated by its own capturedAt, so an expired credential on one cannot take the other’s card down.

Fan-out · on every accepted submission

accepted submission KV · /api/site payload R2 · raw payload R2 · daily export

The precomposed dashboard view is rewritten in KV so reads never wait on D1. The raw payload lands at raw/<device>/latest.json — submissions are full rescans, so the latest one reproduces a device’s whole history. Once per Asia/Shanghai day, the usage tables are exported to backup/YYYY-MM-DD.json and exports past 180 days are pruned. Every leg is bounded: devices rescan on a timer whether or not anything changed, so anything that accrued per submission would grow on cadence alone.

Read · public

GET /api/* ETag · 304 if unchanged KV / D1 lkwplus.com/tokens

No auth on reads — it is usage data on a single-user backend, open to any origin (public, cookie-less). Internal device ids are never exposed, rows identify devices by display name only.

Storage

Usage is stored at maximum granularity: one row per (device, date, client, model, provider) carrying input, output, cacheRead, cacheWrite, reasoning, messages, cost and the parser revision that produced it. /api/site is an aggregation of this matrix, and raw ids are kept here, so nothing canonicalization does is lossy. Sidecar tables: daily_activity (per-day active time), devices (name, CLI version, time metrics, MCP servers) and submissions (audit log, 30-day rolling window). All dates are calendar days in Asia/Shanghai.

BindingServiceHolds
DB D1 · tokens-usage The usage matrix and its sidecar tables.
SITE_CACHE Workers KV Precomposed /api/site payload, rewritten on every accepted submission, plus one reported quota plan per subscription.
ARCHIVE R2 · tokens-archive Raw payload per device plus the daily full-table export.
TOKENS_API_TOKEN Worker secret Bearer token for the write path; compared timing-safe.

Write path

The merge engine ports the official server’s semantics, so a self-hosted history behaves exactly like a hosted one:

Per-day, per-client replace
A submission replaces each (device, day, client) bucket it mentions; clients or days it does not mention are left untouched.
Regression guard
Within the same parser revision, a resubmit that would shrink a client’s day is ignored with a warning — local log cleanup can never erase stored history.
Parser revision floors
A client reporting with a schema version older than any revision already stored for it is rejected wholesale (stale-parser protection).
Authoritative coverage
tokens submit --replace sends full-coverage windows that replace a client authoritatively — including tombstoning days the new scan no longer contains.
Validation
The official mathematical-consistency checks run on every submission (day totals vs client sums, future dates, duplicates…). Failures return 400 with details.
Atomic set-based writes
Changed rows travel as JSON parameters expanded server-side (json_each), so even a full-history first upload is a handful of statements in one D1 batch — one transaction, no partial-write states, safely below D1’s per-invocation query limits.

Read API

Public, no auth — and deliberately one endpoint. A filterable aggregation API (stats, timeseries, breakdown, graph, meta, devices, submissions) used to sit here and nothing ever called it: the CLI computes those views locally, and the dashboard reads the precomposed payload. The matrix behind them is untouched in D1 at full fidelity — reachable through wrangler d1 execute, the R2 raw archives and the daily exports — so what went away is a public surface, not the data or any question you could ask it.

/api/site serves Cache-Control: no-cache with a strong ETag — browsers revalidate every load and get a ~0-byte 304 while the data is unchanged. Every breakdown row carries the full metric set: the five token categories, their sum, messages and cost. Model and provider ids are canonicalized before aggregation (claude-fable-5-thinking-maxclaude-fable-5, while Qwen's real -plus/-max product tiers stay distinct; openai-codexopenai), which is what makes the per-day model slices agree with the model breakdown. Providers mean model vendors: a provider id that isn't a vendor claim is re-attributed by model name (qwen3.8-maxalibaba) — either the client's own gateway (zed.dev, opencode, opencode-go, opencode_go) or anything reported by a client whose models the user configures, where openai names an OpenAI-compatible endpoint rather than the vendor. Models the rules can't place stay under the reported id. D1 keeps the raw spellings, so none of it is lossy.

  1. GET open ↗

    The one-request dashboard view: totals and breakdowns for five ranges (today, 7/30/90 days, all time), each client and provider row split into the models behind it, the daily series sliced by provider, client and model, and the device inventory. The body carries a schemaVersion the consumer validates before rendering. No filters — served from KV with its precomputed ETag, never waits on D1.

  2. GET open ↗

    Liveness check.

Examples

# the whole dashboard, one request
curl "https://tokens.lkwplus.com/api/site"

# revalidate: ~0 bytes back while the data is unchanged
curl -H 'If-None-Match: "<etag>"' -i "https://tokens.lkwplus.com/api/site"

# ad-hoc questions go to the matrix directly, not to an endpoint
npx wrangler d1 execute tokens-usage --remote \
  --command "select model, sum(cost) c from daily_usage group by 1 order by c desc limit 10"

CLI endpoints

The write side implements the official server’s wire contract, so a stock CLI works with TOKENS_API_URL=https://tokens.lkwplus.com. Every CLI endpoint requires Authorization: Bearer $TOKENS_API_TOKEN. The browser OAuth device flow is not implemented — log in with tokens login --token instead.

  1. POST auth

    Full submission payload from tokens submit / serve / autosubmit. Responds with {success, submissionId, metrics, mode, warnings?}.

  2. POST auth

    One subscription’s rate-limit snapshot: codex takes the verbatim output of tokens codex status --json, claude takes Anthropic’s /api/oauth/usage response. Each is narrowed into the payload’s own shape — windows, reset times and unspent reset credits — and overwrites that plan’s key alone. Responds with {success, provider, capturedAt}.

  3. DEL auth

    Wipes all stored data across all three stores (tokens delete-submitted-data): the D1 tables, every R2 archive and backup, every reported quota plan, and the KV site payload — recomposed before the response, so success means the public dashboard is already clean.

  4. GET auth

    Token validation for tokens login --token. Responds with {user: {username}}.

Operations