Story 2172: a firm can only spend so much before we stop the meter — and only staff can lift the brake

WHY. The catastrophe this Story prevents is a runaway firm burning through real OpenRouter dollars overnight with nothing to stop it. The credit gate (D-13) already turns away a firm with zero balance and no paid overage. But a firm with overage enabled is explicitly allowed to run negative — that negative is metered debt we reconcile later. Nothing bounds how far negative it goes. One misbehaving agent loop, one pathological matter, and a ₹5,000-cap firm quietly racks up ₹80,000 of our inference spend before any human looks. The platform-wallet guard (S5) catches our account going dry, but that is platform-wide and fires far too late for one firm. Story-2172 (RFD-BILL-001 S4 / §5, goal G5) adds the missing per-firm brake: a warning as the firm nears its own ceiling, a hard stop AT it with a reason you can tell apart from every other block, and a staff-only, time-boxed override for the “yes, let this one firm through tonight” case.

🛰 Satellite — the one sentence

A pure cap_gate compares a firm’s metered overage debt against its configured ceiling (firm_subscription.extra_usage_cap_inr), warns at cap_warn_pct of it (config, S1), hard-stops AT it with the distinct terminal reason cap_reached, and is lifted for a firm whenever a staff 'adjustment' row carries a live expires_at — the whole thing wired into the clerk worker right after the credit gate and reading only from config, never a hardcoded number.

✈️ Airplane — a prepaid meter with a low-fuel light and a manager’s key

Picture the firm’s overage as a car on a prepaid toll road. The credit gate is the entry barrier: no balance, no entry. But an overage firm is waved through on credit — and without S4 the road has no end-of-tether. This Story installs a fuel gauge with two marks. At 60% of the tank (the config warn line) a dashboard light comes on — the car keeps driving, we just note it. At the ceiling the barrier drops: new trips are refused, and the refusal slip says exactly “spend cap”, not “out of credits” or “our wallet is dry” — so support reads one word and knows the remedy. And there is a manager’s key: staff can grant the firm headroom that lifts the barrier until a stamped expiry. Turn the key, the firm drives tonight; the key expires on its own, the barrier comes back — no one has to remember to re-lower it.

🔬 Microscopic — the four moving parts

1. What the cap measures — overage debt vs. a per-firm ceiling

Two numbers that already existed, paired for the first time. The ceiling is business.firm_subscription.extra_usage_cap_inr — per-firm, RLS-scoped, in ₹ (1 credit = ₹1). The spend is the firm’s metered overage debt: how far its balance has gone below zero (D-13). overage_debt_millicredits(balance) is max(0, -balance) — zero while the firm is still in credit, positive only once overage has pushed the top-up bucket negative. So the cap bites only in overage territory, exactly where the credit gate stops caring.

2. The pure decision — cap_gate in credits.py, no I/O

Four inputs, one CapDecision, unit-tested without a database:

cap_credits <= 0            → allow, no warn   (no ceiling configured — gate INERT)
override_active             → allow, no warn   (staff lifted the cap; see part 4)
overage_debt >= ceiling     → BLOCK, reason "cap_reached"
overage_debt >= warn_pct%   → allow, warn=true (non-blocking signal)
otherwise                   → allow

The inert-when-unset rule is the safety property: a firm with no positive extra_usage_cap_inr (the server default is 0) is never newly blocked by this Story — the cap only ever bites a firm someone deliberately gave a ceiling. cap_reached is a named constant, deliberately distinct from no_credits / cost_cap / consent / platform_wallet_low so the block is attributable to the firm’s own spend and never confused with the platform wallet.

3. Where it runs — one bounded read, right after the credit gate

_cap_gate in the clerk worker opens one firm-scoped session and does a small, near-constant set of reads (DR-1): the firm’s balance (from the projection the credit gate just granted — no second lazy grant), its ceiling (firm_subscription), the config warn % (billing.pricing_config.cap_warn_pct, falling back to the module default when un-seeded, the same fallback-only posture as the wallet floor), and whether a live override exists. It runs only for firms that cleared the credit gate — i.e. positive balance or paid overage — so it is precisely the thing that stops a runaway overage firm. The warn is a non-blocking signal emitted whenever metering is on (pramaan.billing.credit_cap_warning); the block honours the same credit_gate_enforced flag as the credit gate, and stamps a terminal run_failed with stop_reason=cap_reached plus a distinct retry-false user message pointing an admin at the remedy. Reads are never touched — only agent runs.

4. The override — the S3 writer, now time-boxed

“Lift the cap for that firm until expiry” reuses the S3 record_adjustment writer rather than inventing a new one. It gains one optional argument, expires_at, written straight onto the credit_ledger.expires_at column that S6 already added. A staff headroom grant carrying a future expiry does two reinforcing things: the grant itself raises the balance (real, append-only credits — grants never un-happen), and has_active_cap_override — a single bounded WHERE expires_at > now() read — reports the cap as lifted until that instant. This is the first runtime consumer of expires_at: the expiry compares server-side, so the lift ends exactly on time with no sweep job to run and nothing to remember to undo. The admin route echoes expires_at back so the grant form confirms what it set.

What was deliberately not built

SkippedWhy it’s correct to skip
A new migration Every column this Story needs already ships: pricing_config.cap_warn_pct (0158/S1) and credit_ledger.expires_at (0160/S6). alembic check stays clean — no schema change, so none was invented.
An expiry sweep that decrements the projection when an override lapses The override’s runtime effect is a live expires_at > now() read, not a balance mutation, so the cap re-applies the instant the row expires with no background job. The granted credits are real and stay (append-only); only the cap lift is time-boxed.
Entangling with the platform-wallet guard (S5) S5 is platform-level (our OpenRouter balance); S4 is per-firm (the firm’s own ceiling). They stay orthogonal gates with distinct reasons so a block is never mis-attributed.
A dedicated manual span for the cap gate The gate’s reads are covered by the existing SQLAlchemy DB spans (T1) inside the clerk worker’s run span; the block/warn emit low-cardinality counters. Same instrumentation posture as the sibling credit/wallet gates — no new ADR-124 registry span needed.

How it was proven (system boundary, not just green units)

One sentence to remember. The per-firm cap is a prepaid meter with a low-fuel light and a manager’s key: warn at 60% of the firm’s own ceiling, hard-stop AT it with a reason you can name, and lift it only by a staff grant that expires on its own.