Skip to content

Latest commit

 

History

History
430 lines (346 loc) · 48.7 KB

File metadata and controls

430 lines (346 loc) · 48.7 KB

API

The hub exposes a small JSON HTTP API.

Authentication

All endpoints except /api/health require the configured shared secret.

Prefer a header (first-party clients already do this):

Authorization: Bearer <secret>

or:

X-Token-Monitor-Secret: <secret>

The Node hub is header-only. The Cloudflare Worker still accepts ?secret= as a limited compatibility / migration path for iOS widget runtimes that cannot set Authorization. A secret in the URL can appear in proxy, platform, and history logs; new clients should send a header.

GET /api/health

Health check. Does not require authentication.

Example response:

{
  "ok": true,
  "role": "hub",
  "runtime": "cloudflare-worker",
  "version": 1,
  "hubBuild": {
    "schemaVersion": 1,
    "runtime": "cloudflare-worker",
    "coreRevision": 1,
    "coreBuildId": "sha256:…",
    "runtimeRevision": 1,
    "runtimeBuildId": "sha256:…"
  },
  "deviceCount": 2,
  "secretRequired": true,
  "now": "2026-05-18T00:00:00.000Z"
}

version remains the legacy Hub storage/API value and is not a deployment version. hubBuild is the content-derived deployment identity used by Token Monitor to compare the remote Hub with the core bundled by the app. core* identifies shared Node/Worker aggregation logic; runtime* identifies the Node Hub or Cloudflare Worker adapter. Product-only version bumps do not change either build ID. This is a build marker generated from the registered source closure, not a runtime attestation: a fork that changes source without regenerating its metadata may still report the marker it started from. A health response without hubBuild is a legacy Hub and remains otherwise compatible; present but malformed metadata is unrecognized instead of being treated as legacy.

POST /api/ingest

Posts one device usage summary.

Example payload:

{
  "deviceId": "macbook",
  "hostname": "macbook.local",
  "platform": "darwin-arm64",
  "osName": "macOS",
  "osVersion": "26.0",
  "updatedAt": "2026-05-18T00:00:00.000Z",
  "agentVersion": "0.3.0",
  "agentRuntime": "headless-agent",
  "syncUploadIntervalMs": 1200000,
  "projectsEnabled": true,
  "historyAvailable": true,
  "trackedClients": ["codex"],
  "today": {
    "capabilities": {
      "tokenComponents": true,
      "throughput": true
    },
    "totalTokens": 1234,
    "costUsd": 0.01,
    "cacheReadTokens": 1100,
    "cacheWriteTokens": 0,
    "outputTokens": 34,
    "timedTokens": 1230,
    "timedOutputTokens": 34,
    "timedDurationMs": 4200,
    "clients": {
      "codex": 1234
    },
    "clientCosts": {
      "codex": 0.01
    },
    "clientCacheReads": {
      "codex": 1100
    },
    "clientCacheWrites": {
      "codex": 0
    },
    "clientOutputs": {
      "codex": 34
    },
    "models": {
      "gpt-5": 1234
    },
    "modelCosts": {
      "gpt-5": 0.01
    },
    "modelCacheReads": {
      "gpt-5": 1100
    },
    "modelCacheWrites": {
      "gpt-5": 0
    },
    "modelOutputs": {
      "gpt-5": 34
    },
    "clientModels": {
      "codex": {
        "gpt-5": 1234
      }
    },
    "clientModelCosts": {
      "codex": {
        "gpt-5": 0.01
      }
    },
    "sessions": {
      "codex:rollout-2026-05-30T11-44-50-abc": {
        "client": "codex",
        "sessionId": "rollout-2026-05-30T11-44-50-abc",
        "totalTokens": 1234,
        "costUsd": 0.01,
        "messageCount": 3,
        "inputTokens": 100,
        "outputTokens": 34,
        "cacheReadTokens": 1100,
        "cacheWriteTokens": 0,
        "reasoningTokens": 0,
        "startedAt": "2026-05-30T03:44:50.000Z",
        "lastUsedAt": "2026-05-30T04:07:32.679Z",
        "contextTokens": 190867,
        "contextWindow": 950000,
        "turnEnded": false,
        "projectId": "sha256:opaque-project-identifier",
        "projectLabel": "token-monitor",
        "models": {
          "gpt-5": 1234
        },
        "modelCosts": {
          "gpt-5": 0.01
        },
        "providers": {
          "openai": 1234
        }
      }
    }
  },
  "month": {
    "totalTokens": 4567,
    "costUsd": 0.04,
    "clients": {},
    "clientCosts": {}
  },
  "allTime": {
    "totalTokens": 8901,
    "costUsd": 0.08,
    "clients": {},
    "clientCosts": {},
    "projects": {
      "token monitor": {
        "label": "Token Monitor",
        "tokens": 8901,
        "costUsd": 0.08,
        "clients": { "codex": 8901 }
      }
    }
  },
  "periodWindows": {
    "timeZone": "Asia/Hong_Kong",
    "today": { "key": "2026-05-18", "endsAt": "2026-05-19T00:00:00.000Z" },
    "month": { "key": "2026-05", "endsAt": "2026-06-01T00:00:00.000Z" }
  },
  "limits": {
    "updatedAt": "2026-05-18T00:00:00.000Z",
    "refreshMs": 300000,
    "providers": [
      {
        "provider": "claude",
        "accountKey": "sha256:...",
        "status": "ok",
        "updatedAt": "2026-05-18T00:00:00.000Z",
        "windows": [
          {
            "kind": "session",
            "usedPercent": 42,
            "remainingPercent": 58,
            "resetsAt": "2026-05-18T05:00:00.000Z"
          },
          {
            "kind": "weekly",
            "usedPercent": 20,
            "remainingPercent": 80,
            "resetsAt": "2026-05-25T00:00:00.000Z"
          }
        ]
      }
    ]
  }
}

The hub normalizes records before storing them. The Node hub accepts JSON ingest bodies up to 1 MiB; larger bodies return 413 payload_too_large.

projects is a bounded rollup keyed by a canonicalized workspace-folder label. Each entry carries the deterministic display label, token/cost totals, and a per-client token breakdown. Agents upload allTime.projects because synchronized payloads intentionally omit the unbounded allTime.sessions; today.projects and month.projects are normally omitted on upload and rebuilt by the hub from their synchronized sessions. If adding the all-time rollup would exceed the safe ingest budget, the agent drops only that rollup, sets allTimeProjectsOmitted: true, and keeps core totals and session data uploadable. If monthly or daily session detail would still exceed the budget, the agent keeps the newest rows that fit, sends the complete project rollup for that period, and sets sessionDetailsOmitted to the number of omitted rows per affected period. If that project rollup cannot fit even after all session rows are removed, the agent omits it too and sets periodProjectsOmitted; token/cost and client/model totals remain complete while the affected project breakdown is marked incomplete. A normal later upload clears these diagnostics; limits-only updates preserve them. projectsEnabled: false tells the hub that project metadata collection is disabled for this device; sync payloads then remove project rollups plus session projectId / projectLabel fields.

Authenticated stats expose projectsIncomplete: true when a device omitted its rollup, disabled project tracking while contributing usage, or could not preserve exact all-time attribution after its tracked-client list changed. Affected device entries expose allTimeProjectsOmitted, allTimeProjectsIncomplete, or projectsEnabled: false as the reason. The public Worker stats endpoint removes the entire projects map, including both display labels and canonical keys.

timedTokens, timedOutputTokens and timedDurationMs are optional throughput inputs, summed from tokscale's per-entry performance block. timedDurationMs is the sum of per-message durations, not a wall-clock span — concurrent sessions contribute their durations separately — and timedTokens counts the tokens of the messages that carried a duration. Coverage is only meaningful per tokscale entry and must not be reconstructed as timedTokens / totalTokens after aggregation: that ratio mixes clients with completely different coverage, and it is not even bounded by 1, because tokscale counts reasoning in its own token total while totalTokens deliberately does not.

timedOutputTokens is the output of the entries that carried a duration — an entry contributes its output exactly when it contributes its duration, so timedOutputTokens / timedDurationMs always divides two totals describing the same entries. The gate is applied per entry rather than rebuilt from period totals: several tracked clients report no durations at all, so anything derived from summed totals lets one client's output ride on another client's clock, and the resulting rate drifts with the client mix rather than with throughput.

tokscale also reports a per-entry tokenCoverage, and this deliberately does not scale by it. Doing so would assume output is spread evenly across an entry's tokens; in practice output is ~0.3–3% of an entry's tokens while the untimed remainder measures several times an entry's entire output, so that remainder is cache and input rather than generation, and scaling would discount output that was almost certainly timed. Ignoring it also keeps the field a plain integer counter that merges and deltas like every other token count.

All three are reported as raw sums rather than a pre-divided rate because a ratio cannot be summed: consumers add each field across devices and periods and divide only at the point of display, which makes a fleet-wide rate duration-weighted. Current producers also set capabilities.throughput to true. Payloads without these fields are accepted and normalize to 0 with that capability set to false, so a later complete snapshot cannot turn the synthetic zeroes into an all-day live-rate delta.

Because the gate is all-or-nothing per entry, timedOutputTokens ≤ outputTokens is a physical bound: a period cannot have timed more output than it produced. The two are equal when every entry in the period reported durations. A partly timed entry — 1230 of 1234 tokens in the example above — still contributes all of its output, since the untimed remainder is cache and input rather than generation.

The collector satisfies that bound by construction, but the hub and the Worker normalize records posted by any agent, so normalization enforces it: a timedOutputTokens larger than the record's own outputTokens is capped rather than trusted. Ingest is a trust boundary here, and this value divides straight into a headline rate.

All three are additive over append-only messages, which keeps them exact under the delta path a watch-triggered scan uses to carry a today rescan into month and allTime. The one case where timedOutputTokens and a full rescan can disagree is a session that spans the boundary and starts or stops reporting durations partway through, since a rescan then re-gates the whole session on its combined state; the next full scan reconciles it. Closing even that needs a per-message timed-output counter from tokscale.

Each native period may include capabilities.tokenComponents. Current producers set it to true when cache read/write and output were derived from individual Tokscale rows, and to false when any part of the period has only aggregate provenance. Partial periods retain their known components and carry the unsupported remainder in unclassifiedTokens, clientUnclassifiedTokens, and modelUnclassifiedTokens; consumers display that remainder as Unclassified instead of silently treating it as cache miss. When an aggregate remainder has no Tool or Model identity, consumers expose a synthetic Unclassified attribution row so the visible breakdown still adds up to the period total. Session archives preserve aggregate and Tool components, but a session spanning multiple models leaves its Model components unclassified rather than guessing a proportional split. A missing marker remains accepted for older DAY / MONTH / TOTAL payloads, but fixed-range live-day derivation requires explicit true or explicit unclassified fields. Device aggregation and retained client/session restoration preserve these fields and propagate incomplete provenance fail closed.

trackedClients is optional but recommended for agents and widgets. When it is present, the hub treats omitted clients as intentionally not collected in this payload and preserves their previous usage for that device. This keeps "tracking" as "collect future data" rather than "hide existing history".

historyAvailable is an explicit boolean capability for retained History. Current producers send it on every usage snapshot: true means History collection is enabled, while false means disabled. Fixed-range readers require both historyAvailable: true and a retained history object; a missing capability (including records passed through an older Hub) is unavailable rather than an inferred zero. The history field itself remains interval-gated: omission means "no History update this tick", explicit null means unavailable, and an object replaces the retained History.

Current History daily rows may also carry cacheReadTokens, cacheWriteTokens, outputTokens, unclassifiedTokens, and the same fields inside each perClient / perModel entry. tokenComponentsAvailable: true means the entire row has exact component provenance. Missing provenance does not change the exact total tokens, cost, Tool, or Model attribution: fixed ranges retain every known cache/output component and place only the unsupported remainder in unclassifiedTokens instead of treating it as zero or cache miss. The local daily archive keeps component provenance permanently, while sync payloads keep detailed components only for the latest 30 days because WEEK / 7D / 30D never need older detail and /api/ingest has a 1 MiB ceiling. If even that additive detail would push a device payload over its budget, serialization drops the component fields before any existing project or session detail.

Current agents and widgets include osName and, when known, osVersion so device details can show a user-facing operating-system release. macOS uses the product version from Electron or sw_vers; Windows uses the product family and display version from the registry; Linux uses the distribution name and version from os-release. Detection failures fall back to an explicitly labelled Windows build or Linux kernel release. The hub continues to accept older payloads without these fields.

syncUploadIntervalMs is optional. A remote-hub widget includes 0 for live uploads or the selected fixed interval in milliseconds (600000, 1200000, or 1800000). The hub uses a positive interval to keep the device and its limits fresh for at least twice the upload interval; omitted or 0 values retain the configured staleAfterMs behavior. Local collection and embedded-host ingest remain live.

periodWindows is optional. Agents and widgets stamp each snapshot with the UTC instant its today/month windows end, computed in the device's own local time (endsAt = next local midnight / next local month start; key is the device-local day/month for reference). New producers also include their IANA timeZone, which lets retained daily History keep using that device's calendar after it goes offline. The hub uses endsAt to expire a device's today/month from the native aggregate once now >= endsAt, so an offline device does not keep contributing a stale day/month snapshot (allTime never expires). Payloads without periodWindows fall back to a UTC day/month comparison against updatedAt; fixed History ranges fail closed after an unzoned producer window expires.

clientHealth is optional per-client diagnostics: why a tracked tool shows the number it shows. It sits alongside the older clientStatus map (active / waiting / missing per client), which agents continue to send unchanged.

{
  "clientHealth": {
    "version": 1,
    "observedAt": "2026-08-04T09:15:00.000Z",
    "clients": {
      "claude": {
        "source": { "state": "detected", "detectedCount": 1, "checkedCount": 2 },
        "collection": { "state": "direct" },
        "data": { "liveTokens": 481230, "lastActivityDay": "2026-08-04" },
        "overall": "healthy"
      },
      "antigravity": {
        "source": {
          "state": "detected",
          "detectedCount": 2,
          "checkedCount": 3,
          "checks": [
            { "id": "tokscale-antigravity-cache", "exists": true },
            { "id": "antigravity-ide-source", "exists": true },
            { "id": "antigravity-cli-data", "exists": false }
          ]
        },
        "collection": {
          "state": "failed",
          "syncFailureStage": "timeout",
          "syncDetailCode": "network-timeout",
          "lastAttemptAt": "2026-08-04T09:12:00.000Z",
          "lastSuccessAt": "2026-08-04T08:40:00.000Z"
        },
        "data": { "liveTokens": 0, "lastActivityDay": "2026-08-03" },
        "diagnostics": [{ "code": "sync-timeout" }],
        "overall": "attention"
      }
    }
  }
}

Every tracked client sends the same fixed core — source.state, source.detectedCount, source.checkedCount, collection.state, data.liveTokens, and overall — because the hub recomputes overall from those inputs rather than storing what the producer claimed. Detail beyond the core is sparse: source.checks and diagnostics are sent only for a client that is not healthy, and a client with nothing to report sends neither.

overall is healthy (usage was observed), waiting (sources present, nothing counted yet), attention (something we do on the user's behalf is failing), unavailable (no source found at all), or unknown. source.state is detected, missing, or unknown, and is derived from the counts on ingest rather than read from the payload, so a state that contradicts them cannot be stored; detectedCount is clamped to checkedCount first, and a client with nothing probed is unknown rather than missing. collection.state is direct for the clients whose files are parsed in place — the common case, with no fetch step to succeed or fail — and idle / pending / ok / failed for the self-synced clients (Cursor, Antigravity) whose usage is refreshed by a subprocess. A value the reader does not recognize becomes unknown, never direct: direct is the positive claim that there is no fetch step to fail, so collapsing a future state onto it would report a broken client as working.

A client installed only inside a running WSL distro has no host directory, and its usage is merged into the same periods before either derivation runs. Its WSL marker is therefore a source that exists, reported as the wsl-home check — without it the same snapshot would count the client's tokens and call its source missing.

source.checks[].id is a stable identifier for a kind of source root, never a filesystem path: one id can stand for several platform variants (a VS Code workspace-storage root has one per platform), and an absolute path contains the user's home directory. Ids outside the recognized set are dropped on ingest. A failed self-sync likewise reports a stable code in diagnostics (sync-failed, sync-timeout, sync-spawn-failed, sync-exit-error) and never the subprocess's stderr. The other diagnostic codes are source-missing, no-usage-observed, and wsl-detected-no-data; the last one states that a WSL marker was found and the scan returned nothing, which can equally mean the tool is installed in that distro and unused.

For a failed self-sync, collection.syncFailureStage is an optional bounded stage: spawn, timeout, process-exit, or unknown. collection.syncDetailCode is a conservative classification of the failure: language-server-not-found, rpc-failed, permission-denied, cache-write-failed, invalid-response, network-timeout, network-failed, authentication-failed, or unknown. A non-negative collection.syncExitCode is included only when the subprocess reported a numeric exit code. These fields add process-level evidence without exposing stderr, paths, or provider output; an exit code is not interpreted as a universal root cause.

diagnostics entries are objects carrying a code, not bare strings, even though code is the only field today: the extension point belongs inside the entry, matching how LSP, ESLint, SARIF, and RFC 9457 all shape a diagnostic. Adding a field to the object stays backward compatible; turning string[] into object[] would not. Severity is deliberately not on the wire — the same code means different things on different clients, so it is a renderer decision rather than something a collector can know. Observation time is likewise recorded once, as clientHealth.observedAt, rather than per diagnostic: every entry comes from the same scan. It is its own field because a limits-only ingest carries health forward while the record's updatedAt moves on, so updatedAt cannot be read as the time the diagnosis was made.

There is deliberately no code for "some roots found, others absent". A client's roots are alternatives rather than dependencies — Antigravity's IDE cache, native sources, and CLI data are three ways to have it installed — so a partial set is what a normal install looks like. source.checks reports which ones were found as neutral evidence; only finding nothing at all is source-missing.

A diagnostic the rest of the entry does not support is dropped on ingest rather than stored: sync-* requires a failed collection, source-missing a missing source, and no-usage-observed a detected source with nothing counted — "we can read this client and found nothing" is a different statement from "there is nothing to read". source.checks is held to the same standard: it is evidence for detectedCount/checkedCount, so an array whose length or found-count disagrees with them is dropped whole rather than allowed to overwrite the core. The hub stores a record that is internally consistent, not one that merely passes per-field range checks.

data.liveTokens is the collector's per-client all-time usage as scanned, before any archive restoration runs. It is a lower bound on the device record's allTime.clients[<id>], not a copy of it. Two separate restorations run afterwards, in the widget and the agent rather than in the collector: untracked-client usage, which by definition never touches a client that has a health entry, and session usage preserved after its source files were deleted, which applies to any client including tracked ones. So a tracked client can legitimately report liveTokens: 0 in a record whose allTime counts its tokens.

The difference between the two is therefore not a way to derive archived contribution — it mixes two archives with different rules, and consumers must not treat it as one. ClientHealth v1 deliberately describes neither archived usage nor presentation-layer data origins; attributing them belongs where the composition actually happens.

data.lastActivityDay is the most recent day the collector holds usage for this client, taken from the daily history buckets. It is deliberately not "last used": tokscale exposes no per-turn timestamps, and the field is omitted entirely when history is unavailable.

Every value is a closed enum and every list is capped — including the client ids themselves, which are bounded in both count and length. A hub that does not recognize a value downgrades it to unknown rather than storing it, so an older hub in front of a newer agent degrades instead of passing unvalidated data to renderers. clients must be a plain object: an array, or a prototype-sensitive key such as __proto__, is refused rather than stored under an invented client id.

A limits-only ingest carries the previous usage forward, so clientHealth — along with clientStatus and wslStatus — travels with it when the payload omits the field. A full update that omits it still clears it: an agent posting complete usage without health is stating that it has none.

clientHealth rides on the device record and is returned by the authenticated GET /api/stats inside devices[]. It is never aggregated across devices and never appears on GET /api/public/stats, which drops devices wholesale — a cross-device rollup is the one shape that would place these diagnostics on the unauthenticated surface.

limits is optional. Agents and widgets include it when AI Tool Limits detection is enabled. Raw OAuth credentials, access tokens, refresh tokens, and provider response bodies must never be sent.

limits.providers[].provider is one of claude, codex, cline, factory, cursor, antigravity, opencode, openrouter, deepseek, minimax, mimo, grok, copilot, kiro, commandcode, zai, zaiteam, volcengine, qoder, workbuddy, kimi, ollama, alibaba, or thirdparty. limits.providers[].accountKey is a stable hashed account identifier (sha256:…) used to dedupe the same account across devices. OpenCode may additionally carry webAccountKey, a private canonical Web identity kept separately from the device-local DB-path identity, plus accountKeyAliases, a bounded list of private hashed legacy identities used to merge go: / zen: records from older devices with the canonical workspace: identity during rolling upgrades. accountEmail is the account email when available, and accountName is a sanitized display/profile name. Codex may additionally send workspaceKind: "personal" when the workspace has no provider-supplied name, allowing account-management UI to localize the Personal label without persisting translated text. accountLabel is the legacy provider-defined short label retained for mixed-version compatibility: older OpenCode renderers use it as the profile name, while existing providers may use it for the plan. planLabel is the explicit plan label (for example Plus, Go, or Zen) when identity and plan must be carried separately; readers fall back to accountLabel for payloads produced before planLabel existed. Third-party rows additionally carry a bounded adapterId (newapi-account, newapi-token, sub2api, or custom) so every authenticated renderer can retain that adapter's label, icon, and colour after Hub synchronization. These fields MAY be sent to the authenticated hub so devices can identify each account and its plan. The hub ingest is protected by the shared secret; the public stats endpoints (publicLimits) strip accountKey, webAccountKey, accountKeyAliases, accountEmail, accountName, accountLabel, planLabel, workspaceKind, usageSummary, the third-party balance.quotaGroup, and balance.tranches so neither account identity, plan labels, detailed usage/cost data, custom group labels, nor per-grant credit detail are exposed publicly. limits.providers[].source is one of oauth, cli, web, rpc, local, or api; local means the value was read from an on-disk store or app-owned local capability such as OpenCode Go usage from opencode.db or WorkBuddy's local app session, web means a browser/session cookie backed web endpoint (Cursor, OpenCode web accounts, Qoder, Command Code, MiMo, Kimi membership, Ollama), and api means a provider HTTP API authenticated by an API key, access token, or AK/SK credentials (Factory, OpenRouter, DeepSeek, Minimax, Copilot, GLM/Z.ai, Volcengine, Kimi Code, WorkBuddy headless collection, and third-party adapters). WorkBuddy local-app monitoring follows the provider selection, runs in the Electron main process on macOS and Windows, and never places raw credentials or provider response bodies on the wire. limits.providers[].actionRequired is an optional bounded action hint. accountVerification means the provider explicitly requires an interactive account-verification step; current Antigravity renderers direct the user to complete that step in Antigravity and then refresh. appSessionEncrypted means the provider's local app owns a credential it sealed with a key Token Monitor does not hold, so signing in again cannot change the outcome; WorkBuddy renderers label that row instead of prompting for a sign-in. The hint never includes the provider-supplied verification URL, raw credential material, or raw response body. limits.providers[].balanceUsd is an optional prepaid credit balance in USD (OpenCode Zen); null when the provider has no balance concept or none could be read. A genuine 0 (no remaining credit) is distinct from null. OpenCode also reports that balance as a credits window with showMeter: false, so readers that only know windows can render it; balanceUsd remains for readers that predate the window, and the two always carry the same amount. limits.providers[].balance is an optional native-currency balance block. DeepSeek uses { amount, currency, todaySpend, monthSpend, allTimeSpend, trackingSince, monthSinceTracking }: amount is the spendable balance in the account's own currency (e.g. CNY/USD); the spend fields are derived from locally observed paid-balance drawdown, allTimeSpend keeps accumulating after old daily buckets are pruned, trackingSince records when that local observation began, and monthSinceTracking is true until a full month of history has accrued. Z.ai uses the same field set with a different derivation: the finance report gives a provider-reported cumulative totalSpendAmount, and the today/week/month deltas are tracked locally as positive deltas of that total — a drop (refund, reset) moves the baseline without recording negative spend, so allTimeSpend here means accumulated spend since tracking began, not the provider's lifetime figure. WorkBuddy uses { amount, currency: "CREDITS" }: amount is the current provider Credits balance from the billing snapshot; WorkBuddy does not expose official Today/Week/Month/All-time usage history because its billing API does not provide those period totals. OpenRouter uses USD: /key supplies todaySpend, weekSpend, monthSpend, and the provider-reported lifetime allTimeSpend; when OpenRouter authorizes /credits (officially documented for Management keys), amount and the corresponding real Credits meter are also included. Other OpenRouter keys can still report their own spend and configured key limit without inventing an account balance. MiMo may additionally send giftBalance, cashBalance, Token Plan usage fields, and planStatus (active, expired, none, or null). An expired MiMo Token Plan has no quota window even when its prepaid balance remains available. Claude uses { amount, currency, expiresAt, tranches } for its prepaid usage-credit pool; it is read from claude.ai and is therefore present only for web-session accounts, never on the OAuth path. It is reported whether or not the account currently has usage credits switched on, but an account that has never funded the pool sends no balance at all rather than a zero one. balance.tranches is an optional array of { amount, currency, expiresAt } credit grants — purchased and promotional merged, soonest expiry first, grants without an expiry last — omitted entirely when the provider has none. Claude's credits window is reported with showMeter: false: the balance is a sum of independently expiring grants with no quota denominator, so no meter percentage may be derived for it. The public stats endpoints strip balance.tranches along with balance.quotaGroup, so per-grant amounts and expiry dates never leave the authenticated surface. usageSummary is an optional normalized period-detail block containing request/token counts, standard and actual cost, and average response duration when a provider exposes them. null when not applicable.

thirdparty is an explicit adapter registry rather than a universal balance endpoint. Its New API-compatible account preset calls /api/user/self with an access token and adds New-Api-User only when a User ID is configured. New API normally requires that ID, while compatible One API forks may accept the same endpoint without it. amount is the whole account's remaining quota, allTimeSpend is the provider-reported used quota, requestCount is the non-negative lifetime request count when returned, and quotaGroup is the bounded account group label. The New API API-key preset calls /api/usage/token/ with a regular API key and reports only that key's independently configured quota; expiresAt is included when the endpoint supplies a positive expiry timestamp. New API quota points are normalized to USD using the instance's quota_per_unit, and unlimited quota keeps amount: null instead of inventing a zero balance. The Sub2API-compatible account preset calls /api/v1/auth/me on the dashboard Base URL with the browser dashboard's auth_token as a Bearer access token. A current access token is required when saving a profile; the optional refresh token is only a renewal credential for an account that already passed its save probe. Its balance is already denominated in USD, so no quota conversion applies, and a non-zero business code returned with HTTP 200 fails closed. The preset also fetches rolling-month usage stats (/api/v1/usage/stats?period=month) for monthSpend and /api/v1/usage/dashboard/stats for the dashboard's cumulative allTimeSpend; the monthly response additionally supplies usageSummary requests, input/output/cache tokens, standard and actual cost, and average response duration. Both usage requests are optional enrichment, so an older deployment without either endpoint still reports the balance, and the limits meter is derived by the renderer as balance / (balance + monthSpend) — the same display-only rule as DeepSeek top-up balances, never written to the wire. The stable account key is derived from the normalized Base URL and /auth/me user ID; its canonical hash is persisted as non-secret local profile metadata so profile renames, credential rotations, transient failures, and different profile names on another device do not split one account. Dashboard access tokens are short-lived, so a profile may optionally store the rotating single-use refresh_token. An unauthorized balance request triggers one POST /api/v1/auth/refresh; both tokens in the rotated pair are required, and the pair must be accepted by the persistence callback before the balance request is retried, while an incomplete response, missing callback, or failed compare-and-swap fails closed. Because Token Monitor and the browser initially share the copied refresh token, users should prefer a dedicated dashboard session; the source browser session may need to sign in again after the first rotation. The declarative Custom preset performs one GET request on the configured Base URL, authenticates with either Authorization: Bearer or x-api-key, and maps a required remaining-balance JSON path plus optional used and total paths. Its fixed currency and divisor are local configuration; missing or non-numeric mapped values fail closed, and it never guesses a provider's billing endpoint from OpenAI or Anthropic inference compatibility. Third-party presets do not synthesize periods beyond those explicitly supplied by their selected adapter.

DeepSeek uses source: "api" and has no rate-limit windows; its windows array carries only the balance as a credits window. Factory reports Standard 5-hour/weekly/monthly token-rate windows and optional Core windows when the current billing API supplies them; legacy billing accounts instead report Standard/Premium billing windows. Its optional extra-usage balance is USD. OpenRouter, GLM/Z.ai, Volcengine, Qoder, Command Code, WorkBuddy, Kimi, Ollama, and third-party adapters report quota/credit windows through the same windows array. Command Code reports its 5-hour and weekly rolling limits as ordinary percentage windows, and ships its monthly grant (plus any rollover top-up) as credits windows in USD. WorkBuddy reports its provider Credits balance as a credits window with currency: "CREDITS", preserving the existing mixed-version wire contract instead of adding a provider-specific schema marker. Third-party profile Base URLs, endpoint paths, response mappings, user IDs, and raw credentials remain local and are never added to this wire shape; only the bounded non-secret adapterId is synchronized for presentation. windows[].kind is session, daily, weekly, or billing. windows[].source is optionally web or local when one provider row combines components from different origins; OpenCode uses it so a device can hide its own opt-in local DB estimate without hiding Web quota or another device's estimate. Readers must treat the field as component provenance rather than replacing limits.providers[].source, and older windows may omit it. Codex windows may carry the backend metered-feature identity as windows[].limitId; its separately metered buckets also carry windows[].additional: true, so compact readers can exclude them without treating the display-only label as an identity signal. Z.ai Start/Weekend windows carry windows[].limitId: a plan id for individual buckets, a deterministic zcode-model: identity for a model aggregate, or a zcode-bucket: fallback when the plan id is absent. This distinguishes them from MCP, which has no limitId. Aggregation adds complete token pools with the same returned model name and computes percentages from summed units; optional meter and unit_type metadata do not split a model pool. A mixed pool keeps the earliest component boundary as resetsAt, so scheduling and burn-rate re-baselining behave as for any other window. windows[].boundaryKind optionally types that timestamp as reset, expiry, or mixed; omitted means the legacy reset presentation. The type changes wording only, not scheduling: expiry renders Expires, while mixed reports a simultaneous reset and expiry without pretending either lifecycle is the other. These live HTTPS windows omit component source: local; local credential discovery does not imply local quota data. windows[].metric is an optional stable machine-readable role; credits identifies a provider's balance/credits meter and spend a money-already-consumed meter, both independently of the display label (currently OpenRouter account credits, third-party account/token quota, DeepSeek balance, MiMo balance, and Z.ai cash balance). A credits window's headline value is money, carried as an absolute remaining amount rather than a percentage; balance providers with no fixed quota denominator report no usedPercent/remainingPercent at all, and any meter percentage for them is derived by the renderer and deliberately kept off the wire. windows[].currency is an optional currency code (uppercase, at most 8 characters) that applies to the window's absolute used/limit/remaining amounts, so a balance renders in its own currency without conversion. Normalization restores a credits window from a provider's balance.amount when the record carries a balance but no such window, so records posted by devices older than this field keep rendering; only the amount is restored, never a percentage. A collector that reports a balance and does not want that synthesized meter must emit its own credits window with showMeter: false, as Claude does. Claude may additionally report a billing window with metric: "spend" labelled Usage credits, carrying used and — only when the account has set a monthly spend limit — limit and a derived usedPercent. Without such a limit it reports used alone with showMeter: false; the upstream spend.percent field reports 0 rather than null in that state and is deliberately not forwarded. windows[].detail is an optional bounded display-only description for a window, such as the Kimi-vs-Code composition of the single shared monthly membership meter; it must not contain credentials or raw provider response data.

The legacy success response contains { ok, deviceId, stats }. A producer that does not consume that duplicate stats snapshot may send x-token-monitor-response: minimal; a supporting hub then returns only { ok, deviceId }. Older hubs ignore the request header and keep returning the legacy response, so producers must accept both shapes. Stats and ingest JSON responses of at least 1 KiB use gzip when the request's Accept-Encoding allows it.

GET /api/stats

Returns aggregate stats for the widget.

Response includes:

  • staleAfterMs, the effective Hub threshold used to recompute device and provider freshness
  • periods.today
  • periods.month
  • periods.allTime
  • periods.*.clientModels and periods.*.clientModelCosts for preserving model breakdowns when a tracked tool is disabled
  • periods.*.projects for workspace-level tokens, cost, and client attribution; the same canonical folder label aggregates across devices
  • periods.today.sessions / periods.month.sessions keyed by client:sessionId for session-level usage when tokscale exposes session groups; widgets may use lastUsedAt for recent-first sorting, optional projectId / projectLabel for workspace-level aggregation, and sessionKind: "background-review" for non-interactive review runs. contextTokens / contextWindow report what the session's context window currently holds and how large the client said that window is; both are 0 unless the collecting device reads that client's transcript and the session was recent enough to still be open, so they describe a live session and are not a usage total. turnEnded is the client's own turn boundary and is deliberately three-state rather than a plain boolean: true means the transcript said the turn finished, false means it said a turn is under way, and the field is absent when the client reports no boundary at all (or was not read). Consumers must keep false distinct from absent — only an explicit false may retire a true they already hold — and a client that states no boundary leaves the field unset so those readers keep their previous reading. Absolute workspace paths and locally resolved conversation titles stay on the collecting device and are never part of the wire shape. Synchronized clients omit the unbounded allTime.sessions collection and may bound today / month detail when required by the ingest limit while preserving all aggregate totals and breakdowns.
  • sessionDetailsOmitted, when one or more synchronized devices omitted session rows to stay within the ingest limit; the aggregate contains summed today / month counts and each affected device reports its own counts
  • periodProjectsOmitted, when a daily or monthly project rollup was itself too large to fit; the aggregate and affected devices expose omitted project counts and the widget marks that period's project breakdown incomplete
  • projectsIncomplete plus the corresponding devices[].allTimeProjectsOmitted, devices[].allTimeProjectsIncomplete, or devices[].projectsEnabled diagnostic
  • historyPreview.daily[].activeTimeMs, historyPreview.monthly[].activeTimeMs, and historyPreview.summary.activeTimeMs when tokscale graph exposes session active-time metrics
  • historyRevision, a compact invalidation hash for the aggregate History preview, and deviceHistoryRevision, a device-identity-aware hash used to invalidate per-device fixed-range caches when History ownership or availability changes
  • limits.providers aggregated by provider account
  • subscriptionsUpdatedAt, the updatedAt of the hub's shared subscription list, or "" if nothing has been written to it. The version only, never the records: a device compares it against the copy it holds and re-reads /api/subscriptions only when it has been overtaken. This is how an edit made on one device reaches the others, so a client that does not consult it will only see the shared list as it stood when it connected. Omitted from public Worker stats. An absent field means "no news" rather than an empty list.
  • devices, including each device's normalized periods, limits, receivedAt, osName / osVersion when reported, optional syncUploadIntervalMs, and optional periodWindows
  • stale status for devices that have not reported recently

If multiple devices report the same provider account, the hub keeps the freshest valid limits status for that account. Public Worker stats omit account identifiers.

GET /api/stats/stream

Returns an SSE stream. Every connection begins with a complete snapshot event and receives : hb comments every 30 seconds. Reconnecting therefore restores a complete state without relying on an earlier event.

Ingests within a 100 ms window are coalesced. If usage, limits, sessions, projects, History, subscriptions, or derived stale/period state changed, the hub sends one complete stats event containing the newest state. An ingest that changed only the device's transport timestamps does not send another complete snapshot.

Official clients send x-token-monitor-stream: 2. For those clients, a timestamp-only ingest produces a small freshness event containing only limits.updatedAt plus per-device timestamps/stale state; the client merges that metadata into its existing snapshot without replacing limit providers, periods, or session/project detail. Clients without the header retain the legacy behavior and receive a complete stats event for every coalesced ingest window. A newer client remains compatible with an older hub because the header is advisory and the existing snapshot / stats events are unchanged.

GET /api/devices

Returns normalized records for all stored devices.

DELETE /api/devices/:id

Deletes one device record from the hub store.

This is useful after renaming a device id.

GET /api/subscriptions

Returns the hub's shared subscription list.

{
  "ok": true,
  "version": 1,
  "updatedAt": "2026-08-02T09:14:11.204Z",
  "subscriptions": [
    {
      "id": "sub_1754126051204_k3xq",
      "provider": "codex",
      "kind": "subscription",
      "binding": { "profileName": "", "accountKey": "sha256:…", "accountEmail": "you@example.com" },
      "planName": "Plus",
      "amountMinor": 9000,
      "currency": "HKD",
      "interval": "month",
      "intervalCount": 1,
      "startDate": "2026-05-31",
      "topUps": [],
      "autoRenew": true,
      "nextRenewalOverride": null,
      "endDate": null,
      "note": "",
      "updatedAt": "2026-08-02T09:14:11.204Z"
    }
  ]
}

Unlike usage and limits, subscriptions are not part of a device record. A subscription describes an account rather than a machine, and account keys are not stable across platforms — the same OAuth login hashes differently on macOS and Windows — so per-device copies could not be reliably deduplicated and a two-machine setup would double its own monthly total. The hub therefore stores exactly one list, shared by every device connected to it, and a delete is a delete with no tombstone needed to stop another device resurrecting it.

Devices in local mode keep their own list in the widget's settings.json and never call these endpoints. In client and host mode settings.json holds only the last-known copy, so a hub that is unreachable at startup still shows the records; writes made while it is unreachable are refused rather than applied locally, which would fork the shared list.

Both endpoints sit behind the same secret gate as every other data route, and the list is never included in publicStats / publicLimits, which are built from device records alone.

topUps[] entries are { id, date, amountMinor }, newest first. Amounts are integer hundredths of a unit in the record's own currency. Dates are plain YYYY-MM-DD calendar days, never timestamps.

PUT /api/subscriptions

Replaces the shared list.

{
  "subscriptions": [],
  "baseUpdatedAt": "2026-08-02T09:14:11.204Z"
}

subscriptions must be an array; anything else responds 400 and stores nothing. An empty array is a valid clear, but a malformed or truncated body would otherwise normalize to an empty list and be stored as a perfectly successful replacement.

baseUpdatedAt is the updatedAt the client last read. If it does not match the stored document the hub responds 409 with the current document and writes nothing: a device showing a stale copy would otherwise erase every record added elsewhere since it last looked, and this data exists nowhere else. An empty baseUpdatedAt is accepted only against a hub that has never been written to — a hub whose list is empty but whose updatedAt is set has had its records deleted, and re-seeding it from a stale cache would undo that.

Because updatedAt doubles as the concurrency token, it is guaranteed to increase strictly on every accepted write: two writes landing in the same millisecond would otherwise share a token, and a third holding the older one would pass the staleness check against a document it never read.

Records are re-normalized on ingest exactly as POST /api/ingest normalizes device records; unknown fields are dropped and malformed records are discarded rather than stored. currency is validated against the display currencies the app carries rates for (USD, TWD, HKD, CNY); a record naming anything else responds 400 and stores nothing, because coercing it would report an amount the user never entered. A successful write responds 200 with the stored document in the same shape as GET.

An accepted write also broadcasts stats to connected stream clients with reason: "subscriptions", the same way an ingest does. That frame carries the new subscriptionsUpdatedAt, which is how the other devices learn their copy has been overtaken.