# Read every AI plan's real usage, and queue work for what's left: implementation spec for an AI agent

Source: https://jacobkunselman.com/tips/real-usage-and-a-spare-capacity-queue/ (AI tips, No. 5, 2026-10-13).
It replaces No. 3 (https://jacobkunselman.com/tips/usage-meter-and-burn-off/), which estimated Claude and Grok.

You are an AI coding agent. Build two things for the AI plans your user has (any of Claude Code, Codex, Grok Build,
Antigravity for Gemini):

1. **Readers** that take each plan's real usage and plan name from the vendor, with no tokens and no API key, every
   20 minutes, into a small state file.
2. **A queue** that spends each plan's spare weekly capacity on jobs, under a line that rises 10% a day, each job in a
   sandbox, each result a branch for review. **Build it with its timer OFF.** Whether agents run unattended is the
   user's decision: tell them what it will do and wait for a yes.

Reference files (Python 3, standard library only): `reference/plan-usage.py` (the readers), `reference/budget.py`
(the rule), `reference/sandbox.py` (the wall). Everything below marked *undocumented* is vendor internals that were
observed on 2026-10-13, not promised. Check each against the user's installed CLI before relying on it.

## Readers (undocumented)

All four use the login the user's CLI already has. **Never** use or create an API key: a key bills a metered account,
not the plan.

### Claude (Claude Code's login)

- Token: `~/.claude/.credentials.json` → `claudeAiOauth.accessToken`, with `expiresAt` (ms). Also there:
  `subscriptionType` (e.g. `max`) and `rateLimitTier` (e.g. `default_claude_max_20x`).
- `GET https://api.anthropic.com/api/oauth/usage` with headers `Authorization: Bearer <token>` and
  `anthropic-beta: oauth-2025-04-20`. This is what `/usage` calls (the path is in the CLI binary).
  Returns `five_hour` and `seven_day`, each `{utilization (percent), resets_at (ISO)}`, and `limits[]`, where a
  `kind: "weekly_scoped"` row with `scope.model.display_name` is a per-model weekly limit.
- `GET https://api.anthropic.com/api/oauth/profile`, same headers: `organization.organization_type`,
  `rate_limit_tier`, `billing_type`, `subscription_status`. ⚠ It also returns the account's name, email and ids:
  **keep only the plan fields**, never store or publish the rest.
- ⛔ **Read the token; never refresh it.** A refresh rotates the token under every running Claude Code session. If
  it has expired (or expires within a minute), skip this read; the user's next session refreshes it.

### Codex

- Start `codex app-server` (JSON-RPC 2.0 over stdio, one JSON object per line). Send `initialize` with
  `{"clientInfo": {"name": "...", "version": "1"}}`, then the notification `initialized`, then
  `account/rateLimits/read` with `{"excludeResetCreditDetails": true}`. Kill the process after the reply.
- The reply's `rateLimits.primary` and `.secondary` are `{usedPercent, windowDurationMins, resetsAt (epoch s)}`
  (300 = five hours, 10080 = the week), and `rateLimits.planType` names the plan (e.g. `plus`).
  These are the server's numbers, so they include use on other machines and in ChatGPT's apps.
- ⛔ **Never call `account/rateLimitResetCredit/consume`.** It sits beside the read and spends a reset credit.

### Grok (Grok Build)

- Start `grok agent --no-leader stdio` (ACP: JSON-RPC 2.0 over stdio). Send `initialize` with
  `{"protocolVersion": 1, "clientCapabilities": {}, "clientInfo": {...}}`, then `_x.ai/billing` with `{}`.
  ⚠ **The leading underscore matters**: `x.ai/billing` answers "Method not found".
- `result.config.creditUsagePercent` is the weekly limit's percentage; `result.config.currentPeriod` is
  `{type: "USAGE_PERIOD_TYPE_WEEKLY", start, end}`; `result.subscription_tier` names the plan. Use the period's end
  as the reset: it can move without notice, and a hand-entered reset time silently goes stale.

### Gemini (Antigravity CLI)

- `agy -p /usage --output-format json` (no model turn: `usage.total_tokens` is 0). `command.data.groups[]`, each
  with `buckets[]` of `{window: "weekly"|"5h", remaining_fraction, reset_time}`. Used = 100 × (1 − remaining).
- ⚠ A window nobody has opened reports 100% remaining with a reset of "now + its length", which slides on every
  call. Store that as no reset, not as a time.
- No plan name is reported. Keep it in a hand-edited table.
- Clear `GEMINI_API_KEY` from the environment first: with it set, agy bills the metered API.

### Between reads

Read every 20 minutes (a timer), and before any decision the queue makes. Between reads, add the tokens spent since
the read at a calibrated rate (tokens per percent), and mark the figure exact only while the read is under 30 minutes
old. Without a read, fall back to an estimate and say so.

### Plans and prices

Map the plan id each vendor reports to a name and the price the user actually pays (`tiers.tsv`:
`vendor, tier, name, price_month, source`). **A plan id that isn't in the table keeps the old price and is flagged,
not guessed.** Users change plans; the meter should notice before the user does.

## The queue

### The budget rule (`reference/budget.py`)

For each plan's weekly limit:

    day  = ceil((now − (reset − 7 days)) / 1 day), clamped to 1..7
    line = min(10% × day, 70%)
    room = line − margin − used        (margin 0 for an exact read, 5 points for an estimate)

- **The user's own use counts first.** On a heavy day the room is zero.
- A plan may spend only when: the reading is under 30 minutes old; there is a weekly reset time; the plan's
  five-hour window (if it has one) is at most 50% used; room > 0; and it's after 21:00 local, or within two hours
  of that plan's own weekly reset.
- An estimated plan also needs a human reading taken since its week began.
- Never give the queue the most expensive model (on Claude, the per-model weekly limit's model).

### A job

One file per job (so two writers can't clobber each other), all changes under one lock: `title`, `repo`, `worker`
(`claude`, `sonnet`, `codex`, `grok`, `gemini`, or `any` = the plan with the most room), `kind` (`change` or
`report`), `minutes` (5–45, default 20), `prompt` (written so it can be done with no questions), `state`
(`queued → running → review → done | rejected`, plus `failed`, `held`), `order`, `result`.

### A run (`reference/sandbox.py`)

1. One runner at a time (flock). A `running` job with no live process is `failed` on the next pass, attempt kept.
2. `git clone --shared` the repo's default branch into the job's own folder, check out `ai/<id>`, remove the remote.
3. Run the worker in **bubblewrap inside `sudo systemd-run`**:
   - bubblewrap: `--ro-bind / /`, `--tmpfs $HOME` (hides `~/.ssh`, the GitHub token, every other repo, every secrets
     file), `--tmpfs /run` (no user bus, no docker socket), `--tmpfs /tmp`, the clone read-write, the CLIs'
     binaries read-only, and the login files bound in. Bind the repo's object store read-only for `--shared`.
   - systemd scope: `IPAddressDeny=` every private range (127/8, ::1, 10/8, 172.16/12, 192.168/16, 100.64/10,
     169.254/16, fe80::/10, fc00::/7), `IPAddressAllow=` the DNS resolver only, `CPUQuota=150%`, `MemoryMax=3G`,
     `TasksMax=256`, `RuntimeMaxSec=` the job's limit.
   - Give Claude and Codex **their own config folders** (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`) with the real login file
     bound in, so a job can't plant a hook, a setting or a memory in the user's own sessions. Claude:
     `--strict-mcp-config`, `--setting-sources user`, settings `{"disableAllHooks": true}`. Make sure the meter
     scans those folders too, or queue runs go uncounted.
   - Put the runner's rules first in the prompt (`PREAMBLE`): no commit, push, deploy or services; no network but
     the internet; nothing heavy; if a decision is needed, change nothing and ask in the answer.
4. Every 5 minutes, re-read the counters; stop the worker (SIGTERM, then SIGKILL) if its plan passes line + 3.
5. **Commit inside the sandbox with no network and hooks off**
   (`git -c core.hooksPath=/dev/null -c core.fsmonitor= commit --no-verify`), because a `.git/config` the worker
   wrote would otherwise run code outside the wall. Then `git fetch file://<clone> +HEAD:refs/heads/ai/<id>` into
   the real repo, and delete the clone.
6. Write the result: branch, files, diffstat, the worker's final answer, minutes, the plan's usage before and after,
   and flags (anything that looks like a secret; changes to agent instruction files; a report job that touched more
   than its report). State → `review`.
7. **Nothing merges unread.** A reviewing session reads the branch and merges it into the checkout (never pushes),
   or rejects it with a reason and deletes the branch.

## Acceptance tests

1. Each reader returns the same percentages as the vendor's own page or `/usage`, within a point, and uses no
   tokens (check the plan's counter before and after).
2. With an expired Claude token, the Claude read is skipped and the token file is unchanged.
3. A plan id not in `tiers.tsv` is flagged and priced at the old price.
4. `budget()` on a Tuesday 22:00 with Claude's week resetting Friday at 32% used gives day 5, line 50, room 18
   (exact), and refuses at 12:00 the same day; a stale (31-minute-old) state refuses every plan.
5. Inside the sandbox: `ls ~` shows only the CLIs' folders and the clone; `~/.ssh` and `/run/user` are missing;
   a LAN address, `127.0.0.1` and the tailnet don't answer; a public HTTPS site does.
6. A job on a throwaway repo comes back as branch `ai/<id>` with the change and the worker's report; the real repo's
   checkout is untouched; nothing was pushed.
7. With the queue empty, a pass does nothing and reads nothing.
