# Quota: track balance and burn rate, not percentage of plan Status: done -- balance/burn/runway per provider **Status: FINAL — decision-complete.** Written 2026-09-04 from live data. ## The modelling error `metrics.quota_burn` reports usage as a fraction of `plan_kwh_per_period`, and `coverage.warnings` renders it as: > metered usage is 146% of the 6.25 kWh plan allowance. **A quota is a wall, > not a bill — requests fail rather than costing more.** That claim is false for this account. Usage sailed past 100% and nothing failed, because the provider bills overage against a credit balance. The warning is simultaneously alarming and unactionable: it says a wall was hit when no wall exists, and it offers no number an operator can act on. Meanwhile the authoritative signal — **the provider reports `allowance_remaining_usd` on every single response** — is captured (`config/schema.sql:156`, read at `src/dispatcher.py:1416`), persisted to `energy_observations`, and then used by exactly one thing: `seed_energy.py`, for sweep accounting. Nothing watches it. ## What the recorded data actually shows Queried 2026-09-04 from `energy_observations`: | date | calls | balance range (USD) | |---|---|---| | 2026-09-04 | 1,853 | 19.70 → **12.19** | | 2026-09-03 | 174 | 19.95 → 19.70 | | 2026-09-02 | 3,573 | 20.01 → **0.0071** | | 2026-09-01 | 3,116 | 14.34 → 4.34 | The 2026-09-02 provider outage is in the table. The balance walked down through `$0.4991 → $0.4967 → $0.4924` at 05:54:26–05:54:32 and bottomed at **$0.0071**. **The router observed the balance approaching zero, request by request, and said nothing.** Hours of warning were available and discarded. Current state: **$12.19 remaining**, burning **$7.51 over 1,853 calls in 14.3 hours** ≈ **$0.52/hour** ≈ **~23 hours of runway**. That is the sentence the dashboard should be showing. "146% of plan" is not. ## Three defects, in order of value ### 1. The wrong signal Warn on **balance and projected time-to-zero**, sourced from `allowance_remaining_usd`, not on percentage of a kWh plan. - Headline: current balance, recent burn rate, projected hours remaining. - Warn when projected runway drops below a configurable threshold (hours, not percent). Default it to something an operator can act within — a few hours. - Keep the kWh plan figure as **secondary context**. It is still the right unit for the subscription; it is the wrong thing to alarm on. **The implementation trap, and it is the whole difficulty:** a top-up makes the balance JUMP UP. On 2026-09-02 it went `0.0071 → 20.0071`. A naive `MAX - MIN` over a window reports a burn of ~$20 when the real burn was ~$20 *down* plus a $20 credit. Compute burn from **consecutive decreasing deltas only**, or segment the series at every increase and use the most recent segment. A test must cover a window containing a top-up; getting this wrong produces a confidently wrong runway estimate, which is worse than none. Handle `allowance_remaining_usd IS NULL` (older rows, and any response that omits it) by excluding those rows, not by treating them as zero. ### 2. The wrong window `quota_burn` sums `energy_kwh` over a **30-day rolling window** while the subscription resets monthly on `objective.billing_reset_day` (currently 6). Those are different periods, so the reported fraction does not correspond to the billing period it appears to describe. It also returns `reset_date = today − 30 days`, which is the rolling-window *start* named as if it were a billing reset — the same confusion `billing_reset_day` was added to fix in the admin modal. Compute the kWh figure over **the current billing period** (since the most recent `billing_reset_day`), and rename the rolling-window field so it cannot be mistaken for a reset date. Keep the rolling figure if it is useful, but label it honestly. ### 3. The wrong words `"A quota is a wall, not a bill — requests fail rather than costing more"` appears in the warning text and the same framing is in `config/config.yaml` (`max_energy_per_request`, `plan_kwh_per_period` comments) and in `CLAUDE.md`. It is demonstrably false for this account and it changes what an operator does: a wall means "stop", overage means "you are being billed, decide if that is fine." Correct all three places. State plainly that overage is billed against a credit balance, and that `plan_kwh_per_period` **gates nothing** — verified: it appears only in `metrics.py` reporting and the admin allowlist. ## Surfacing - Admin dashboard quota chip: show **balance and runway** as the headline, percentage-of-plan demoted to detail. - `/metrics` `quota` block gains the balance/burn fields alongside the existing ones. Do not remove existing keys — the TUI and admin snapshot read them. ## Non-goals - **Do not gate or refuse requests on quota.** `plan_kwh_per_period` gates nothing today and this plan does not change that. Refusing traffic because a local estimate says the balance is low would turn a billing question into an outage — and the estimate can be wrong (see the top-up trap). - Do not add a second source of truth. `allowance_remaining_usd` is the provider's own number; do not reconstruct a balance from summed `cost_usd`. - Do not touch `max_energy_per_request` behaviour. - Do not change how `energy_observations` rows are written. ## Success criteria - `/metrics` `quota` reports current balance, burn rate over a stated window, and projected hours to zero, all derived from `allowance_remaining_usd`. - A test with a top-up inside the window (balance increasing) produces a correct burn rate, not one inflated by the credit. - A test with all-NULL `allowance_remaining_usd` degrades gracefully rather than reporting a zero balance. - The kWh figure is computed over the current billing period, and no field named `reset_date` returns a rolling-window start. - The "wall, not a bill" framing is corrected in `metrics.py`, `config/config.yaml` and `CLAUDE.md`, and states that `plan_kwh_per_period` gates nothing. - The admin quota chip leads with balance and runway. - Existing `/metrics` `quota` keys still present. - Full suite green with `local_energy.enabled` both true and false. - The user's `config/config.local.yaml` is byte-identical after the run (`local_energy.enabled: true`, `tariff_usd_per_kwh: 0.159`). **Corrected 2026-09-04:** this criterion previously said the tariff lines in `config/config.yaml` must stay uncommitted. That is stale — PR #26 moved deployment values into the gitignored overlay, so `config/config.yaml` is now clean and tracked. Note the consequence for this plan specifically: §3 edits comments *in* `config/config.yaml`, which is now an ordinary tracked edit to commit deliberately, not a file to tiptoe around. The file to protect is the overlay, and it is not in git history at all. ## The pattern worth naming This is the third plan in a row whose finding is **"the signal was already recorded and nothing surfaced it"** — after the capability-gated ceiling collapse and the unread `route_decisions` rejections. The router is good at capturing evidence and poor at putting it where an operator looks. Worth considering a general review of what is persisted versus what is displayed, rather than a fourth one-off detector.