Technical reference

Throttle — Technical Reference

Throttle is a Chrome extension paired with a private backend. The extension observes the quota indicators already rendered by Claude and Lovable, converts them into a continuous time series, and syncs them to your account when paired. This page documents the full technical surface: architecture decisions, the data model, the pairing flow, and the API reference.

If you are looking for privacy disclosures, see /privacy. If you are looking for research methodology, see /research.

Architecture

Throttle is designed around one constraint: the extension should be useful without sending anything anywhere.

The default state is local-only. Snapshots are written to the extension's IndexedDB store in the browser. No account is required, no network call is made, and the dashboard at tokenmaxx.me is irrelevant until the user explicitly pairs.

Cloud sync is a second layer that activates only after explicit pairing. Once paired, the extension batches numeric snapshots and uploads them to the tokenmaxx backend over HTTPS. The transport carries numeric usage data only — no prompt content, no chat content, no DOM text outside the quota surface.

Aggregate research contribution is a third, separate toggle, off by default, revocable at any time. See /research for what that dataset includes and excludes.

Why Shadow DOM for the speedometer overlay. The speedometer UI element that attaches to Claude and Lovable pages is rendered inside a Shadow DOM root. This isolates it completely from the host page's stylesheets and scripts — the overlay cannot be styled or queried by the host, and it cannot read or mutate the host page in return. It is an instrument bolted onto the outside of the window, not wired into the interior.

Why a tolerant parser. Claude and Lovable change their UI without notice. The extension's quota reader is written as a tolerant parser: it targets the numeric surface (percentages, counters, reset timestamps) using selectors that degrade gracefully rather than fail hard when markup changes. When the parser cannot find a quota indicator, it records a gap rather than a malformed value. Gaps are visible in the dashboard and in /stats.

Why IndexedDB for local storage. chrome.storage.local has a default quota of around 10 MB and is synchronous in its API design. IndexedDB is async-first, quota is negotiated with the browser (typically several hundred MB), and it supports structured queries needed for the local graph rendering. Snapshots are keyed by (provider_id, t) and deduplicated on insert.

Multi-provider design. The data model is provider-agnostic from the first write. provider is a string field ("claude", "lovable", "other"). The same data JSONB schema accepts any quota surface. OpenAI and Gemini are on the roadmap; adding them requires a new parser module and no schema migration.

Data model

A snapshot is a single numeric read of a quota surface at a point in time. It contains no content — no prompt, no response, no code, no filename. The data field is a JSONB object whose shape is determined by the provider parser.

snapshot {
  provider:     string         — "claude" | "lovable" | "other"
  provider_id:  string         — stable upstream account identifier
  label:        string?        — user-assigned name ("Personal", "Work")
  plan:         string?        — plan label when present in the UI
  t:            integer        — Unix timestamp in milliseconds
  data:         object         — numeric quota reads (provider-specific)
}

The data object for Claude captures the percentage of the active quota window consumed, the reset timestamp when visible, and the plan identifier when present. It does not capture message content, model name, or any session metadata. The equivalent for Lovable captures the credit counter displayed in the UI.

What provider_id must be. The provider_id is the uniqueness key per upstream account. It must be derived from the upstream account, not from the browser or the extension install. A random UUID stored in chrome.storage is wrong — the same Claude account in two browsers would produce two unrelated account rows. A constant string like "default" is wrong — two different Claude accounts would collapse into one row, and collisions are silently deduplicated (data is dropped without an error). The correct value is the upstream account UUID, org ID, or — as a last resort — a SHA-256 hash of the lowercased account email.

Deduplication. Re-uploads with the same (account_id, t) are silently dropped. This makes retry safe: the extension can re-upload a batch after a network error without creating duplicate rows.

Retention. Cloud snapshots are retained indefinitely unless the user deletes them from Data Controls. Per-account retention can be configured; retention_days: null means no automatic expiry.

Pairing flow

Pairing links one browser installation to one tokenmaxx account. It is a one-time exchange: a short-lived code becomes a long-lived API key stored in chrome.storage.local.

One code per browser, one key per browser. A pairing code is single-use and expires after 15 minutes. Each browser or profile that should sync independently must go through its own pairing. Multiple browsers can pair to the same account — each gets its own API key, visible and revocable from the Access tab in the dashboard.

Three ways to complete the pairing:

  1. Manual paste. User generates a code in the dashboard, copies it, and pastes it into the extension popup.
  2. Deep link. User clicks https://tokenmaxx.me/pair#code=… (also accepts ?code=). The page broadcasts the code to the extension via window.postMessage on the same origin.
  3. DOM element. The same page exposes the code on [data-throttle-pair-code] for extensions that prefer attribute scraping over message events.

Redeeming the code

POST https://tokenmaxx.me/api/public/pair
content-type: application/json

{
  "code": "PASTE_FROM_DASHBOARD",
  "label": "Chrome desktop"
}
{
  "ok": true,
  "api_key": "tk_live_…",
  "prefix": "tk_live_…",
  "user_id": "uuid"
}

Failure modes: 404 invalid_code, 410 expired, 410 already_redeemed.

Content script: auto-capture from the deep link

// content_script.js — runs on https://tokenmaxx.me/pair*
window.addEventListener("message", (e) => {
  if (e.origin !== "https://tokenmaxx.me") return;
  if (e.data?.type !== "throttle:pair") return;
  chrome.runtime.sendMessage({ type: "PAIR", code: e.data.code });
});

const el = document.querySelector("[data-throttle-pair-code]");
if (el) chrome.runtime.sendMessage({ type: "PAIR", code: el.dataset.throttlePairCode });

Background service worker: redeem and persist

// background.js
chrome.runtime.onMessage.addListener(async (msg) => {
  if (msg.type !== "PAIR") return;
  const res = await fetch("https://tokenmaxx.me/api/public/pair", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ code: msg.code, label: "Chrome desktop" }),
  });
  const json = await res.json();
  if (!json.ok) throw new Error(json.error);
  await chrome.storage.local.set({
    throttle_api_key: json.api_key,
    throttle_user_id: json.user_id,
  });
});

Required manifest.json entries: "host_permissions": ["https://tokenmaxx.me/*"], "permissions": ["storage"], and a content script matching "https://tokenmaxx.me/pair*".

After pairing, call GET /api/public/whoami to confirm the key is valid before the first upload.

API reference

All endpoints live under https://tokenmaxx.me/api/public. Every endpoint accepts CORS preflight — the extension can call them directly from a content or background script without a proxy.

Authentication

Every endpoint except /pair requires a bearer token:

Authorization: Bearer tk_live_…

Keys are hashed at rest (SHA-256) and scoped to the issuing user. Revoking a key from the Access tab in the dashboard invalidates it immediately — the next request with that key returns 401 unauthorized.

Whoami

Cheap auth probe. Call after pairing, or whenever a saved key may have been revoked.

GET https://tokenmaxx.me/api/public/whoami
authorization: Bearer tk_live_…
{ "ok": true, "user_id": "uuid", "key_id": "uuid" }

Upload snapshots

Snapshots are append-only. Throttle upserts the account record on first upload, keyed on (user_id, provider_id).

POST https://tokenmaxx.me/api/public/snapshots
authorization: Bearer tk_live_…
content-type: application/json

{
  "provider":    "claude" | "lovable" | "other",
  "provider_id": "user@example.com",
  "label":       "Personal",
  "plan":        "pro",
  "snapshots": [
    { "t": 1738305600000, "data": { } }
  ]
}
{
  "ok": true,
  "accepted":   2,
  "duplicates": 0,
  "account_id": "uuid"
}

Constraints. 1–2,000 snapshots per request. Body capped at 1 MB (413 payload_too_large otherwise). Each data object is stored verbatim as JSONB. provider_id must be stable — see the data model section above.

Recommended client behaviour. Batch on a timer (every N seconds). Retry with exponential backoff on 5xx and network errors. Drop the batch on 400 — malformed payloads will not succeed on retry. Prompt the user to re-pair on 401.

async function upload(snapshots) {
  const { throttle_api_key } = await chrome.storage.local.get("throttle_api_key");
  const res = await fetch("https://tokenmaxx.me/api/public/snapshots", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${throttle_api_key}`,
    },
    body: JSON.stringify({
      provider: "claude",
      provider_id: await providerIdForClaude(), // see provider_id guidance above
      snapshots,
    }),
  });
  const json = await res.json();
  if (!json.ok) throw new Error(`${res.status} ${json.error}`);
  return json;
}

Read snapshots

GET https://tokenmaxx.me/api/public/snapshots
authorization: Bearer tk_live_…

Query params (all optional): account_id, provider, provider_id, since (ms, inclusive), until (ms, inclusive), cursor (ms, exclusive — for pagination), limit (1–1000, default 100). Results are sorted by t descending.

{
  "ok": true,
  "snapshots": [
    { "id": 42, "account_id": "uuid", "t": 1738305660000, "data": {}, "uploaded_at": "ISO" }
  ],
  "next_cursor": "1738305660000"
}

Batch read — multiple accounts

Pull snapshots for up to 50 accounts in a single round-trip. Use this for incremental sync instead of looping GET /snapshots?account_id=…. Results are sorted ascending by t (opposite of single-account GET).

POST https://tokenmaxx.me/api/public/snapshots/multi
authorization: Bearer tk_live_…
content-type: application/json

{
  "accounts": [
    { "account_id": "uuid-a", "since": 1738305600000 },
    { "account_id": "uuid-b", "since": 1738305600000 },
    { "account_id": "uuid-c", "since": 0 }
  ],
  "limit_per_account": 2000,
  "until": 1738400000000
}
{
  "ok": true,
  "until": 1738400000000,
  "results": [
    {
      "account_id": "uuid-a",
      "snapshots": [
        { "id": 42, "t": 1738305660000, "data": {} }
      ],
      "truncated": false,
      "next_since": null
    },
    {
      "account_id": "uuid-b",
      "snapshots": [],
      "truncated": true,
      "next_since": 1738350000000
    }
  ]
}

since is exclusive (ms); 0 returns full history within retention. Per account: t > since AND t <= until, ordered ASC, capped at limit_per_account (1–5000, default 2000). The top-level until is echoed back — use it as the next since floor on subsequent calls so writes that arrive mid-request are not missed. truncated: true means the account has more data; resume with since = next_since for that account. Unowned or unknown account IDs are silently dropped (no 403). Body capped at 64 KB.

List accounts

GET https://tokenmaxx.me/api/public/accounts
authorization: Bearer tk_live_…
{
  "ok": true,
  "accounts": [
    {
      "id": "uuid",
      "provider": "claude",
      "provider_id": "user@example.com",
      "label": "Personal",
      "plan": "pro",
      "retention_days": null,
      "created_at": "ISO",
      "snapshot_count": 1234,
      "latest_t": 1738305660000
    }
  ]
}

Export (NDJSON stream)

GET https://tokenmaxx.me/api/public/snapshots/export?account_id=uuid
authorization: Bearer tk_live_…

Returns application/x-ndjson. Streamed in pages of 1,000, newest-first. Suitable for piping into jq, DuckDB, or any NDJSON-aware tool.

curl -s -H "authorization: Bearer tk_live_…" \
  "https://tokenmaxx.me/api/public/snapshots/export?account_id=uuid" \
  | jq '.data'

Multiple accounts and browsers

One Throttle user can own many upstream accounts (e.g. two Claude logins) and run the extension in many browsers simultaneously.

Two browsers, same upstream account. Each browser pairs once (two API keys, both valid). Each upload uses the same provider_id derived from that upstream account. Throttle records one account row, and both browsers contribute snapshots to the same stream. Users can rename the account from the Sync tab.

Two browsers, two upstream accounts. Each browser pairs once. Each upload uses a different provider_id. Throttle records two account rows under the same provider. Both are visible in the dashboard and have independent snapshot streams.

Deriving provider_id correctly. Use the most stable identifier the upstream surface exposes, in this order:

  1. Account or user UUID from an authenticated API endpoint.
  2. Org ID + member ID.
  3. SHA-256 of the lowercased email — only as a last resort, and only if the email will not change.
async function providerIdForClaude() {
  const me = await fetch("https://claude.ai/api/account").then(r => r.json());
  return me.account_uuid;
}

Never use crypto.randomUUID() stored in chrome.storage, the literal string "default", or any browser-local or install-local value. The failure modes are silent and hard to recover from: browser-local IDs produce duplicate account rows; constant IDs cause data to be dropped without error.

End-to-end flow

  1. User opens the dashboard, generates a pairing code.
  2. Extension picks up the code via postMessage, DOM element, or manual paste.
  3. Background worker POSTs to /api/public/pair, stores api_key in chrome.storage.local.
  4. Extension calls GET /api/public/whoami to confirm.
  5. On a timer, extension batches snapshots and POSTs to /api/public/snapshots.
  6. To rehydrate local graphs across many accounts, extension calls POST /api/public/snapshots/multi once per sync tick (ascending order, single round-trip).
  7. Dashboard and scripts read with GET /api/public/snapshots or stream via /snapshots/export.

Error reference

All endpoints return a uniform envelope. ok: true on success; ok: false + error code on failure. Treat unknown error codes as fatal.

{ "ok": false, "error": "unauthorized", "detail": "optional human-readable string" }
StatusErrorWhen
400invalid_bodyBody failed schema validation
401unauthorizedMissing, invalid, or revoked bearer key — re-pair
404invalid_codePairing code not found
410expiredPairing code older than 15 minutes
410already_redeemedPairing code already used
413payload_too_largeBody exceeds 1 MB
500query_failed / insert_failedServer-side DB error — retry with exponential backoff

Auditing this extension

Throttle is open source. The repository is at github.com/lucioamorim/throttle. The build is reproducible. You can inspect what the extension reads before installing it, build it locally against the same backend once paired, and verify that no content surface is reachable from the parser code.

The API schema above is the complete contract. There is no undocumented endpoint, no analytics pixel, and no third-party SDK in the extension bundle. If the schema here and the code in the repository diverge, that is a bug — file an issue.

Contributing

Parser maintenance is the most frequent contributor path. When Claude or Lovable updates their UI and breaks the quota reader, a fix usually means updating selectors or tolerant fallbacks in the parser module. The repository README has instructions for running the extension locally against a development backend.

Bug reports, parser fixes, and new provider implementations are welcome via pull request. Feature proposals that require storing content — any content — will be declined. That constraint is not a roadmap item; it is the architecture.