Skip to content

feat(045): usage-based cost model — billed cost, allowances, credits - #128

Merged
studert merged 19 commits into
mainfrom
045-usage-based-cost-model
Oct 2, 2026
Merged

studert merged 19 commits into
mainfrom
045-usage-based-cost-model

Conversation

@studert

@studert studert commented Oct 2, 2026

Copy link
Copy Markdown
Member

What this changes

The Hub was built when developers shared pooled Anthropic workspaces. It has been modelling a usage-based tool as if it were a seat ever since, and that shows up in three places. All three are fixed together, because fixing one alone leaves a visible contradiction on screen.

1. Per-user cost is read, not re-derived. Cost came from token counts × a hard-coded price table. Every model released after the table was last updated fell back to the highest rate ever charged — overstating one user's August spend 7.6×. The cost sync now requests group_by[]=description, stores line items in micro-cents, and attributes the billed total: whole to a sole owner (billed), split by measured usage across several (apportioned), or to nobody (unattributed). For a complete day, no price table is consulted at all.

2. A tier price stops being a cost when it is an allowance. access_tiers.pricing_model (seat | usage, default seat) decides. Seat tiers are byte-identical to before, pinned by a test against sumExpectedSpendCents. Usage tiers use measured consumption for completed periods and a trailing 3-complete-month mean for open ones, falling back to the allowance only with no history — and marked as the placeholder it is.

3. Credit purchases are cash, not period cost. API access is prepaid. An invoice can be recorded as a top-up, excluded from period cost by exclusion rather than by unpicking the link, and the Hub derives a balance (opening + purchases − consumption) that the Admin API cannot supply.

Plus: recorded workspace caps mirrored from the Console with a last-confirmed date and an allowance-mismatch flag, sync-time reconciliation so a future divergence surfaces within a day, admin-correctable ownership, and retirement of the 12 dead boost-* pools.

Verified against real data

Applied to an isolated Neon branch forked from production (wt/045-usage-based-cost-model). Production was never written to.

Criterion Result
SC-001 — single-owner month equals the Console 0 cents difference for both users, August
Rollup vs line items 0 mismatches across 923 workspace-days
Attribution invariants I1/I2/I3 0 violations
SC-004 — usage expected spend $3,650 allowances → $406.48 measured (August)
SC-004 — seat expected spend unchanged to the cent ($11,147)
SC-010 — deprecated history $12,010.42 still readable
Licence register 40 active / $3,650 → 3 active / $375

Reconciliation, run against live September data, immediately names the defect this feature exists to close: Oliver 7.35×, Marlon 1.63×.

Restatement

Historical per-user figures move, and so does expected spend. docs/045-restatement-announcement.md covers both in one announcement with before/after per person and per period, written for the people whose numbers changed. Headline: Marlon's August $630.85 → $254.66, Oliver's $174.94 → $23.37. Svenja's barely moves ($29.30 → $29.31) — every model she used was already in the price table, which is the clearest evidence the change is doing what it claims.

Saved forecast scenarios (041) read the same data and move with it.

Two decisions to review before merge

  1. The revocation date has an off-by-one against its own intent. OQ-3 says periods "from July onward" should stop counting the pooled licences, but the Hub's overlap rule is inclusive, so revoked_at = 2026-07-01 leaves July still counting all $3,700; the drop lands in August. May and June are correctly untouched. If July should be clean, the date needs to be 2026-06-30 — a one-word change and a re-run of the script. I kept the literally agreed date rather than quietly shifting it.

  2. Automations needed an ownership exclusion. Its API key resolves to a person, so attribution made its spend theirs — contradicting OQ-4, which says it stays unattributed. A third ownership source, excluded, exists for exactly this and is applied. The workspace card exposes it as "not a person's workspace?".

Migration

0032 is additive: 2 enums, 3 tables, 9 nullable/defaulted columns, no rename, no rewrite. Reviewed by drizzle-migration-reviewer before it ran; its findings on FK indexes, ON DELETE rules, token_type width and an empty-string/NULL collision are folded in. The data migration seeds ownership, flips five tiers by id and name, and deprecates 12 workspaces by explicit id — all idempotent.

pricing_model defaults to seat, so the schema change alone changes nothing on deploy.

Checks

tsc --noEmit clean · eslint --max-warnings=0 clean · 818 unit tests + attribution integration suite green.

Phases

Each is independently deployable and leaves the app correct: schema → line-item ingestion → attribution → read-path swap → pricing models → credits → reconciliation and caps → ownership and retirement. Phases 4 and 5 carry the restatement and should ship together.

🤖 Generated with Claude Code

claude and others added 17 commits September 18, 2026 08:52
… 4.0

Models released after the price table was last updated (2026-06-01) were
not matched by resolveModelPricing() and silently fell back to the
highest tier ever charged, $15/$75 per MTok. Every Sonnet 5, Opus 5,
Fable 5 and Fable 5.1 row in anthropic_usage_metrics was therefore
over-costed — up to 7.5x for Sonnet 5 — which is why per-user costs in
the profile API diverged from the Claude Console, while the workspace
figures (sourced from cost_report) stayed correct.

- Add Fable 5.1 / Fable 5 ($10/$50), Opus 5 ($5/$25), Sonnet 5 ($2/$10)
  with their cache read and write rates.
- Match the longest prefix rather than the first table entry, so
  claude-fable-5-1 is not swallowed by claude-fable-5.
- Name the unknown-model fallback FALLBACK_PRICING instead of relying on
  MODEL_PRICING[0], which depended on array order.

Run recalculateUnresolvedCosts() after deploy to re-price the affected
historical rows. Analysis and remaining modelling gaps are documented in
docs/anthropic-cost-accuracy.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
Makes Anthropic's billed cost the source of truth for per-user Claude
reporting, instead of re-deriving it from token counts and a hard-coded
price table. The estate has inverted since the tracker was built: >=172
users are on subscription seats, 40 hold API keys, and only 4 had any API
usage in September — and those users each own a workspace, so the billed
workspace cost IS their cost.

Covers: cost_report line items (group_by=description, which also supplies
cache TTL, context window and service tier as data rather than
assumptions), an attribution layer (billed / apportioned / unattributed),
the current-day estimate that stays because cost_report is daily-only,
sync-time reconciliation so a future divergence surfaces within a day,
admin-correctable workspace ownership, and deprecation of the 12 dead
boost-* pooled workspaces.

Out of scope by decision: attributing project/client workspace spend
(~46% of September) to cost centres — deferred to a separate feature.
Two product questions are recorded as open rather than answered, notably
whether a tier price should be reported as an allowance rather than a
cost.

Six independently deployable phases; no destructive migration. Includes
spec, plan, research, data model, attribution contract, quickstart,
tasks and the requirements checklist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
Supersedes the narrower workspace-attribution draft (folder renamed) with
one coherent specification covering the whole cost-model correction. The
Hub models a usage-based tool as if it were a seat, and that single
mistake shows up in three places:

1. Per-user cost is re-derived from token counts and a price table
   instead of read from cost_report. Where a workspace has one owner the
   billed cost IS that person's cost.
2. A tier price is summed into expected spend even when it is an
   allowance. Claude Console tiers contribute $3,650/month of expected
   against $398.69 of August consumption.
3. API access is prepaid, so invoices are credit top-ups, not period
   cost — a top-up inflates the month it lands in and leaves the
   following months looking free.

Adds pricing_model (seat | usage) on access_tiers, expected spend from
measured consumption with a trailing projection and an allowance
fallback, allowance/consumption/purchase/balance labelling, credit
purchases with a derived balance the Anthropic API cannot supply, and
recorded workspace caps mirrored from the Console with a last-confirmed
date and an allowance-mismatch flag.

The Hub tracks; it does not enforce. The Admin API exposes no endpoint
for spend limits or credit balance, and caps are set in the Console
directly — so caps and the opening credit balance are admin-entered and
their staleness is made visible rather than hidden.

Still out of scope by decision: attributing project/client workspace
spend (~46% of September), which stays visible as unattributed.

Eight independently deployable phases, no destructive migration, no
column renames. Two restatements (per-user history and expected spend)
ship together behind one announcement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
…pace

The 36 assignments on the boost-* pools are already deactivated in the
Claude Console, so the Hub is carrying licences that no longer exist.
Revocation is now in scope (FR-028..030, US9 scenarios 4-6).

Scoped by WORKSPACE, not by tier: assignment 262 carries a boost-advanced
tier but sits on the Automations workspace, which is live and has current
spend. Revoking by tier would take out a working licence. It is reported
separately instead (new OQ-4).

Revocation carries an explicit date rather than now(), because revoked_at
decides which budget periods still count the assignment — dating them
today would keep them in every past period's expected spend. Org API
spend collapsed from $3,016.86 (June) to $194.89 (July), which points at
an end-of-June deactivation; the date itself is new OQ-3.

Adds Appendix A with the identifiers an implementer needs: the five tier
ids to flip to usage pricing, the twelve workspace ids to deprecate, the
thirty-six assignment ids to revoke, and the three indie assignments that
must survive untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
Resolves OQ-3 and OQ-4. All 37 Claude Console assignments — the 36 on
pooled workspaces plus 262 on Automations — are revoked with
revoked_at = 2026-07-01, matching when org API spend collapsed from
$3,016.86 (June) to $194.89 (July). Budget periods ending before that
date keep counting them; periods from July onward do not.

Revoking 262 does not hide the Automations workspace's ongoing spend:
that spend belongs to the workspace, which stays visible as unattributed
under the out-of-scope project-workspace rule. FR-029 still governs any
future boost-tier assignment found outside a pooled workspace.

After the cleanup the Claude Console licence register reads $375/month
of allowance rather than $3,650.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
… the grain

Verified group_by[]=description against the live API. Two defects in the
data model surfaced, both of which would have broken SC-001.

1. Per-line-item rounding drifts. `amount` is a decimal string of CENTS
   with up to six decimal places. The existing Math.round per result row
   is safe today only because there is ~one row per workspace-day; with
   description grouping there are 12-14, and rounding each before summing
   drifts +/-1 cent per workspace-day. Line items now store bigint
   micro-cents and rounding happens once at the aggregate (contract
   R6-R8). The sample rolls up to exactly $29.28 and $8.86 — the figures
   the dashboard shows today.

2. inference_geo was missing from the unique grain. Haiku 4.5 reports
   not_available while every other model on the same workspace-day
   reports global, so two genuinely different rows would have collided
   and the upsert would have silently overwritten one.

Also confirmed from the sample: 200k-1M context-window rows are real and
material (13.6% of one workspace's day), service_tier and pagination
behave as specified, and volume is ~1.1k rows/month — an order of
magnitude below the earlier estimate, which takes the maxDuration risk
down to low.

The response is checked in as a test fixture so T014 asserts against real
data rather than invented numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
Follow-up to the precision fix: the quickstart still selected
cost_cents from the line-item table, which no longer exists, and the
plan still carried the pre-measurement 12k rows/month estimate.

- Quickstart queries read cost_microcents and round at the aggregate,
  matching contract R6-R8, and note that 143569125 means $1.4357.
- New step 0 runs the fixture-backed rollup test first, so the
  arithmetic can be verified with no database connection at all.
- Plan scale/scope and risk 7 updated to the measured ~1.1k rows/month;
  the maxDuration risk is closed, with the wall-clock check kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BT4qCNYhERb5BQdG4uLxMv
Contract R9/R10 fix the one real design gap: attributed cost is stored
per (user, date, model), so the daily figure needs a second
largest-remainder pass, and billed cost with no usage row needs a
carrier row rather than being dropped. Invariant I3a and the quickstart
query assert both.

Records the decisions taken: OQ-2 resolved to a trailing 3-complete-month
projection window, the Claude Console opening credit balance ($422.72 as
of 2026-09-18, admin-entered, not seeded), and workspace caps entered by
hand. Refreshes the stale checklist (OQ-1/3/4 were already resolved),
drops the revocation from the plan's out-of-scope list where it
contradicted the spec, and fixes a D6/D8 cross-reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migration 0032 (additive): attribution_mode + pricing_model enums, three new
tables (anthropic_workspace_cost_items, anthropic_workspace_owners,
credit_purchases), and nullable columns on access_tiers, ai_tools,
anthropic_usage_metrics, anthropic_workspace_limits and anthropic_workspaces.
pricing_model defaults to 'seat', so nothing changes on deploy.

The data migration seeds ownership from resolved API keys (excluding the
userId=0 sentinel and default-workspace keys, which stay unattributed), flips
the five Claude Console tiers to 'usage' guarded by both id and name, and
deprecates the 12 pooled boost-* workspaces by explicit id.

The cost sync now requests group_by[]=description and stores line items in
micro-cents; the daily rollup is DERIVED from them, so the two cannot drift.
Rounding happens once, at the aggregate — per-row rounding was off by 2 cents
on 2026-09-01 alone. Verified against the branch: 8455 line items, 923
workspace-days, zero disagreement with the rollup.

Migration reviewed by drizzle-migration-reviewer; its findings on FK indexes,
ON DELETE rules, token_type width and empty-string/NULL collision are folded in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/lib/anthropic/cost-attribution.ts is pure: deriveMode, apportion (largest
remainder, ties by ascending userId, even split when every weight is zero) and
distributeAcrossModels, the second pass that spreads a user's daily figure over
their (user, date, model) rows so the stored parts still sum to the day.

The sync step reads the rollup, ownership and computed costs, then writes
attributed_cost_cents / attribution_mode in two batched statements. It clears
the window first, so an ownership correction cannot leave a stale figure
behind, and writes a __billed_only__ carrier row where billed cost exists but
the usage report covers nothing — the cost is never silently dropped.

Key resolution now maintains anthropic_workspace_owners: stale `resolved` rows
are removed so moving a key moves ownership rather than adding a co-owner, and
`manual` rows are never touched.

Verified against the branch: 3208 attributed rows over 923 workspace-days, with
zero I1/I2/I3 violations. The idempotency test caught a real defect — carrier
rows were being read back as usage, losing their cost on every re-sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six consumers move to COALESCE(attributed_cost_cents, computed_cost_cents):
the profile API, the Claude user actions, the MCP tools, the org actions, the
user-bucket histogram and the saved forecast scenarios. Every existing field
keeps its name and meaning; the values are now billed-based for complete days.

Added alongside: costData.attribution (method, billed/estimated split,
workspace), a per-day method, attributionMethod on list_claude_users and the
user table, and an AttributionBadge that stays silent for `billed` and marks
only apportioned / estimated / mixed — in words, not colour.

The today-estimate calibration deliberately stays on the computed basis: it
measures how far the token-derived estimate sits from the bill, so reading
billed cost on both sides would pin the ratio at 1.0 and destroy the signal.
Carrier rows are excluded from it for the same reason.

Attribution now runs once per sync over the range it touched, and is non-fatal
— a failure there leaves correct billed figures and the previous attribution in
place rather than failing the whole cost sync.

Verified against the branch for August: both single-owner users match their
workspace's billed total to the cent (SC-001). The restatement this implies:
Marlon $630.85 -> $254.66, Oliver $174.94 -> $23.37.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/lib/expected-spend.ts is pure: seat tiers take the tier-price path and are
byte-identical to before (pinned by a test against sumExpectedSpendCents);
usage tiers use measured consumption for a completed period, a trailing
3-complete-month mean for an open one, and the allowance only when there is no
history at all — marked as the placeholder it is. Months with no data are
skipped rather than counted as zero, and the period's own partial month never
feeds a projection.

Tiers gain a pricing model on create and edit; every price for a usage tier is
now labelled an allowance (tool detail, assignment dialogs and table, user
detail), with seat wording untouched. The budget's Expected column states its
basis whenever it is not purely tier prices, and combineExpectedSpend reports
the weakest basis so a placeholder is never hidden inside a total.

August on the branch: usage-tool expected spend moves from $3,650 of allowances
to $406.48 measured, while seat tools stay at $11,147 to the cent (SC-004).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/lib/credits.ts derives opening + purchases − consumption, counting only
events strictly after the as-of date so the opening balance is not
double-charged. No opening balance recorded means "unavailable", never a
balance derived from an assumed zero — and a negative balance is shown rather
than clamped, because it is the signal that a top-up is missing or the opening
figure is stale.

Server actions record, reverse and re-read a purchase, and record the opening
balance with the date an admin read it in the console. Period cost excludes
billed_costs rows whose invoice has a credit_purchases entry — by exclusion,
not by unpicking the link, so reclassifying stays reversible — and the
purchases are reported beside the cost, never inside it.

The Claude dashboard's credits panel stops saying the balance is unavailable
from Anthropic and shows the derived figure with its as-of date, the
opening/purchased/consumed arithmetic, and a plain statement that the Hub
derived it rather than reading it.

Claude Console's opening balance is recorded on the branch: $422.72 as of
2026-09-18, as supplied by the budget owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/lib/anthropic/reconciliation.ts compares billed against the Hub's own
COMPUTED figure — not the attributed one, which for a sole-owner workspace is
the billed number and would always agree, measuring nothing. Tolerance is the
larger of 5% and $5, so small workspaces stay quiet and large ones stay honest.

Run against live September data it immediately names the defect this feature
exists to close: Oliver 7.35x, Marlon 1.63x. Workspaces with spend and no owner
are collected into ONE line rather than one warning each — project and client
workspaces are unattributed by design, and per-workspace noise would teach an
admin to ignore the column; what matters is a PERSON's workspace appearing in
that list, which means their key stopped resolving.

The usage sync now names models missing from the price table, replacing the
pricing_resolved flag that nothing has ever read. Both kinds are marked
"Warning:" and the sync view says they are not failures.

Recorded caps stamp confirmed_at/confirmed_by on every save, and the workspace
list shows owners, the allowance-mismatch flag with both figures, and three
distinct states — no cap recorded / a recorded cap of $0.00 / a recorded cap —
each stating the Hub mirrors the console and does not enforce. Deprecated
workspaces are excluded from alerting and from the list by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Workspace ownership is listable and correctable. A third source, `excluded`,
says "this user is NOT an owner" and survives re-resolution — needed because a
project workspace's API key can belong to a person, and attributing the
project's spend to them would be wrong. Every ownership read honours it, and a
correction re-attributes immediately rather than waiting for the next sync.

The workspace list shows who each workspace is billed or apportioned to, or
states plainly that it is unattributed. Deprecated pools are out of the list and
out of alerting, reachable via an explicit toggle, with history untouched.

The 37 pooled Claude Console assignments are revoked with revoked_at =
2026-07-01 through revokeLicenseCore, which now accepts a backdate — `now()`
would have left licences that ended in June counting toward every period since.
Verified on the branch: 3 active assignments left ($375/month, down from
$3,650), 37 change-history rows written, and $12,010.42 of deprecated history
still readable.

Automations is excluded from ownership per spec OQ-4: its $5.26 stays visible
at org level and is attributed to nobody, which is the deferred project-
workspace case rather than a defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The announcement covers both restatements in one place, as planned: per-user
cost moving onto Anthropic's billed figure, and expected spend moving off
allowances, with the real before/after per person and per period. It says
plainly that the earlier numbers were overstated rather than that spending
fell, and that pre-July per-person figures are apportioned estimates of a known
total.

docs/anthropic-cost-accuracy.md now says up front that its architecture is
superseded, and CLAUDE.md records the feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The budget's expanded period shows credit purchases as their own row, labelled
a prepayment and kept out of the Actual column. The workspace card gains the
control the Automations case needed: an admin can say a workspace is not a
person's, which excludes its resolved owner and re-attributes immediately —
the key keeps resolving, the spend stays unattributed.

All 61 tasks are complete: lint clean, 818 unit tests and the attribution
integration suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 11:56
@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ai-developer-hub Ready Ready Preview Oct 2, 2026 1:46pm UTC

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ studert
❌ claude
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Core attribution, ownership, credit, forecasting, and sync-status paths contain unresolved correctness and security issues.

Review effort: Balanced
Findings: 6 High severity · 10 Medium severity

Open (16)
What changed in this PR

Introduces billed-cost attribution, usage-based forecasting, prepaid credits, workspace ownership, and cap tracking for Anthropic usage.

Changes:

  • Stores billed line items and attributes workspace costs to users.
  • Distinguishes seat costs from usage allowances and projections.
  • Adds credit balances, ownership controls, reconciliation, migrations, UI, and tests.
File Description
tests/​unit/​sync/​anthropic-workspace-backfill.test.ts Updates DB execution mock.
tests/​unit/​sync/​anthropic-cost-line-items.test.ts Tests line-item aggregation and rounding.
tests/​unit/​expected-spend.test.ts Tests pricing-model forecasts.
tests/​unit/​credits.test.ts Tests derived credit balances.
tests/​unit/​anthropic/​reconciliation.test.ts Tests divergence warnings.
tests/​unit/​anthropic-pricing.test.ts Tests new model pricing.
tests/​integration/​cost-attribution.test.ts Verifies attribution invariants.
src/​types/​index.ts Adds attribution, credit, and workspace fields.
src/​lib/​validators.ts Validates tier pricing models.
src/​lib/​sync/​sources/​anthropic-workspace.ts Ingests and attributes billed costs.
src/​lib/​sync/​sources/​anthropic-usage.ts Reports unknown pricing models.
src/​lib/​scenarios/​queries.ts Uses attributed costs in forecasts.
src/​lib/​profile-data.ts Returns billed-based profile costs.
src/​lib/​price-labels.ts Defines cost/allowance wording.
src/​lib/​measured-consumption.ts Loads monthly measured consumption.
src/​lib/​mcp/​data.ts Adds attribution to MCP responses.
src/​lib/​expected-spend.ts Implements expected-spend rules.
src/​lib/​db/​schema.ts Adds pricing, attribution, credit, and ownership schema.
src/​lib/​db/​migrations/​meta/​_journal.json Registers migration 0032.
src/​lib/​db/​migrations/​0032_curious_weapon_omega.sql Applies schema and data migration.
src/​lib/​credits.ts Derives prepaid balances.
src/​lib/​core/​tools.ts Persists tier pricing models.
src/​lib/​core/​assignments.ts Supports historical revocation dates.
src/​lib/​anthropic/​reconciliation.ts Detects billed/computed divergence.
src/​lib/​anthropic/​queries.ts Loads ownership, caps, and allowances.
src/​lib/​anthropic/​cost-attribution.ts Implements exact cost apportionment.
src/​lib/​anthropic-sync.ts Synchronizes resolved ownership.
src/​lib/​anthropic-pricing.ts Adds Claude 5 pricing and robust matching.
src/​components/​profile/​cost-tracking-section.tsx Displays attribution provenance.
src/​components/​error-popover.tsx Distinguishes sync warnings.
src/​components/​claude/​workspace-budget-list.tsx Shows owners, caps, and mismatches.
src/​components/​claude/​users-table.tsx Adds attribution badges.
src/​components/​claude/​org-credits-panel.tsx Displays derived credit balance.
src/​components/​claude/​attribution-badge.tsx Defines attribution labels.
src/​app/​users/​[id]/​user-detail-client.tsx Labels usage allowances.
src/​app/​tools/​[id]/​tool-detail-client.tsx Adds pricing-model controls.
src/​app/​claude/​page.tsx Loads credits and deprecated workspaces.
src/​app/​budget/​components/​period-allocations-table.tsx Shows bases and credit purchases.
src/​app/​assignments/​assignments-client.tsx Labels costs versus allowances.
src/​app/​assignments/​[id]/​assignment-detail-client.tsx Updates tier price wording.
src/​actions/​credits.ts Adds credit purchase and balance actions.
src/​actions/​budget.ts Applies usage forecasts and credit exclusions.
src/​actions/​anthropic-users.ts Switches user analytics to attributed costs.
src/​actions/​anthropic-global.ts Adds ownership and cap administration.
src/​actions/​alerts.ts Excludes deprecated workspaces.
specs/​045-usage-based-cost-model/​research.md Records design research.
specs/​045-usage-based-cost-model/​quickstart.md Documents verification steps.
specs/​045-usage-based-cost-model/​plan.md Describes implementation phases.
specs/​045-usage-based-cost-model/​contracts/​pricing-and-credits.md Defines pricing and credit rules.
specs/​045-usage-based-cost-model/​contracts/​cost-attribution.md Defines attribution rules.
specs/​045-usage-based-cost-model/​checklists/​requirements.md Tracks specification readiness.
scripts/​revoke-pooled-console-assignments.ts Revokes deprecated assignments.
scripts/​attribute-anthropic-costs.ts Provides manual re-attribution.
docs/​profile-api-integration-guide.md Documents attribution response fields.
docs/​anthropic-cost-accuracy.md Explains the superseded cost model.
docs/​045-restatement-announcement.md Communicates historical restatement.
CLAUDE.md Records the feature summary.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/actions/budget.ts
Comment on lines +662 to +665
pricingModel:
assignments.find((a) => a.pricingModel === "usage") !== undefined
? "usage"
: "seat",
Comment thread src/actions/budget.ts
Comment on lines +686 to +688
const creditPurchaseCents = period.billedCosts
.filter((bc) => creditPurchaseBilledCostIds.has(bc.id))
.reduce((s, bc) => s + bc.amountCents, 0);
-- intent, and nothing has ever compared them (045 W4).
COALESCE(o.owner_count, 0) AS owner_count,
o.owner_names,
o.allowance_sum_cents
Comment thread src/lib/db/schema.ts
table.toolId,
table.purchasedAt,
),
index("credit_purchases_invoice_id_idx").on(table.invoiceId),
Comment thread src/lib/expected-spend.ts
Comment on lines +101 to +108
// A completed period with data has a right answer; use it.
if (period.isComplete) {
const measured = period.months
.filter((m) => measuredByMonth.has(m))
.reduce((sum, m) => sum + (measuredByMonth.get(m) ?? 0), 0);
if (period.months.some((m) => measuredByMonth.has(m))) {
return { cents: measured, basis: "measured" };
}
Comment thread src/lib/mcp/data.ts
// otherwise. The field keeps its name and meaning; its VALUE is now
// billed-based for complete days.
costCents: sql<string>`coalesce(sum(coalesce(${anthropicUsageMetrics.attributedCostCents}, ${anthropicUsageMetrics.computedCostCents})), 0)`,
attributionMethod: sql<string>`coalesce(max(${anthropicUsageMetrics.attributionMode}::text), 'estimated')`,
Comment on lines +44 to +47
SELECT to_char(date, 'YYYY-MM') AS month, SUM(cost_cents)::bigint AS cents
FROM anthropic_workspace_costs
GROUP BY 1
ORDER BY 1
Comment thread src/lib/price-labels.ts
Comment on lines +41 to +45
export function priceWording(
pricingModel: PricingModel | null | undefined,
): PriceWording {
return pricingModel === "usage" ? USAGE : SEAT;
}
Comment thread src/lib/profile-data.ts
Comment on lines 176 to 180
const modelEntry: DailyModelCost = {
model: row.model,
costCents: row.computedCostCents,
costCents: rowCents,
inputTokens,
outputTokens: row.outputTokens,
Comment thread src/lib/sync/sources/anthropic-usage.ts Outdated
Comment on lines +81 to +87
const unknown = await unknownModelWarning();
if (unknown) {
const warning = `Warning: ${unknown}`;
counts.errorCount += 1;
counts.errorMessage = counts.errorMessage
? `${counts.errorMessage}; ${warning}`.slice(0, 1000)
: warning.slice(0, 1000);
Reconciliation always has something to report — project workspaces have
spend and no owner by design — and every finding went through appendError,
which increments errorCount. Two consequences, both found on the preview
deployment rather than in tests:

- Every cost sync ended `partial`, every hour, because the framework marks a
  run partial whenever errorCount > 0.
- The dashboard's "last synced" indicator froze. The cost sync stamps the
  sentinel row behind it only on a run with no errors, so after the first
  finding it would never update again, making a healthy sync look stopped.

The usage sync's unpriced-model warning had the same flaw.

appendSyncWarning (src/lib/sync/warnings.ts) records a finding in
errorMessage, the field the sync view renders whatever the outcome, without
touching errorCount. Real errors still count, and still keep the indicator
from claiming a full sync. The popover now decides "warning, not a failure"
from the run's outcome rather than from the message prefix, which only
worked because errors happened to be appended first.

The unit tests missed this because their mocked database returns no rows, so
no warning ever fired. The new run-level tests answer the reconciliation
query with real-shaped rows; all three fail against the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note saying what Expected is based on sat inline in the column header.
Table headers do not wrap (whitespace-nowrap), so the sentence stretched the
Expected column across the table. It is now an info icon beside the header,
with the explanation in a tooltip: seat licences use the tier price, and
usage-based tools use what was spent for closed months and the 3-month
average for open ones, because their licence price is an allowance rather
than a cost. The icon is a focusable button, so keyboard users get the
tooltip too, and it is absent on a seat-only budget, leaving that header
unchanged.

Checked in a real browser against the worktree database: the Expected
header is now 115px, against 194px for Planned and 133px for Actual, and the
table fits inside its card.

basisLabel in src/lib/expected-spend.ts lost its only caller and is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@studert
studert merged commit 46ba568 into main Oct 2, 2026
6 of 7 checks passed

This branch was successfully deployed

1 active deployment
Preview — e8523cf9 Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants