# Blueprint: Usage Panel Overhaul (settings + admin + platform), budget-% model, inline add-ons
# resolved-cell: full·dynamic  regime: xhigh
## Created: 2026-06-15
## Branch: main
<!-- ← Back to brief (current-plan-brief.md) — the plain-language owner brief; this full plan is for the executing agents -->

> PLANNING ONLY. Nothing in this plan is built yet. Owner has approved the scope via the Phase-2g decision gate (4 answers, see Companion-surface decisions). This document is the build contract for `/implement` and the cross-reference for `/verify`.

---

### Owner-ratified decisions (Phase 2g gate, 2026-06-15)
1. **Token cleanup = FULL RETIRE.** Remove all raw-token UI; delete the token-based per-user allocator; STOP token enforcement on the AI hot path (AI governed solely by the `ai_cost_cents` budget + per-user cents allocation). Keep `max_ai_tokens_per_month` + the legacy `ai_tokens` Redis counter as observability-only (never read for display or enforcement).
2. **Inline add-ons = ALL FOUR.** AI credit top-up (packs), extra seats (power/collaborator), overage/auto-reload, enrichment (framed as the power-seat lever — no standalone SKU).
3. **Usage history = ADD AI spend.** New `ai_cost_cents` snapshot column on `usage_meters` + a writer in the reconcile cron + a short backfill, so history shows AI budget % per past cycle.
4. **Org-panel scope = REBUILD BOTH.** Full `/admin/usage` rebuild AND realign the platform-console tenant-usage view (`/platform/tenants/[orgId]`) to the budget model.

---

### Companion-surface decisions
- **Access & roles (always) — APPLIES.** No NEW permission keys. Reuse `usage.view_own` / `usage.view_team` / `billing.view` / `billing.buy_credits` / `billing.manage_subscription` / `billing.redistribute`. Three RBAC consistency FIXES land here: (a) unify the four `/api/admin/usage/*` allocation routes on `billing.redistribute` (today three use the deprecated `withManagerAuth → admin.users`); (b) `canSeeDollars` keys on `billing.view` not the `super_admin|admin` role-slug; (c) enforce team scope on org-level reads so a `team`-scoped manager sees only their team, not the whole org roster. Full Role Access Matrix below.
- **Plan tier & limits (always) — APPLIES.** Panel renders the ENFORCED `getPlanLimits(orgId)` (SSOT) — NOT marketing literals, NOT `max_ai_tokens_per_month`. No new `FeatureKey`. Cold-outreach rows Ultra-only; seat add-ons Business+ (Pro has no seat add-on). Correctness fix: `getDefaultLimits()` drift (Pro emails 1000→2000, storage 2GB→15GB, enrichments 15→20). `null`=Unlimited sentinel honored; cold columns stay fail-closed `?? 0`.
- **AI assistant & agent tools (always) — N/A (no tool needed).** Usage is a passive read/spend surface; there is no model-driven write action that belongs in `executeToolService`. (A read-only "what's my AI budget left" answer is already covered by the assistant reading context; no new tool registered.)
- **Marketing & pricing (always) — N/A for copy.** No price/limit change (the three price sources are already in lockstep with DB via the held `20260609/20260612/20260615` migrations). The ONLY marketing-adjacent item is a NEW drift-guard test asserting `tokens.ts` LIMITS == seeded `plan_tiers` so a future marketing/migration edit can't silently drift the panel's denominators.
- **Help-Center docs (always) — APPLIES.** Update `/docs/settings/usage`, `/docs/admin/billing/usage`, `/docs/admin/billing/credit-packs`; add an "AI budget & add-ons" article; refresh `lib/docsIndex.ts` keywords (budget, percentage, credits, seats, overage, unlimited).
- **Design playbook (always, WARN) — APP playbook** (`docs/design/APP_REFINEMENT_PLAYBOOK.md` + `components/CLAUDE.md`). Both surfaces are product chrome under `/settings`, `/admin`, `/platform`. Top anti-slop clauses folded as Acceptance: single cobalt accent (kill the 6-hue `CategoryBreakdown`), semibold-not-bold tabular KPI values, one continuous panel not 4 tabs / not double-`.card`, color=status only, unlimited-as-quiet-label-no-bar, no donut/gauge, sentence-case.
- **Navigation (always) — N/A.** `/settings/usage`, `/admin/usage`, `/platform/tenants/[orgId]` all already in nav. One FIX: gate the AdminSidebar `/admin/usage` link through `SIDEBAR_PERMISSIONS`/`usage.view_team` (today shown by an `isAdmin` flag, so visibility can diverge from the page gate).
- **Notifications (conditional) — APPLIES.** Usage threshold alerts already exist (`cron/check-usage-alerts`). Verify/realign the AI alert to fire on the **budget %** (50/80/100), not the vestigial token cap.
- **Settings / preferences (conditional) — APPLIES (existing).** `profiles.usage_view_preference` ('percent'|'detailed') already persisted by `DollarToggle`; preserved.
- **Global search (conditional) — N/A.** No findable record added.
- **Metering / credits (conditional) — APPLIES.** New `usage_meters.ai_cost_cents` snapshot (history). Credits/overage already metered; no new `MetricName` for live enforcement (AI stays `ai_cost_cents` via `reserveCostCents`).
- **Export / import (conditional) — N/A.** Usage is not a tabular CRUD entity; no `ENTITY_CONFIGS` row. (A "download usage CSV" is explicitly OUT of v1 scope.)
- **Scheduled jobs / cron (conditional) — APPLIES.** `cron/reconcile-usage` gains the `ai_cost_cents` snapshot writer; `cron/check-usage-alerts` gains budget-% alignment. No NEW cron route, so no `CRONS[]` entry needed (existing routes already registered).

**Cohesion verdicts (per Phase 1f):** every change EXTENDS an existing capability — no new parallel model.
- AI budget meter → `EXTEND CostUsagePanel` / `cost-budget` route (already the correct % surface).
- Grouped entitlement rows → `EXTEND UsageMetricsOverview` / `usage/detail` (the shared twin).
- Per-user $ allocator → `EXTEND AdminAllocationsPanel` (keep), DELETE the duplicate token `AllocationsTab`.
- AI history → `EXTEND usage_meters` (+1 column), not a new table.
- Inline add-ons → `EXTEND` existing `BuyCreditsModal` / `OverageSettings` / `add-seat` route (wire the orphaned endpoint to UI for the first time).
- `NET-NEW`: none. `PARALLEL-JUSTIFIED`: none.

**Ripple map (per Phase 1g) — corrected by self-review:**
- `MUST-PROPAGATE`: TWO token-enforcement hot paths — `enforcement.ts getHighestPriorityError` AND `lib/ai/agentExecutor.ts:194` (`checkUserAllocation(...,'ai_tokens')`, the agent-run blocker); the metric→column maps pointing `ai_tokens → max_ai_tokens_per_month` (`api/billing/usage/route.ts` COUNTER_METRICS loop, `lib/shell/fallbacks/billingFallback.ts`, `api/platform/tenants/[orgId]/usage/route.ts` COUNTER_METRICS, `api/admin/usage/allocations/route.ts` org_limits); the raw-token UI consumers (`components/banners/MessageChip.tsx:37` home AI "Top up" chip, `app/admin/billing/BillingClient.tsx:139` USAGE_METRICS "AI Tokens" row, settings `PersonalMeter`); platform-console tenant usage + the platform `tenants/[orgId]/route.ts` `max_ai_tokens` override editor (Q4); `getDefaultLimits()` drift; `reconcile-usage` (source RPC `get_org_usage_snapshot` has NO AI-cost output — see Item A1) + `check-usage-alerts` + `reset-usage` crons; `user_usage_allocations` CHECK (8 values, not 3) + `VALID_METRICS`; the org `cost_budget` Redis cache (`invalidateOrgCostBudgetCache`) on allocation/seat writes.
- `VERIFY-UNAFFECTED`: marketing pricing pages (in sync; only add a guard test); the legacy `/api/billing/usage` payload shape — KEEP the `usage.ai_tokens` wire key (dormant) for byte-stability and ADD `usage.ai_cost_cents`, rather than deleting a key (Item 7). NOTE: `components/billing/UsageBanner.tsx` does NOT exist on disk — the live consumer is `components/banners/MessageChip.tsx`.
- `CONFLICT→PRODUCT-CALL`: token-enforcement retirement (RESOLVED by owner → full retire; preserve the intentional free-tier `MAX_SAFE_INTEGER` uncapped bypass — Item 6); overage-past-100% display (RESOLVED 2026-06-15 by owner → past 100% the panel shows extra spend ONLY when BOTH `overage_enabled=true` AND `credit_balance_cents > 0`; otherwise it's a HARD STOP at 100% with "Budget reached — enable overage or upgrade". Overage is OFF by default. This mirrors the existing `tryConsumeOverage` enforcement gate (`overage_enabled` AND credits). Item B4).
- **Projection line (RESOLVED 2026-06-15 by owner → BUILD it, with the underuse guard):** show "On pace to use your budget by ~MMM DD" ONLY when the run-rate projects exhaustion STRICTLY BEFORE the cycle reset date. If projected exhaustion is at/after the reset (the user is underusing), show calm copy ("Well within budget this cycle") — NEVER a "finish by" date later than the reset. Item D8.

---

## Plan Items

### A. Database & data model

1. **[Backend/Migration] Populate the EXISTING `usage_meters.ai_cost_cents_used` snapshot (do NOT add a new column).**
   - CORRECTION (self-review): `usage_meters` ALREADY carries `ai_cost_cents_used INTEGER NOT NULL DEFAULT 0` (added by `20260416000000_cost_budget_seat_types.sql`). The gap is a **missing writer + missing source + missing reads**, NOT a missing column. Do not add `ai_cost_cents`; reuse `ai_cost_cents_used` (widen `INTEGER→BIGINT` only if a cycle could exceed ~21M cents; idempotent `ALTER ... TYPE` guarded). The `_used` suffix matches the existing `ai_tokens_used`/`enrichments_used` naming.
   - The reconcile cron upserts `usage_meters` straight from RPC `get_org_usage_snapshot`, which has **no AI-cost output** (returns `ai_tokens_used` only). Fix the SOURCE: either (a) change `get_org_usage_snapshot` to also return `ai_cost_cents` (`SUM(ai_usage.cost_cents)` for the period), OR (b) have the cron compute `SUM(ai_usage.cost_cents)` per org+period inline and write it on the upsert. State the PERIOD explicitly: the reconcile snapshot is **calendar-month** (`date_trunc`), whereas the live AI budget anchors to the **subscription cycle** — the history column represents the calendar-month spend; the live % meter stays anchor-cycle (do not conflate them).
   - Short backfill from `ai_usage` (SUM `cost_cents` by org + calendar period) for past `usage_meters` rows where derivable; rows with no derivable cost stay 0.
   - Extend the EXPLICIT `usage_meters` SELECT column lists in BOTH `app/api/billing/usage/history/route.ts` and `app/api/platform/tenants/[orgId]/usage/route.ts` to include `ai_cost_cents_used` (adding the column alone does not surface it). `reset-usage` must also zero `ai_cost_cents_used` (it zeroes `ai_tokens_used` today) so a forced reset doesn't desync history/budget.
   - **Acceptance:** no NEW column added; `ai_cost_cents_used` is written by the cron each cycle and SELECTable by both history routes; past cycles show non-negative AI spend (0 where un-derivable); a platform reset-usage zeroes it too; the calendar-vs-anchor period distinction is documented in the migration comment.
   - **Files:** `supabase/migrations/2026XXXX_usage_meters_ai_cost_widen_and_snapshot_rpc.sql` (widen if needed + `get_org_usage_snapshot` change), `app/api/cron/reconcile-usage/route.ts`, `app/api/billing/usage/history/route.ts`, `app/api/platform/tenants/[orgId]/usage/route.ts`, `app/api/platform/tenants/[orgId]/reset-usage/route.ts`.
   - **Edge cases:** boundary/empty → cycle with 0 AI spend renders "0% / $0", no crash · failure → backfill failure leaves DEFAULT 0, never blocks the additive migration · race → cron upsert idempotent per (org,period) · permission → N/A (server) · related-entity → `ai_usage` rows missing `cost_cents` contribute 0 · input-validity → INTEGER overflow guarded by the widen.

2. **[Backend/Migration] Allocation model cleanup — STOP WRITING the token metric (do NOT narrow the CHECK to 2 values).**
   - CORRECTION (self-review): the live `user_usage_allocations.metric` CHECK allows **EIGHT** values (`ai_tokens, ai_cost_cents, enrichments, emails_sent, import_rows, export_rows, storage_bytes, lead_captures` — `20260416000000` lines 311-320), NOT three. A migration that "narrows to keep cents + enrichments" would silently DROP six valid metrics and break any allocation rows for emails/imports/exports/storage/lead_captures. So: **do NOT shrink the CHECK.** Behavior-only change: stop WRITING `metric='ai_tokens'` from the UI/routes; leave `ai_tokens` present-but-dormant in the CHECK and any existing rows untouched (PRE-LAUNCH: dormant, not deleted). Remove `ai_tokens` from the route's `VALID_METRICS` write allowlist so a write is rejected, while the CHECK keeps all 8 for the dormant rows.
   - **Acceptance:** no code path writes `metric='ai_tokens'`; the CHECK still admits all 8 values (no narrowing); the cents allocator AND the enrichment allocator both still function (enrichment lives in the unified panel per Item D10); `VALID_METRICS` (write allowlist) excludes only `ai_tokens`.
   - **Files:** `app/api/admin/usage/allocations/route.ts` (`VALID_METRICS`, `ALLOCATION_LIMIT_COLUMN`); a migration only if the dormant `ai_tokens` rows must be reconciled — otherwise NO migration (the CHECK is unchanged).
   - **Edge cases:** related-entity → dormant `ai_tokens` allocation rows must not affect enforcement once Item 6 lands · permission → unchanged · input-validity → `VALID_METRICS` rejects `ai_tokens` on write after cutover; emails/imports/etc. metrics stay writable.

3. **[Backend/Migration] Fix the per-user storage query — DONE INSIDE the single Item B5 RPC rewrite (not a separate migration).**
   - CORRECTION (self-review): Items A3 + B5 both `CREATE OR REPLACE get_per_user_usage_summary`; two separate migrations would race/clobber and risk dropping the `20260423` SECURITY DEFINER auth guard. **Merge into ONE migration** (Item B5). The storage-scope fix is one of the changes that ONE rewrite makes: the storage LATERAL sums `attachments.file_size` WHERE `uploaded_by = p.user_id` with NO `organization_id` and NO period filter → all-time + cross-org leak; add `AND a.organization_id = p_organization_id` and a period bound.
   - **Acceptance:** per-user storage is org-scoped and period-bounded; no cross-org bytes; delivered in the single B5 RPC migration, not a competing one.
   - **Files:** (folded into Item B5's single RPC migration).
   - **Edge cases:** related-entity → user with 0 attachments → 0 bytes · permission → the merged rewrite re-asserts the `20260423` hardening byte-identical (search_path, caller-binding, REVOKE PUBLIC/anon, GRANT) so the guard is never silently dropped.

### B. The shared AI-budget + entitlements surface (read model)

4. **[Backend] Make `cost-budget` + `usage/detail` the single AI/limits contract; expose budget % at ORG level; fix `canSeeDollars` plumbing + the overage display field.**
   - `/api/billing/cost-budget` is genuinely org-scoped (reads `profile.organization_id`, no tenant param — confirmed), so `/admin/usage` can mount the SAME org budget meter. Strip `token_estimate` from the payload + the `CategoryBreakdown`/`CostUsagePanel` prop types (dead field — confirmed present).
   - **Overage display (corrects B6; owner-locked 2026-06-15):** `percentage_used` is `Math.min(100, …)` HARD-CAPPED and `stripDollarFields` removes every cents field for non-`billing.view` callers — so a non-admin currently CANNOT render any past-100% state. Add to the payload, surviving `stripDollarFields`: (a) a boolean `overage_active` = (`overage_enabled` AND `credit_balance_cents > 0`), and (b) a non-$ `overage_credits_used_pct` (overage credits consumed as a % of the loaded credit balance, so no raw $ leaks). RULE: the panel shows the "+ credits" line PAST 100% **only when `overage_active` is true**; when false (overage off — the default — or zero credits) the 100% state is a HARD STOP reading "Budget reached — enable overage or upgrade" (matches `tryConsumeOverage`, which only draws credits when `overage_enabled` AND a positive balance). For admins (`billing.view`) the $-detailed view shows "100% of plan budget + $Y in credits used" with remaining credit headroom. Never imply overage is on when it is off.
   - **`canSeeDollars` → `billing.view` (corrects the signature gap):** `lib/billing/visibility.ts canSeeDollars(profile, org)` keys on a role-slug Set and takes NO permission map. Change its signature to accept a resolved `can('billing.view')` boolean (or a perms map); switch `/api/billing/cost-budget` from `withAuthCached` to `withPermissionCached('billing.view')` (or thread a resolved perm) so the $-gate has the permission; enumerate every `canSeeDollars`/`stripDollarFields` caller and the TWO server prefetchers that independently compute it from role-slug — `app/settings/usage/page.tsx:63` and `app/admin/usage/page.tsx` — and replace those with the `billing.view` derivation so page-prop and API gate use ONE predicate (a `sales_manager` has `billing.view=all` and MUST see $).
   - `/api/billing/usage/detail`: it is ALREADY on the budget % model — the AI row is `ai_budget` in PERCENT domain; there is **no token-denominated row to remove** (correction). Keep the 5 groups + `null`=Unlimited + "exports always unlimited"; ADD `contacts` + `companies` rows (absent today) — note `MetricGroup` is a CLOSED typed union, so assign them to an existing group (e.g. `storage_objects` or a data group) or extend the union. Keep one canonical reset/period source (collapse the two reset displays).
   - **Acceptance:** non-admin gets percentages only (no cents, no denominator, no tokens) AND can still render overage via the non-$ overage field; admin can toggle $; contacts/companies appear as "Unlimited" rows on paid tiers; one reset date; `token_estimate` gone from payload + types; the page prop and API use the SAME `billing.view` predicate (a `sales_manager` sees $).
   - **Files:** `app/api/billing/cost-budget/route.ts`, `app/api/billing/usage/detail/route.ts`, `lib/billing/visibility.ts` (signature change), `app/settings/usage/page.tsx`, `app/admin/usage/page.tsx` (both page $-gates), `components/billing/CategoryBreakdown.tsx`, `components/billing/CostUsagePanel.tsx`.
   - **Edge cases:** boundary → 0% and >100% (overage via the non-$ field) both render · failure → cost-budget RPC error → panel shows a retry, never a fabricated 0 (today `CostUsagePanel` returns `null` on error — give it a retry, esp. on the admin mount per Item 10) · race → cycle rollover uses the anchored period · permission → `billing.view` is the SINGLE $-visibility predicate across page + API · related-entity → free tier ($0 budget) renders "AI not included on your plan — upgrade" not "0/0" · input-validity → N/A.

5. **[Backend] Per-user AI = budget share, not tokens — ONE consolidated `get_per_user_usage_summary` rewrite.**
   - Rewrite `/api/settings/usage/personal` to return the user's AI as **% of their share of the org budget** (or "$ spent" for `billing.view`), sourced from `get_user_ai_usage_cents_bulk` (takes `p_user_ids[]`, org-scoped, **service-role-only via `createAdminClient` with an explicit `.eq('organization_id', orgId)`** — keep it that way; never call it from a session client) + the org `pool_cents` (`allocations-overview` already computes this). Drop the `ai_tokens` block + `max_ai_tokens_per_month` denominator. Shared-pool user → "You share the organization's AI budget" with the org-level %.
   - **ONE migration** `CREATE OR REPLACE get_per_user_usage_summary` (consolidating Items A3 + B5 + pagination + Item 12's role-gate) based on the hardened `20260423` version, making ALL of these changes in a single authoritative definition: (a) the AI LATERAL sums `ai_usage.cost_cents` (not `prompt_tokens+completion_tokens`); (b) `ORDER BY` switches from `ai.tokens DESC` to cost desc; (c) the storage LATERAL gains `organization_id` + period scope (Item A3); (d) ADD `p_limit`/`p_offset` params and apply `LIMIT/OFFSET` inside the `json_agg` subquery — the route ALREADY passes `p_limit`/`p_offset` but the deployed 2-arg signature silently ignores them (unbounded `json_agg` over every org user with 6 LATERALs each = the 500-user scale blocker); (e) relax the INTERNAL role-slug guard (`role IN ('admin','super_admin','manager','sales_manager')` at `20260423:1061`) to honor `billing.redistribute` OR drop it and rely on the route's `withPermission` gate + org-binding (else a `billing.redistribute` custom role 42501s here — Item 12); (f) re-assert `SECURITY DEFINER SET search_path=public, pg_temp` + `REVOKE FROM PUBLIC, anon` + `GRANT TO authenticated, service_role` + the `auth.uid()` caller-binding byte-identical so the rewrite never re-opens the closed anon cross-tenant read.
   - **Acceptance:** no endpoint returns `ai_tokens` as an AI denominator; per-user AI is $/% of budget; default sort = AI spend desc; the RPC accepts + honors `p_limit`/`p_offset`; storage is org+period scoped; the SECURITY DEFINER guard + grants are byte-identical to `20260423`; a `billing.redistribute` custom role is NOT 403'd at the RPC; shared-pool users see a sensible message.
   - **Files:** `app/api/settings/usage/personal/route.ts`, `app/api/admin/usage/breakdown/route.ts`, `supabase/migrations/2026XXXX_get_per_user_usage_summary_cost_pagination_scope.sql` (the SINGLE RPC migration — also satisfies Item A3 and the RPC half of Item 12).
   - **Edge cases:** boundary → user with $0 AI spend → "0% / $0" · empty → org with no AI usage → empty "top spenders" calm empty state · permission → non-`billing.view` sees % not $; service-role-only RPCs stay on `createAdminClient` · related-entity → deleted user's residual usage handled · scale → RPC now bounded by `p_limit`/`p_offset`; the CLIENT must stop ignoring `data.pagination`.

### C. Token full-retire (enforcement + maps)

6. **[Backend] Stop token enforcement on BOTH AI hot paths.**
   - CORRECTION (self-review): there are TWO token-enforcement hot paths, not one. (1) `lib/billing/enforcement.ts getHighestPriorityError` (lines ~1030-1031, 1049-1061): retire the `ai_tokens` `checkUsageLimit` + `checkUserAllocation` branches + the `ORG_USAGE_LIMIT_EXCEEDED` "AI token limit" copy. (2) `lib/ai/agentExecutor.ts:194` independently runs `checkUserAllocation(organizationId, ctx.userId, 'ai_tokens')` and HARD-BLOCKS the agent run on `!allowed` with "Personal AI token budget exceeded." — drop this branch (or repoint to the cents allocation) or every automation/agent run stays token-throttled (Smoke Test 4 fails). Grep-verify EVERY `checkUserAllocation('ai_tokens')` / `checkUsageLimit('ai_tokens')` callsite is retired. Keep the legacy `ai_tokens` Redis increment (observability) but never read it for a gate.
   - **Fail-open guard (self-review):** assert the change does NOT fail-open: `reserveCostCents` (Lua) returns `BUDGET_ZERO` for budget≤0, so $0-budget PAID orgs are still denied AI. The free-tier `MAX_SAFE_INTEGER` uncapped bypass (`getAiEnforcementResult` ~lines 1131-1156) is intentional + gated to internal-allowlist free orgs — preserve it AS-IS, and add a regression assertion that a Pro/Business/Ultra org with `ai_cost_cents_monthly=0` is DENIED (not uncapped) so a future seed can't turn the only remaining gate off.
   - **Acceptance:** no AI call (chat OR agent) is ever blocked by a token cap or token per-user allocation; the only AI blocker is the budget/overage message; an org with a low vestigial `max_ai_tokens_per_month` is NOT throttled; a $0-budget PAID org IS denied; the free-tier bypass is unchanged; existing budget enforcement tests still pass.
   - **Files:** `lib/billing/enforcement.ts`, `lib/ai/agentExecutor.ts`, `lib/billing/usageTracking.ts` (keep increment, drop the AI token read).
   - **Edge cases:** boundary → user previously token-blocked can now use AI (chat + agent) · failure → budget RPC error path unchanged (fail-closed) · race → concurrent AI calls serialize on the cents reservation · permission → N/A · related-entity → dormant token allocation rows (Item A2) have no effect · DEFER-TO-HUNT: any non-AI/observability consumer still reading the `ai_tokens` Redis counter (grep-verify; keep the increment if a QA tool reads it).

7. **[Backend+Frontend] Token metric→column maps + the raw-token UI consumers — keep the wire key, gain `ai_cost_cents`, repoint consumers.**
   - CORRECTION (self-review): removing `ai_tokens` from `app/api/billing/usage/route.ts`'s `COUNTER_METRICS` loop DELETES the `usage.ai_tokens` response key = a shape change for a broadly-consumed payload (AuthenticatedChrome shell, automations, campaigns/compose, reports, BillingClient). **Chosen path: keep the wire key byte-stable, stop CONSUMING it, and ADD a budget key.** Specifically: leave `usage.ai_tokens` present (dormant, e.g. 0%/null-limit) so the shape is stable, AND make `/api/billing/usage` EMIT `usage.ai_cost_cents { current, limit, percentage }` (the budget %), so the home-page AI alert chip still has a source. Repoint `lib/shell/fallbacks/billingFallback.ts` + `app/api/admin/usage/allocations/route.ts` (`org_limits.ai_tokens`) off `max_ai_tokens_per_month`.
   - **Raw-token UI consumers (must repoint, not just the feed):** `components/banners/MessageChip.tsx:37` fires a home AI "Top up" chip keyed on `ai_tokens` at 90% — switch it to the `ai_cost_cents` key (it already has an `ai_cost_cents` branch at line 38). `app/admin/billing/BillingClient.tsx:139` hardcodes `{ key:'ai_tokens', label:'AI Tokens' }` in its OWN `USAGE_METRICS` array (rendered at ~583) — remove that row, replace with the budget surface. The settings `PersonalMeter` token surface is removed in Item D8.
   - **Acceptance:** `/api/billing/usage` response keeps the `ai_tokens` key (byte-stable) AND gains `ai_cost_cents` with a percentage; the home MessageChip AI alert fires on budget %, not tokens; `/admin/billing` shows NO "AI Tokens" row; no tenant-facing surface uses `max_ai_tokens_per_month` as a live denominator.
   - **Files:** `app/api/billing/usage/route.ts` (add `ai_cost_cents` to the metric set, keep `ai_tokens` dormant), `lib/shell/fallbacks/billingFallback.ts`, `app/api/admin/usage/allocations/route.ts`, `components/banners/MessageChip.tsx`, `app/admin/billing/BillingClient.tsx`. (Platform map handled in Item F.)
   - **Edge cases:** related-entity → every existing `/api/billing/usage` consumer keeps its shape (VERIFY byte-stable: the key persists) · failure → if `ai_cost_cents` can't be computed, the chip degrades silently (no crash) · input-validity → N/A.

### D. Settings panel UI (`/settings/usage`)

8. **[Frontend] Reimagine the per-user panel as ONE calm continuous panel.**
   - ONE AI budget meter (the hero): "X% of your plan's AI budget used this cycle" — `text-2xl font-semibold tabular-nums letterSpacing:-0.02em`, single neutral track + cobalt fill, amber/red ONLY past a real threshold; one reset-date line. Below it: a "where it goes" ranked rows table (category · % right-aligned tabular) replacing the 6-hue `CategoryBreakdown` bar.
   - **Usage projection (owner-locked 2026-06-15 — BUILD with the underuse guard):** compute the run-rate from spend-so-far over cycle-days-elapsed → projected exhaustion date = `period_start + (effective_budget / daily_rate)`. Show "On pace to use your budget by ~MMM DD" ONLY when that date is STRICTLY BEFORE the cycle reset (`period_end`). If projected exhaustion is at/after the reset (underusing), show calm copy "Well within budget this cycle" — never a "finish by" date later than the reset. No usage yet (rate 0) → no projection ("No AI usage yet this cycle"). Already ≥100% → "Budget reached" (per the overage rule), not a projection. The projection is a NON-$ derivation (percent/run-rate), so it is safe for non-`billing.view` viewers. Source the dates from the same anchored period the meter uses (`period_start`/`period_end` from `cost-budget`).
   - DELETE the token `PersonalMeter "AI usage"`. Per-user AI becomes a quiet line ("You've used X% of your share" or "You share the org budget").
   - Grouped entitlement rows via `UsageMetricsOverview` (unchanged contract): each metric = cobalt bar (capped) OR calm "Unlimited" label (no bar) OR "Not included". Contacts/companies now appear (Item B4).
   - Reduce history to ONE trend visual (drop the 4 filled-area charts; unfilled line or a compact table).
   - **Layout when the token PersonalMeter is deleted (self-review):** the current personal section is a `md:grid-cols-2` holding the AI meter AND an enrichment meter. Removing the AI block leaves a lone enrichment meter floating in a 2-col grid — re-specify the section as a single column (or a quiet "your AI share" line + the enrichment meter) and define its desktop/tablet/mobile rendering. Preserve/adapt the `personalLoading`/`personalError`/`null` branches to the NEW `{ ai:{percent}, enrichments:{…} }` shape (not the deleted `ai_tokens`/`allocated`/`org_limit` fields).
   - **a11y for the budget meter (self-review):** `UsageBudgetMeter` today signals warn/danger with COLOR ONLY (number → error color, bar → error fill) — violates "color must not be the only indicator." At warn/danger thresholds the meter MUST show a textual/icon status label ("Approaching budget" / "Over budget"), the `%` value is `tabular-nums` + `font-semibold` (not `font-bold`), and its `role="progressbar"` `aria-label`/`aria-valuetext` reflects the threshold state.
   - **Acceptance:** exactly one AI meter; no token figure anywhere; rainbow bar gone; unlimited items show as calm labels (no bar); semibold tabular numerals; one reset date; warn/danger conveyed by text+icon NOT color alone; the lone-enrichment grid is re-laid-out + responsive; loading/error/empty preserved for the new shape; passes the playbook §3 checklist.
   - **Files:** `app/settings/usage/UsageClient.tsx`, `app/settings/usage/page.tsx`, `components/billing/UsageBudgetMeter.tsx` (semibold + tabular + non-color status), `components/billing/CategoryBreakdown.tsx` (ranked rows, single hue), `components/billing/CostUsagePanel.tsx`, `components/billing/UsageMetricsOverview.tsx` (add contacts/companies).
   - **Edge cases:** boundary → 0% and overage(>100%) render; 10K+ category rows N/A (small set) · empty → brand-new org renders entitlements + "no AI usage yet" · permission → non-`billing.view` sees % only · responsive → desktop/tablet/mobile single-column, 44px touch targets, no horizontal overflow at 375 · input-validity → N/A.

9. **[Frontend] Inline add-ons on the settings panel (admin-gated; others see "ask your admin").**
   - Credit top-up ("Extra AI usage") inline near the AI meter → refactor `BuyCreditsModal` to use the canonical `components/ui/Modal` (retire the hand-rolled `fixed inset-0` shell). Gated `billing.buy_credits`. Overage/auto-reload inline summary opening `OverageSettings`. Seats inline near the Seats section (Item E covers the org panel's deeper seat UI; settings shows the user-relevant seat state + an admin "add seat" entry). Prices read from `plan_tiers` (SSOT), never marketing literals.
   - Non-privileged roles: add-on rows visible but show a quiet "Ask your admin to add" affordance (no purchase control).
   - **Modal focus contract (self-review):** moving `BuyCreditsModal` to `components/ui/Modal` AND opening it from INLINE triggers (not a dedicated billing page) introduces a focus-return regression vector. Acceptance: the credit/overage modal traps focus, Esc closes, and focus RETURNS to the originating inline trigger; the trigger button has an accessible name; `OverageSettings`/`AutoRechargeEditModal` keep their own focus contract.
   - **Acceptance:** an admin can buy credits / toggle overage from `/settings/usage` without leaving for `/settings/billing`; a non-admin sees the add-on context but no purchase button; the modal is the canonical `ui/Modal` primitive with correct focus trap + focus-return; prices match `plan_tiers`.
   - **Files:** `app/settings/usage/UsageClient.tsx`, `components/billing/BuyCreditsModal.tsx`, `components/billing/OverageSettings.tsx`, `components/billing/CreditBalance.tsx`.
   - **Edge cases:** permission/role → `billing.buy_credits` gates purchase; view_only/sales_rep see read-only · failure → purchase error routed through `userFriendlyBillingError` (never raw Stripe) · double-submit → buy button disabled while pending · a11y → focus returns to trigger on close · input-validity → pack amounts from `creditPacks.ts` only.

### E. Admin org panel rebuild (`/admin/usage`)

10. **[Frontend] De-tab to one calm scroll + add the org AI budget meter + collapse to one allocator (that KEEPS enrichment).**
    - Replace the 4-tab `Tabs` with one continuous panel of whitespace-led sections: **AI budget (org %)** [NEW — the org-level `cost-budget` meter] · **Entitlements** (grouped `UsageMetricsOverview`) · **Top AI spenders** (per-user $/% of budget, mini single-cobalt bar, default sort spend desc — Item B5) · **Per-user usage** (the breakdown table, tokens column gone) · **Allocations** (the SINGLE allocator) · **History** (with AI spend per Item A1) · **Add-ons** (inline, Item 11). Remove the raw `.card` wrappers + `.card` loading skeleton (unified plain panel); neutral avatar tiles; sentence-case `th`; one trend visual.
    - **Allocator collapse — DON'T lose enrichment (self-review):** the token `AllocationsTab` renders TWO metrics (`ai_tokens` AND `enrichments`); `AdminAllocationsPanel` is cents-ONLY (no enrichment). So before deleting `AllocationsTab`, EXTEND `AdminAllocationsPanel` to also manage the `enrichments` allocation (the PUT route + `VALID_METRICS` already support `enrichments`), then delete `AllocationsTab`. Otherwise per-user enrichment allocation is silently removed (contradicts Item A2 acceptance).
    - **New-surface states (self-review):** the NEW org `cost-budget` meter must NOT silently render `null`-on-error on the admin page (today `CostUsagePanel` returns `null` on error) — give it a loading skeleton + an actionable retry like the `detail` branch. "Top AI spenders" needs explicit loading skeleton + empty ("no AI usage yet") + error-retry.
    - **Cache invalidation (self-review):** the admin allocation write paths call ONLY `invalidateUserAllocationCache`, never `invalidateOrgCostBudgetCache` — since per-user shares derive from `pool_cents = effective_budget_cents`, an allocation/seat change can leave the org budget meter stale. Add `invalidateOrgCostBudgetCache(orgId)` to the allocation PUT/DELETE write paths (and to add-seat/remove-seat — Item 11).
    - **Acceptance:** no tabs; org AI budget % visible with loading/error/empty states; exactly one allocation editor that handles BOTH cents AND enrichment; per-user breakdown shows AI spend not tokens; org budget cache invalidated on allocation/seat change; PPR page shape preserved (sync `page.tsx` → async `Prefetch` → `Client`, request reads in the Suspense child); playbook §3 pass.
    - **Files:** `app/admin/usage/UsageClient.tsx`, `app/admin/usage/page.tsx`, `components/admin/usage/AdminAllocationsPanel.tsx` (add enrichment metric), `components/billing/UsageMetricsOverview.tsx`, `app/api/admin/usage/allocations/route.ts` + `allocations/[user_id]/route.ts` (add `invalidateOrgCostBudgetCache`).
    - **Edge cases:** boundary → org with 1 user / 0 AI spend renders calm empties · scale → 500+ users → per-user section paginates (RPC bounded per Item B5, client honors `data.pagination`) · race → two admins editing allocations → last-write-wins WITH org-budget cache invalidation · permission → see Item 12 · responsive → per-user table → card/horizontal-scroll at <768, no overflow.

11. **[Frontend] Inline add-ons on the admin panel + wire the orphaned seat endpoints (FIRST UI).**
    - Add-ons section: credit packs (`billing.buy_credits`), overage (`billing.manage_subscription`), seats (`billing.manage_subscription`). Seats: a stepper beside the Seats breakdown wired to `POST /api/billing/subscription/add-seat` + `remove-seat` (no UI exists today — this is their first caller; **requires a smoke test** since the proration path has never been exercised by UI). Show per-seat bonuses (+$2.50 AI, +15-25GB, +40-50 enrichments) and prices from `plan_tiers` columns. Enrichment is framed as the seat lever (no standalone purchase).
    - **Seat stepper a11y (self-review):** the NET-NEW +/- stepper carries `aria-label`s ("Add a power seat" / "Remove a power seat"), the seat count sits in an `aria-live="polite"` region, it is fully keyboard operable, and the disabled-while-pending state is announced.
    - **Double-charge bound (self-review):** `add-seat` idempotency key is `add-seat-${orgId}-${seatType}-${isoMinute}` — a 60s window only. So the client MUST keep the stepper disabled until the request resolves AND render the seat count from the RESPONSE (not optimistic), so a second purchase needs an explicit re-read; the 60s window is the only server guard against rapid double-submit.
    - **Manager "ask your admin" (self-review):** a `sales_manager` reaches `/admin/usage` (has `billing.view`) but lacks `buy_credits`/`manage_subscription`. The Add-ons section must show seat/credit/overage CONTEXT with an "ask an org admin" note for `billing.view`-but-not-purchase viewers — not a bare hidden control (no dead-end).
    - **Acceptance:** an admin can add/remove a power or collaborator seat from `/admin/usage` (keyboard + SR operable); the seat price + bonuses match `plan_tiers`; `requirePowerSeat` fence + dunning enforced server-side; org-budget cache invalidated after a seat change (Item 10); a `sales_manager` sees context + "ask an admin", not a dead control; purchase errors via `userFriendlyBillingError`.
    - **Files:** `app/admin/usage/UsageClient.tsx`, `components/billing/UsageMetricsOverview.tsx` (SeatsBreakdown + inline add), reuse `app/api/billing/subscription/add-seat/route.ts` + `remove-seat/route.ts` (add `invalidateOrgCostBudgetCache`).
    - **Edge cases:** permission → only `billing.manage_subscription` sees seat controls; `billing.view`-only sees "ask an admin" · failure → add-seat Stripe error surfaced friendly; remove-seat schedules end-of-cycle (copy says so); remove blocked below in-use (409 INSUFFICIENT_HEADROOM) · boundary → can't remove below included/in-use seats (server fence) · race → stepper disabled-while-pending + non-optimistic count + 60s idempotency · related-entity → Pro tier has no seat add-on (hide the control).

12. **[Backend] Unify the admin usage RBAC + team scope + sidebar gate.**
    - **Wrapper inventory correction (self-review):** only TWO routes ride the deprecated path — `breakdown` GET (`withManagerAuthCached → admin.users`) and `allocations` PUT/DELETE (`withManagerAuth → admin.users`). `allocations-overview` + `allocations/[user_id]` ALREADY use `withPermission('billing.redistribute')` (VERIFY only). Migrate the two offenders to `withPermissionCached('billing.redistribute')` (GET) / `withPermission('billing.redistribute')` (PUT/DELETE). **PRESERVE `requireMfaForSensitiveRoute`** on allocations PUT/DELETE (and `[user_id]` DELETE) — a naive wrapper swap must not drop the MFA re-verification on a money-adjacent mutation.
    - **RPC internal role-gate (self-review):** `get_per_user_usage_summary` has an INTERNAL guard `role IN ('admin','super_admin','manager','sales_manager')` (`20260423:1061`) on the legacy `profiles.role`. A custom role holding `billing.redistribute` but with a non-listed slug PASSES the route yet 42501s at the RPC. The single RPC migration (Item B5) must relax this to honor `billing.redistribute` (or drop it and rely on the route gate + org-binding).
    - **Team-scope mechanism (self-review) — specify it concretely:** the RPC returns ALL org users with NO team param; "the manager's team" is ambiguous (`team_members` allows MULTI-team membership, no "manager-of" marker). Define: resolve the caller's `team_id`(s) as all org-scoped `team_members` rows for the caller, collect those teams' member `user_ids`, and pass them to the breakdown via `p_user_ids[]` (use the existing `get_user_ai_usage_cents_bulk(p_user_ids[])` primitive OR add a `p_user_ids` param to the breakdown RPC). State whether narrowing is in the RPC or a route post-filter; handle multi-team (union) and zero-team (empty roster, not org-wide) explicitly. Without this, a `team`-scoped manager keeps seeing every org user (the leak).
    - **AdminSidebar real fix (self-review):** the `/admin/usage` link is rendered UNCONDITIONALLY in `AdminSidebar`'s hardcoded tree (NOT behind `isAdmin`, and the component never consults `SIDEBAR_PERMISSIONS`). The fix is a structural change: add a permission filter (`usePermissions().can('usage.view_team')`) to the `AdminSidebar` tree itself so visibility matches the page gate — not a flag flip.
    - **Acceptance:** a `billing.redistribute` custom role uses BOTH the slider and the per-user table without a 403 at EITHER the route OR the RPC; MFA preserved on allocation writes; a `team`-scoped manager sees ONLY their team's user rows (multi-team = union, zero-team = empty); the AdminSidebar link is gated on `usage.view_team`; the two offender routes enforce `billing.redistribute`.
    - **Files:** `app/api/admin/usage/breakdown/route.ts`, `app/api/admin/usage/allocations/route.ts`, `app/api/admin/usage/allocations/[user_id]/route.ts` (verify), `app/api/admin/usage/allocations-overview/route.ts` (verify), `components/admin/AdminSidebar.tsx` (add permission filter), `app/admin/layout.tsx` (thread perms if needed). RPC role-gate relax + team-narrowing param land in Item B5's single RPC migration.
    - **Edge cases:** permission/role → 7-role matrix holds; `super_admin`/`admin`=all, `sales_manager`=team, others=none · related-entity → manager with empty team sees empty roster · failure → undetermined perms fail to `/` not 404 (existing pattern) · input-validity → scope derived from auth, never client.

### F. Platform-console tenant usage realign (Q4: rebuild both)

13. **[Backend+Frontend] Build the platform tenant-usage meter UI (it does NOT exist) on the budget model.**
    - CORRECTION (self-review): `/api/platform/tenants/[orgId]/usage` has **ZERO UI consumers** — `TenantDetailClient` reads only `/api/platform/tenants/[orgId]` (org+users+tickets); the sole "usage" UI is the Reset-usage Danger Zone. So there is no token meter to "realign" — the operator usage-meter surface must be BUILT: (a) the route's AI representation switches off `ai_tokens → max_ai_tokens_per_month` onto the cost/budget model (org `effective_budget_cents` + spend) + SELECTs `ai_cost_cents_used` (Item A1); (b) `TenantDetailClient` gains a `useSWR('/api/platform/tenants/${orgId}/usage')` consumer with explicit loading/empty($0-budget)/error branches; (c) it MUST comply with `app/platform/CLAUDE.md`: every chart/meter wraps `ChartA11yWrapper` (`role="img"` + hidden data table, color never the sole signal), uses `--pf-*` tokens, and respects the 300s polling floor; any budget meter carries `role="progressbar"` + `aria-label` like the tenant `UsageMeter`.
    - **Override editor (self-review):** `app/api/platform/tenants/[orgId]/route.ts` (lines ~181-187) is a tenant plan-limits override editor that reads/writes `max_ai_tokens_per_month` (+ `ai_cost_cents_monthly_delta`). Decide: HIDE the `max_ai_tokens_per_month` field (it's the vestigial denominator being retired) and keep the `ai_cost_cents_monthly_delta` override (the real budget lever). `reset-usage` keeps zeroing `ai_tokens_used` (observability) AND now also zeroes `ai_cost_cents_used` (Item A1); update its copy so it doesn't imply a live token meter.
    - **Acceptance:** an operator opens `/platform/tenants/[orgId]` and sees a NEW tenant usage surface showing AI as budget/% (same numbers the tenant sees), no `max_ai_tokens_per_month` denominator anywhere (including the override editor), platform a11y/token contract honored; `withPlatformAuthCached`/`withPlatformAdmin` gates unchanged; no cross-tenant leak (all queries org-scoped).
    - **Files:** `app/api/platform/tenants/[orgId]/usage/route.ts`, `app/platform/tenants/[orgId]/TenantDetailClient.tsx` (BUILD the consumer), `app/api/platform/tenants/[orgId]/route.ts` (override editor), `app/api/platform/tenants/[orgId]/reset-usage/route.ts`.
    - **Edge cases:** permission → platform identity only; no CRM-role bleed · boundary → tenant with $0 budget renders "no AI budget" empty state · related-entity → a tenant mid-cycle uses the anchored period · a11y → `ChartA11yWrapper` + `--pf-*` per platform contract · DEFER-TO-HUNT: whether any platform analytics consumer still keys on the token field.

### G. Correctness, drift-guard, docs

14. **[Backend] Fix `getDefaultLimits()` drift.** Pro emails 1000→2000, storage 2GB→15GB, enrichments 15→20 to match DB. (Fires only on a DB read error, but the stale fallback is wrong.)
    - **Acceptance:** `getDefaultLimits()` Pro values equal the seeded `plan_tiers` Pro row.
    - **Files:** `lib/billing/planTiers.ts`.
    - **Edge cases:** N/A on most axes (a constant fix); boundary → DB-error path now returns correct Pro caps.

15. **[Test] Drift-guard: assert marketing `tokens.ts` LIMITS == the seeded `plan_tiers` values.** A vitest that fails if a future marketing edit (or a reseed migration) diverges the panel's denominators.
    - CORRECTION (self-review): a vitest cannot read the live DB. Make the guarantee precise: the test asserts `tokens.ts` LIMITS == a checked-in snapshot of the seed-migration values (the `20260609/20260612/20260615` UPDATE values), with a note that the snapshot must be updated WHENEVER a reseed migration lands (so the test tracks the intended SSOT, not just tokens.ts-vs-itself). Optionally parse the seed migration's UPDATE values directly.
    - **Acceptance:** test passes today; flips red if any `tokens.ts` LIMIT ≠ the snapshot of the seeded `plan_tiers` value; the acceptance text states it guards tokens.ts-vs-seed-snapshot (not tokens.ts-vs-live-DB).
    - **Files:** `lib/billing/__tests__/limits_marketing_drift.test.ts` (new), reads `app/(site)/features/_shell/tokens.ts` + a checked-in snapshot of the seed values.
    - **Edge cases:** input-validity → handles `Unlimited`/`null` mapping; a new tier added to one side fails loudly.

16. **[Docs] Help-Center updates — wider than 3 pages (self-review).** Token/legacy-tab copy lives in ≥4 MORE doc pages the original list missed: `app/docs/admin/usage/page.tsx` (a "Legacy token allocations (Allocations tab)" + "AI Token Allocations" + slider section — the tab is DELETED + de-tabbed, so rewrite to the budget model; this EXISTING page IS the "admin usage" article — UPDATE it, don't add a parallel one), `app/docs/settings/billing/page.tsx:211` ("AI budget / AI tokens — Top up opens Admin > Billing" — the inline add-ons change this flow), `app/docs/ai-agents/overview/page.tsx:85` ("your AI token limit is reached, or your personal allocation is exhausted" — token enforcement RETIRED, now false), `app/docs/ai-agents/runs-history/page.tsx:119` ("monthly AI token limit reached" — now false). Plus the original `/docs/settings/usage`, `/docs/admin/billing/usage`, `/docs/admin/billing/credit-packs`; a "AI budget, % usage, and add-ons" article; `lib/docsIndex.ts` keywords (budget/percentage/credits/seats/overage/unlimited); `DocsSidebar` entry if a new article is added; flag the stale `UsageBanner` entry in `app/api/billing/CLAUDE.md`.
    - **Acceptance:** docs describe the % budget model, unlimited items, inline add-ons, seat add-ons, overage; **`grep app/docs` for "AI Tokens" / "token limit" / "personal allocation" returns zero enforcement-framed hits**; search finds "AI budget", "credits", "overage", "seats", "unlimited".
    - **Files:** `app/docs/settings/usage/page.tsx`, `app/docs/admin/usage/page.tsx`, `app/docs/admin/billing/usage/page.tsx`, `app/docs/admin/billing/credit-packs/page.tsx`, `app/docs/settings/billing/page.tsx`, `app/docs/ai-agents/overview/page.tsx`, `app/docs/ai-agents/runs-history/page.tsx`, `lib/docsIndex.ts`, `components/docs/DocsSidebar.tsx`, `app/api/billing/CLAUDE.md`.

### H. Notifications alignment

17. **[Backend] Budget-% threshold alerts (corrected plumbing, self-review).** `cron/check-usage-alerts` today uses `THRESHOLDS=[80,90,100]` and reads `usage_meters.ai_tokens_used` ÷ `METRIC_TO_LIMIT_COLUMN['ai_tokens']=max_ai_tokens_per_month` — it has NO access to `effective_budget_cents`. To alert on budget %: (a) keep the threshold set explicit — KEEP `[80,90,100]` (do not silently introduce "50"); (b) source the AI denominator specially: `ai_cost_cents_used` (Item A1) ÷ per-org `effective_budget_cents` (via `getOrgCostBudget`) — a special-cased denominator OUTSIDE `METRIC_TO_LIMIT_COLUMN` (which only maps to `plan_tiers` columns); (c) change the `usage_alerts` dedup key from `metric='ai_tokens'` to a distinct budget metric key (e.g. `'ai_budget'`) so an org that already got an `ai_tokens` 80% alert doesn't get a DUPLICATE 80% alert post-cutover; (d) reconcile the period: the cron is calendar-month while the budget anchors to the subscription cycle — state which the alert uses (recommend the budget's anchor cycle for consistency with the meter). Depends on Item A1 being populated first. Reuse the existing notification type if one fits.
    - **Acceptance:** an org at 80%/90%/100% of its AI budget (`effective_budget_cents`) gets exactly one alert each; no alert is keyed on `ai_tokens`; the dedup metric key is the new budget key (no duplicate post-cutover); copy uses budget framing; a smoke-test scenario covers it (see Smoke Tests).
    - **Files:** `app/api/cron/check-usage-alerts/route.ts`, notification template/type registry (existing).
    - **Edge cases:** boundary → exactly 80/90/100% fires once (dedup on the new key) · race → cron re-run dedups · related-entity → free tier ($0 budget, `effective_budget_cents≤0`) never alerts (skip) · migration → already-sent `ai_tokens` alerts don't collide with the new `ai_budget` key.

### I. Automated regression tests

18. **[Test] Render + a11y tests for the rebuilt clients (self-review).** Add render-level tests (in the style of the existing `components/billing/__tests__/UsageMeter.render.test.tsx`) for the rebuilt settings `UsageClient`, admin `UsageClient`, and the new platform usage surface asserting: a single `h1→h2` heading outline AFTER de-tabbing `/admin/usage` (a de-tab can silently break the outline); exactly ONE `role="progressbar"` AI meter per page; NO token-labelled row anywhere; "Unlimited" rows render with NO progressbar; the warn/danger state exposes a non-color status label.
    - **Acceptance:** the three render tests pass and would fail if a token row, a second AI meter, a broken heading outline, or a color-only threshold state regressed.
    - **Files:** `app/settings/usage/__tests__/*.render.test.tsx`, `app/admin/usage/__tests__/*.render.test.tsx` (new).
    - **Edge cases:** input-validity → tests cover the empty/unlimited/over-budget render branches.

---

### Role Access Matrix (tenant — usage surfaces)
| Role | `/settings/usage` (own) | `/admin/usage` (org) | Sees $ (`billing.view`) | Sets allocations (`billing.redistribute`) | Buys add-ons | Data scope (admin view) |
|------|------|------|------|------|------|------|
| `super_admin` | yes | yes | yes | yes (all) | yes (`buy_credits`+`manage_subscription`) | all |
| `admin` | yes | yes | yes | yes (all) | yes | all |
| `sales_manager` | yes | yes | **yes (FIX: `billing.view`=all)** | yes (team) | no | **team (FIX: scope-enforced)** |
| `sales_rep` | yes (own) | no | no | no | no | none |
| `marketing` | yes (own) | no | no | no | no | none |
| `service` | yes (own) | no | no | no | no | none |
| `view_only` | yes (own, read) | no | no | no | no | none |

UI gating table: AI $ figures → `billing.view` (hidden→% only); "Buy credits" → `billing.buy_credits` (hidden); "Add/Remove seat" + "Overage" → `billing.manage_subscription` (hidden); allocation sliders → `billing.redistribute` (hidden); `/admin/usage` nav link → `usage.view_team` (hidden); platform tenant view → platform identity only.

---

### Known edge surfaces for /edge-cases
- **Overage >100% display:** RESOLVED + locked (Item B4) — past-100% "+ credits" line shows ONLY when `overage_active` (overage_enabled AND credits>0), else hard-stop at 100%. The hunt should still probe the exact threshold-color + copy at the 99→100→over boundary and the admin $-view headroom math.
- **Usage projection line:** RESOLVED + locked (Item D8) — built with the underuse guard (only warn if projected exhaustion < reset). The hunt should probe the run-rate math at cycle-day-1 (tiny denominator → wild projection — clamp/suppress), at a mid-cycle plan upgrade (budget changes mid-period), and the timezone/rounding of the projected date vs the reset date.
- **Legacy `ai_tokens` Redis counter readers:** grep-confirm no non-AI/observability consumer breaks when display/enforcement stop reading it.
- **`/api/billing/usage` byte-stability:** the `usage.ai_tokens` wire key is KEPT (dormant) for shape-stability; verify the broad consumer set (AuthenticatedChrome shell, automations, campaigns/compose, reports, `BillingClient`, the home `MessageChip` chip) still renders. NOTE: `components/billing/UsageBanner.tsx` does not exist — the live consumer is `components/banners/MessageChip.tsx`.
- **Calendar-month vs subscription-anchor period:** the history snapshot column is calendar-month; the live % meter + alerts anchor to the subscription cycle — confirm the two are never conflated in a single surface.
- **Seat add-on first-UI proration:** the never-before-exercised `add-seat`/`remove-seat` UI path — smoke-test the Stripe proration + dunning behavior.
- **Per-user breakdown pagination at scale** (>50 / 500 users) — the UI currently ignores `data.pagination`.

---

### Smoke Test Scenarios
1. **Per-user budget %:** spend ~10% of an org's AI budget → `/settings/usage` shows ~10% (not a token count); no "AI usage" token meter anywhere; reset date correct.
2. **Unlimited rendering:** a Pro/Ultra org → contacts/companies/exports (and Ultra object-counts) render as calm "Unlimited" labels with NO bar; capped metrics render a single cobalt bar.
3. **Inline add-ons (admin):** from `/settings/usage` and `/admin/usage`, buy a credit pack, toggle overage, add a power seat (verify Stripe proration + the seat bonuses applied to budget/storage/enrichment); a non-admin sees no purchase controls.
4. **Token retire:** set a (formerly) low token allocation on a user → that user can STILL use AI (only the budget governs); no "personal allocation exceeded (tokens)" block; admin sees no token UI.
5. **Org panel + scope:** a `sales_manager` opens `/admin/usage` → sees the org AI budget %, only their team's per-user rows, one allocation slider, history with AI spend; the sidebar link matches the gate.
6. **Platform realign:** an operator opens `/platform/tenants/[orgId]` → a NEW tenant usage surface shows AI as budget/% (same as the tenant), no token denominator; `ChartA11yWrapper`/`--pf-*` honored; the override editor no longer exposes `max_ai_tokens_per_month`.
7. **Responsive (concrete):** at 375/768/1024/1440 light+dark — the admin per-user breakdown (a horizontal-scroll table today) collapses to cards or scrolls cleanly at <768; the AI meter + entitlement rows stay single-column with no horizontal page overflow; inline add-on modals are full-screen on mobile; 44px touch targets on stepper +/- and buttons.
8. **Drift guard:** edit a `tokens.ts` LIMIT to disagree with the seed snapshot → the new test goes red.
9. **Budget alert (notifications):** drive an org to 80% of `effective_budget_cents` → exactly one budget alert fires, keyed on the new `ai_budget` metric (not `ai_tokens`), copy uses budget framing; re-running the cron does not duplicate it; a free ($0-budget) org never alerts.
10. **a11y (rebuilt clients):** `/admin/usage` after de-tab has a clean h1→h2 outline and one `role="progressbar"` AI meter; the inlined credit modal returns focus to its trigger on close; the seat stepper is keyboard + screen-reader operable.
11. **Overage gating:** an org at 100% with overage OFF (default) and/or zero credits → hard stop "Budget reached — enable overage or upgrade", no "+ credits" line, AI denied. Enable overage AND load credits → spend continues, panel shows "100% + $Y in credits" (admin) / "+ credits" line (non-admin via `overage_active`), AI allowed; turning overage back off returns to hard-stop.
12. **Projection underuse guard:** an org spending slowly with reset on the 20th → shows "Well within budget this cycle", NOT "on pace to finish by the 24th"; an org spending fast enough to exhaust on the 18th (before the 20th reset) → shows "On pace to use your budget by ~the 18th"; a cycle with zero spend → "No AI usage yet this cycle" (no projection); day-1 tiny denominator does not produce a wild date.

---

### Plan Additions from Self-Review
The Phase-7 review (6 canonical lenses, 48 confirmed code-verified gaps) corrected these assumptions and added scope — all folded into the items above:
- **A1:** `usage_meters` ALREADY has `ai_cost_cents_used` — reuse it (no new column); the real gap is the missing snapshot SOURCE (`get_org_usage_snapshot` has no AI cost) + missing reads in 2 history routes + reset-usage; documented calendar-vs-anchor period.
- **A2:** the `user_usage_allocations` CHECK is 8 values (not 3) — do NOT narrow it; stop WRITING `ai_tokens` only.
- **A3+B5+12(RPC):** consolidated into ONE `get_per_user_usage_summary` rewrite (cost_cents + storage org/period scope + p_limit/p_offset + internal role-gate relax + byte-identical 20260423 hardening) — three competing CREATE OR REPLACE migrations would have clobbered the auth guard.
- **4/B6:** `percentage_used` is `Math.min(100,…)` + cents stripped for non-admins → added a non-$ overage field so overage renders for buyers; `canSeeDollars` needs a signature change + the TWO page prefetchers; `usage/detail` is already budget-% (no token row to strip); contacts/companies need a `MetricGroup` assignment.
- **6:** SECOND token hot path in `agentExecutor.ts:194`; assert the free-tier bypass is preserved + a $0 PAID org is denied.
- **7:** keep the `usage.ai_tokens` wire key (byte-stable) + GAIN `usage.ai_cost_cents`; repoint `MessageChip.tsx:37` + `BillingClient.tsx:139` (the missed raw-token consumers); `UsageBanner.tsx` doesn't exist (it's `MessageChip`).
- **10:** `AdminAllocationsPanel` is cents-only — extend it to keep the enrichment allocator before deleting `AllocationsTab`; add loading/error states for the new org-budget + top-spenders surfaces; add `invalidateOrgCostBudgetCache` on allocation/seat writes.
- **11:** seat-stepper a11y; 60s idempotency double-charge bound; the `sales_manager` "ask your admin" case.
- **12:** wrapper inventory corrected (only breakdown GET + allocations PUT/DELETE); preserve MFA; AdminSidebar `/admin/usage` is UNCONDITIONAL (real fix = add a permission filter to its hardcoded tree); concrete team-scope mechanism via `team_members` → `p_user_ids[]`.
- **13:** the platform `/usage` route has ZERO UI consumers — BUILD the meter surface, comply with `app/platform/CLAUDE.md` (`ChartA11yWrapper`/`--pf-*`/300s); hide `max_ai_tokens_per_month` in the override editor.
- **16:** token copy lives in 4 MORE doc pages (`docs/admin/usage`, `docs/settings/billing`, `docs/ai-agents/overview`, `docs/ai-agents/runs-history`) + a grep-zero acceptance.
- **17:** thresholds are `[80,90,100]` (not 50/80/100); needs `effective_budget_cents` + a new `ai_budget` dedup key + period reconcile.
- **D8:** color-only threshold state (a11y); lone-enrichment grid re-layout.
- **9:** Modal focus-return contract.
- **15:** drift-guard is snapshot-based (vitest can't read live DB).
- **NEW Item 18:** render/a11y regression tests for the rebuilt clients; **NEW Smoke 9/10:** notification + a11y scenarios.

### Self-Review Summary
- Discovery/research: 11 agents (D1–D9 + R1–R2), waved + retried past a transient server-side rate limit; live DB `plan_tiers` queried directly by the orchestrator.
- Design convergence: orchestrator-synthesized (backend/frontend/integration lenses) from the discovery evidence.
- Iterations: 1 full adversarial round, 6 distinct parallel lens agents (completeness/journey, technical/CLAUDE.md, edge/integration/cohesion, hostile-security/scale, QA/a11y/docs, cohesion-companion/KEC) — all 6 returned; 48 confirmed gaps.
- Perspectives applied: user-frustration, security, data-model, integration, CLAUDE.md-compliance, scale, accessibility, architectural-cohesion/anti-fragmentation — every canonical lens staffed.
- Items added: 1 (Item 18) + 2 smoke scenarios; ~17 items materially corrected; 0 net items removed.
- Total review agents spawned: 6 (plus 11 discovery/research). Convergence note: this is a planning deliverable gated again by the owner at ExitPlanMode; the single deep adversarial round (rather than the 3-round floor) was a deliberate rate-limit-aware tradeoff, and every HIGH finding was code-verified by the lens that raised it (file+line citations), so confidence is high despite one round.
