Skip to content

fix(costs): price subscription runs from a rate card instead of booking $0 as reported - #10544

Open
phattbeats wants to merge 7 commits into
paperclipai:masterfrom
phattbeats:fix/cost-event-rate-card
Open

fix(costs): price subscription runs from a rate card instead of booking $0 as reported#10544
phattbeats wants to merge 7 commits into
paperclipai:masterfrom
phattbeats:fix/cost-event-rate-card

Conversation

@phattbeats

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The cost ledger is part of that. It writes one cost_events row per run, and the Costs dashboard, the per-agent spend comparison, and the budget guard all read from it
  • Many adapters now run against a flat-rate subscription instead of a metered API key. The Claude Code CLI reports total_cost_usd: 0 on those runs, because no API call was billed
  • The ledger accepted that 0 as a measured fact. Its guard tested the type of the value, not whether the value was credible
  • The result is worse than a missing number. A gap shows as a gap. A cost_status='reported' zero shows as a confirmed zero, and it is indistinguishable from genuinely free work
  • This pull request makes an implausible cost fail closed. It also adds a rate card, so subscription usage gets a notional price instead of nothing
  • The benefit is that subscription burn becomes visible. Cash accounting and budget enforcement do not change

Linked Issues or Issue Description

No public GitHub issue exists for this. The problem is described inline below, following .github/ISSUE_TEMPLATE/bug_report.yml.

What happened?

Every agent run on a subscription-authenticated adapter writes a cost_events row with cost_status = 'reported' and cost_cents = 0, next to correct and non-zero token counts.

resolveLedgerCostStatus in server/src/services/heartbeat.ts decides the status:

return input.costUsd == null && hasTokenUsage ? "unpriced" : "reported";

The unpriced branch needs costUsd to be null. The Claude Code CLI emits a well-formed numeric 0 under OAuth auth, and 0 != null, so the branch never runs. On the instance where this was found, cost_status='unpriced' had zero rows for all time, across the whole database. The guard was dead code on this path.

Expected behavior

A run that used millions of tokens must not be recorded as a confirmed $0. Either the ledger prices the run, or it marks the cost as unknown. It must not assert zero.

Steps to reproduce

  1. Configure a claude_local agent with OAuth or subscription auth, not an API key.
  2. Run a heartbeat that uses a non-trivial number of tokens.
  3. Read the new row:
    select model, cost_status, cost_cents, input_tokens, output_tokens, cached_input_tokens
    from cost_events order by created_at desc limit 1;
  4. The row has real token counts, cost_cents = 0, and cost_status = 'reported'.

Scale observed

One instance, one week, provider anthropic: 156 of 172 rows and 822,597,919 tokens, all reported at $0.00. The same tokens priced at list are about $326.

Environment

  • Paperclip commit: 4813ed3 (master)
  • Deployment mode: self-hosted, Docker
  • Adapters involved: claude_local; the ledger change also affects any adapter that reports a subscription cost
  • Database mode: external PostgreSQL 17
  • Node.js: v24.18.0
  • OS: Linux

Supersedes #10476 — same commits, same author. That PR was opened from a branch whose name carried an internal ticket id, which the contributing checklist disallows. Renaming the branch closed it automatically, so this PR replaces it. #10476 has no review history to preserve: its review gate never passed.

Related pull requests (searched, not duplicates)

What Changed

  • cost_status gains a third value, derived. The guard now asks whether the reported cost is credible, not only whether it is non-null. Real token usage plus a zero or absent cost can no longer produce reported. It becomes derived when the rate card can price it, and unpriced when it cannot.
  • A run with no token usage still reports the provider figure, including a legitimate zero. There is nothing to price in that case.
  • New column rate_card_cents holds the notional list price. It is separate from cost_cents on purpose. cost_cents still means actual cash, it stays 0 for subscription_included, and it stays the only input to budget enforcement.
  • New column cache_write_tokens. Cache-creation tokens were folded into input_tokens, so the cache-write premium was not recordable on any adapter. The claude-local parser and the ACPX engine now report the field separately.
  • New packages/shared/src/model-rate-card.ts holds per-model list prices in cents per million tokens, for input, output, cache read, and cache write.
  • New packages/db/src/backfill-cost-event-rate-card.ts recomputes rate_card_cents and the status for existing rows. The token counts on disk are already correct, so no run must be repeated.
  • Migration 0197_cost_event_rate_card.sql adds both columns. It is additive and uses ADD COLUMN IF NOT EXISTS.
  • Storybook cost fixtures and one server test expectation are updated for the widened types.

Verification

Automated:

npx vitest run packages/shared/src/model-rate-card.test.ts
npx vitest run packages/adapters/claude-local/src/server/parse.test.ts
cd server && npx vitest run src/__tests__/heartbeat-cost-accounting.test.ts
cd server && npx vitest run src/__tests__/claude-local-execute.test.ts
cd ui && npx tsc -b

The accounting test suite asserts the rule directly. It proves that token usage plus a zero cost can never resolve to reported, that cost_cents stays 0 for subscription_included, and that rate_card_cents is computed from tokens.

Manual, on a live instance:

  1. Apply the migration.
  2. Run one heartbeat on a subscription-authenticated claude_local agent.
  3. Read the new row:
    select model, cost_status, cost_cents, rate_card_cents, cache_write_tokens
    from cost_events order by created_at desc limit 1;
  4. The row must show cost_status='derived', cost_cents=0, and rate_card_cents > 0.
  5. Confirm agents.spent_monthly_cents did not change.

A backfill of 6,694 historical rows on one instance moved them to derived. cost_cents stayed 0 for every row.

Risks

  • Migration safety: low. The migration only adds two columns. Both are NOT NULL DEFAULT 0, so existing rows get 0 and no read path breaks. There is no data loss and no rewrite of existing values.
  • cost_status gains a value. Any consumer that switches on the status must handle derived. In-tree consumers are updated. An out-of-tree dashboard that assumes two values will see an unknown status.
  • Rate cards go stale. The table is static and list prices change. A stale entry makes rate_card_cents wrong. It cannot make cost_cents wrong, so it cannot affect billing or budgets.
  • rate_card_cents must not be read as money owed. It is notional. Nobody is invoiced for it. This is the reason it is a separate column and not a correction to cost_cents. A reviewer who prefers the opposite policy should read [codex] Count subscription-included usage in budgets #5724, which proposes counting these estimates against budgets. This PR does not, because the budget guard enforces real spend, and an estimated figure would let a rate-card error stop real work.
  • Rows without a rate card entry. An unknown model resolves to unpriced rather than a wrong price. This is a visible gap, which is the intended failure mode.
  • Behavioral shift in reporting. Dashboards that previously showed $0 for these runs will now show a non-zero notional figure. That is the point of the change, but it will look like a sudden increase in spend to anyone who does not read the column name.

Roadmap Note

ROADMAP.md lists Better Budgeting as delivered, and it names "clearer spend visibility". This PR is adjacent to that item, so a maintainer should confirm the direction before merge.

Two points argue that it does not duplicate the roadmap work. It is a bug fix, not a feature: the existing unpriced status was unreachable on this path, and the fix restores the behaviour the ledger already intended. It also leaves budget enforcement exactly as it is, because cost_cents is untouched. If a maintainer prefers this to land as part of a larger budgeting effort, redirect it and I will rework the shape.

Model Used

Claude Opus 5, model id claude-opus-5, 1M context configuration (claude-opus-5[1m]), with extended thinking and tool use, running in Claude Code.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work (see Roadmap Note — it is adjacent to "Better Budgeting", which is already marked delivered)
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds notional rate-card pricing for subscription-backed runs and surfaces it throughout cost reporting.

  • Adds derived cost status, rate-card and cache-write token fields, schema migration, and historical backfill tooling.
  • Propagates separate cache-write usage through Claude Local and ACPX accounting.
  • Updates cost-service aggregations and dashboard components to display subscription rate-card equivalents without changing cash-budget enforcement.
  • Adds regression coverage for pricing, aggregation, parsing, and mixed-billing presentation.

Confidence Score: 4/5

The PR is not yet safe to merge because its historical backfill still persists understated rate-card values for rows containing folded cache writes.

The current backfill explicitly leaves pre-migration cache-creation tokens inside input_tokens while cache_write_tokens remains zero, so those historical tokens are recomputed at the ordinary input rate rather than the cache-write premium; the previously reported dashboard visibility issues are otherwise fixed at current HEAD.

Files Needing Attention: packages/db/src/backfill-cost-event-rate-card.ts

Important Files Changed

Filename Overview
packages/db/src/backfill-cost-event-rate-card.ts Adds an idempotent rate-card backfill, but historical cache writes remain knowingly priced at the lower ordinary-input rate.
server/src/services/costs.ts Propagates rate-card and cache-write totals through cost aggregation endpoints and fixes subscription-aware project ordering.
server/src/services/heartbeat.ts Derives notional prices for credible token usage while preserving cash cost as the budget input.
packages/shared/src/model-rate-card.ts Introduces centralized model normalization and token-category rate-card pricing.
ui/src/components/BillerSpendCard.tsx Correctly displays subscription rate-card equivalents in biller, billing-type, and provider breakdowns.
ui/src/pages/Costs.tsx Updates cost views to show notional subscription spend and correctly handles mixed cash and subscription model rows.

Reviews (11): Last reviewed commit: "fix(costs): decide cash vs rate card per..." | Re-trigger Greptile

Comment thread server/src/services/costs.ts
Comment thread packages/db/src/backfill-cost-event-rate-card.ts
@phattbeats

Copy link
Copy Markdown
Author

Thanks — both findings are valid. One is fixed in 874eab3; the other is real and I'd like to argue it belongs in a follow-up.

Issue 2 (backfill misprices historical cache writes) — confirmed, and deliberately not "fixed"

Correct diagnosis. Pre-migration rows folded cache-creation tokens into input_tokens, the migration defaults cache_write_tokens to 0, so those tokens get the ordinary input rate instead of the 1.25x write premium. Historical rate_card_cents is low.

I did not write a correction, because the split was never recorded and is not recoverable. Anything I computed — a fixed cache-write ratio, a per-model heuristic — would be a guess presented as data, in a column whose entire purpose is to stop guesses from reading as facts. That seemed like the wrong trade for this particular field.

What 874eab3 does instead:

  • Documents the understatement in the file header, with the reason it can't be recovered.
  • Counts the affected rows (cache_write_tokens = 0 with non-zero input_tokens) and prints a note at runtime, so nobody reads the backfilled total as exact.

Scope of the error is bounded: it applies only to rows written before this change, the affected span stops growing the moment it merges, and the figure is notional — it never reaches cost_cents, so no cash total, budget guard, or invoice is affected. Happy to add a heuristic if you'd prefer a closer number over a defensible one, but I'd want that to be an explicit call.

Issue 1 (subscription costs remain invisible in the UI) — real, and the natural next PR

Also correct: costs.ts now returns rateCardCents / subscriptionRateCardCents, and the production dashboard components still render costCents only, so subscription rows keep showing $0.00.

I'd like to land this PR without it, for two reasons:

  1. This PR is the ledger fix. Its claim is that a token-bearing run can no longer be recorded as a confident zero — that's now true at the data layer and covered by tests. The UI work is a different surface, different reviewers, and different risk.
  2. Rendering it needs a labelling decision I shouldn't make alone. rate_card_cents is notional. Putting it in a column that today means cash is exactly how someone ends up reading it as dollars owed — which is the failure mode this whole change exists to prevent. It needs its own treatment and wording, not a swapped field.

The columns, the aggregates, and the shared response types are all in place, so the UI PR is additive and small. If you'd rather see them together, say so and I'll fold it in here instead.

Also in 874eab3

Removed internal tracker ids from three comments (backfill-cost-event-rate-card.ts, model-rate-card.test.ts, heartbeat-cost-accounting.test.ts), per CONTRIBUTING → "No Internal Issue References". Verified: packages/db typecheck clean, model-rate-card.test.ts 20/20, heartbeat-cost-accounting.test.ts 56/56.

@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch from 874eab3 to 8723f46 Compare August 1, 2026 06:23
phattbeats added a commit to phattbeats/paperclip that referenced this pull request Aug 2, 2026
… (PHA-1654)

Implements House's HYBRID verdict from PHA-1643 research. Two coupled
changes on cost_events:

1. New non-nullable `pricing_methodology` column with allowed values
   'measured' / 'pre_cache_write_aware' / 'unpriced', enforced by a
   CHECK constraint. Lets dashboards and BI tools tell measured rows
   apart from rows that were priced at the input rate because
   cache-write tokens were folded into input_tokens before the 0198
   migration.

2. `rate_card_cents` is now nullable. Subscription auth / unlisted
   models record NULL with `pricing_methodology='unpriced'` rather
   than zeroing the column, so the silence is not mistaken for a fact.

The migration backfills pre-0199 rows to `pre_cache_write_aware` and
flips their `cost_status` to `reported_pre_migration`. Both UPDATEs
are idempotent (only touch rows still at their defaults). The
backout path is documented as a commented block in the migration.

New writes from the heartbeat are always `measured` (or `unpriced`
when the rate card is NULL). The cost service insert now carries
`pricing_methodology` through. The shared `CostEvent` type uses
`rateCardCents: number | null` and adds `pricingMethodology:
PricingMethodology` so the column lands in the same response shape
PHA-1640 will read.

PHA-1643 stays in_review as the research record. PHA-1640 (dashboard
rendering) is unblocked once PR paperclipai#10544 merges — the new column lands
in the response shape here, so PHA-1640 should not need to redo its PR.

PR paperclipai#10544 status: open, unmerged (mergeable: false at the time of
writing). This branch sits on top of paperclipai#10544's HEAD so it auto-rebases
when paperclipai#10544 lands. If paperclipai#10544 closes without merging, this branch
will need to be rebased onto master directly.
@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch from 8723f46 to ed85bd3 Compare August 3, 2026 00:47
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch 3 times, most recently from ed18edf to 1708709 Compare August 4, 2026 06:45
@phattbeats

Copy link
Copy Markdown
Author

Both Greptile P1s addressed. One was right and is now fixed in code; the other is right about the arithmetic but not fixable from stored data, so it is disclosed instead.

P1: "Subscription costs remain invisible" — valid, fixed in 6b2f806

This was the real one, and it went to the heart of the change: the service returned subscriptionRateCardCents, the routes shipped it, the shared types declared it, the storybook fixtures carried it — and not one production component read it. grep -rn "rateCard" ui/src/ returned zero matches. BillerSpendCard takes a CostByBiller that has the field and reads only row.costCents; ProviderQuotaCard does the same with CostByProviderModel. The data arrived at the render layer and was dropped there, so every dashboard would still have shown $0.00 for subscription usage after this PR merged. A backend-only fix would have shipped the number into a void.

Added ui/src/components/RateCardEquivalent.tsx — one shared tag, so the treatment is identical everywhere — and wired it into the surfaces that were lying:

  • By-agent list (Costs.tsx) — the ranking this PR exists to fix. It sorted the heaviest consumer cheapest.
  • BillerSpendCard and ProviderQuotaCard headline figures, plus the per-model rows.

It renders only when there is subscription usage, and it is always labelled rate card with a hover explainer: what these runs would have cost at list price; they bill against a plan, not per token. That labelling is deliberate and load-bearing. The figure is correct for comparing agents against each other and for catching a runaway, and wrong for an invoice — nobody should be able to read it off a dashboard as cash owed.

While in there I also fixed a second-order version of the same bug: the per-model share was modelRow.costCents / row.costCents, which renders 0% on every row of a subscription-only agent. It now falls back to the rate-card basis when there is no cash to divide by.

Covered by RateCardEquivalent.test.tsx (4 tests, passing), including that it stays silent on zero and on non-finite input rather than printing a bogus figure.

P1: "Backfill misprices historical cache writes" — correct, and disclosed rather than guessed

The arithmetic is right. Rows written before cache_write_tokens existed folded those tokens into input_tokens, so the backfill prices them at the input rate instead of the 1.25x cache-write premium, and the historical rate card is a slight underestimate.

It is not recoverable. The split was never stored, so any correction would be a guessed ratio presented as data — worse than a known, stated understatement. Handled in 1708709 by documenting it in the file header and printing the affected-row count at runtime, so the number cannot be read as exact. On the current dataset that is 5,986 of 7,244 rows. cost_cents is unaffected; this only touches the notional rate card.


Full suite was green on the previous head — Build, all four server shards, all four serialized suites, e2e, typecheck. This push adds UI-only changes plus one new test file. UI typechecks clean.

Comment thread ui/src/components/BillerSpendCard.tsx
@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch from 64c1b81 to 8e2825a Compare August 4, 2026 18:17
@phattbeats

Copy link
Copy Markdown
Author

Greptile's third P1 is correct, and it is the same mistake one layer down. Fixed in 8e2825a5f.

What was wrong

My previous commit put the rate-card equivalent in BillerSpendCard's header and stopped there. Both breakdowns inside the card still aggregated only costCents:

current.costCents += entry.costCents;   // subscriptionRateCardCents dropped

So for a subscription-only biller the card totalled $158.68 at the top and then listed every upstream provider beneath it at $0.00. The spend was visible but not attributable — you could see the company had a runaway and not which provider caused it, which is most of the value of the breakdown.

The billing-type list had it worse: the row labelled "Subscription included" summed costCents and therefore rendered $0.00 by construction. That row can never be anything but zero cash. It was a guaranteed lie.

Fix

  • Both providerBreakdown and billingTypeBreakdown now carry subscriptionRateCardCents, rendered through the same RateCardEquivalent tag as everywhere else, so the "rate card" label and its hover explainer stay attached to the number.
  • Both sort on cash + rate card. Sorting on cash alone pinned the heaviest consumer to the bottom of the list at a notional zero — the same ranking inversion this PR exists to fix, reproduced inside the card.

On the tests

Worth being explicit, because the last round of tests is what let this through. My storybook fixtures carried subscriptionRateCardCents, it typechecked, the story rendered — and the component ignored the field, so the fixtures proved nothing.

BillerSpendCard.test.tsx renders the real component. Four of its five tests fail against the previous commit and pass on this one; the fifth is a negative control asserting metered-only usage shows no rate card at all.

Each assertion is scoped to the section under test, via a section() helper. That is not incidental — the header repeats the same totals, so an unscoped expect(container.textContent).toContain("$158.68") passes while the provider rows still render $0.00. My first draft of these tests did exactly that and only 2 of 5 failed against the old code. Scoping them took it to 4.

Also in this push

Rebased onto ded813ad6. Master added migrations 0203 and 0204 while this sat, so mine renumbered 0203_cost_event_rate_card0205_cost_event_rate_card; the journal is now master's chain with one entry appended. 204 journal entries, 204 SQL files, no duplicate tag or filename prefix. (The journal's duplicate idx: 178 is upstream's and predates this branch.)

Unchanged: the second P1 about historical cache-write pricing stands as previously answered — arithmetically right, not recoverable from stored data, so it is disclosed in the file header and counted at runtime rather than guessed at.

@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch from 8e2825a to efedb54 Compare August 4, 2026 20:02
@phattbeats

Copy link
Copy Markdown
Author

Rebased onto master tip 678728f65. The PR had gone CONFLICTING since the last push — master landed two migrations while this sat, and one of them took my number.

The collision

feat(issues): contain cross-issue agent side effects (#10837) added 0205_narrow_shiva, and feat: maintained in_review review-path contract (#10675) added 0206_review_path_recovery_idempotency_index. Mine was 0205_cost_event_rate_card, so both the filename prefix and the journal index were taken.

Renumbered to 0207_cost_event_rate_card, idx 207, appended to master's chain. git mv on the SQL file so history follows it. Only _journal.json conflicted; the other 27 files auto-merged clean.

I ran the repo's own guard rather than eyeballing the journal, because a clean git rebase will happily produce a journal that passes review and fails at boot:

$ tsx src/check-migration-numbering.ts   → pass
$ tsx src/check-migration-safety.ts      → pass (20 historical findings covered by baseline)

No duplicate tag, no duplicate 4-digit prefix. The journal does carry one duplicate idx: 178 — that is upstream's, present before this branch existed; I diffed it against origin/master to be sure.

Verified on the rebased tree

packages/shared typechecks. model-rate-card.test.ts 20/20, BillerSpendCard.test.tsx + RateCardEquivalent.test.tsx 9/9. Diff unchanged in substance: 28 files, +1350/−47.

On the two red checks

Neither is a code failure, and I want to be exact about the second one rather than wave it off:

  • Contributor trustaction_required, scores the phattbeats account rather than the diff. 43 other open PRs carry it.
  • Greptile Review — its own summary says it: "Greptile reviewed this pull request successfully — this check reflects your team's confidence threshold, not a review failure. The review scored 4/5, below the 5/5 this repository requires." Its last substantive pass was the 12:06Z P1 on BillerSpendCard, which is fixed. No new findings on either of the last two heads.

zabolotiny's approval has now survived four force-pushes. Ready when a maintainer is.

Comment thread ui/src/pages/Costs.tsx Outdated
phattbeats and others added 7 commits August 8, 2026 18:04
…ng $0 as reported

The Claude Code CLI emits a well-formed `total_cost_usd: 0` when it runs under
OAuth / subscription auth. The ledger's cost-status guard was type-check-only
(`costUsd == null`), so that numeric zero sailed through and every subscription
run was written as an authoritative `cost_status='reported'` zero. In practice
that meant ~822M tokens/week of real inference was recorded not as a gap in the
data but as a confirmed $0 — indistinguishable from genuinely free work, and
invisible to anyone looking at spend.

What changes:

- `cost_status` gains a third value, `derived`. The guard now asks whether the
  provider gave a *credible* cost, not merely a non-null one: real token usage
  plus a zero/absent cost can never be `reported` again. It becomes `derived`
  when we can price it and `unpriced` when we cannot.

- A new `rate_card_cents` column carries the notional token x list-price figure.
  `cost_cents` intentionally still means actual cash — it stays 0 for
  subscription-included runs, and it remains the only figure that feeds budget
  enforcement, so budgets and overage behaviour are untouched.

- A new `cache_write_tokens` column makes cache-write spend recordable for the
  first time. Cache-creation tokens carry a write premium over plain input and
  were previously folded into `input_tokens`, which flattened the distinction
  and hid the premium. Producers now report them on their own field.

- `packages/shared/src/model-rate-card.ts` holds the published list prices
  (USD per million tokens, with dated tiers for introductory pricing) and
  derives the notional figure from a model id plus a token count. An unlisted
  model yields `null`, which surfaces as `unpriced` rather than a silent zero.

- `packages/db/src/backfill-cost-event-rate-card.ts` recomputes the two new
  columns and `cost_status` for historical rows from the token counts already
  stored on them.
…mate

The first pass keyed `reported` off `costUsd > 0`. That misses the case it
was written for: the Claude Code CLI prints a rate-card `total_cost_usd`
(~$2.35 on a real run) even under subscription auth, while
`normalizeBilledCostCents` zeroes the cash because nothing is metered. Live
rows therefore kept landing as `reported` with `cost_cents = 0` and real
tokens - the exact shape this issue exists to eliminate - and disagreed with
the historical backfill, which classifies off `cost_cents` and marked
identical rows `derived`.

Classify off the billed cents instead, so live and backfilled rows agree and
a subscription run is `derived` (priced from the rate card) rather than an
authoritative zero.
The rate-card change widened CostByProviderModel, CostWindowSpendRow, and
CostByBiller with cacheWriteTokens/rateCardCents (plus the subscription
variants), and added cacheWriteTokens to the adapter usage summary. The
storybook fixtures and one server assertion were not updated, which broke
the UI typecheck (18 TS2739) and one server test.
…ternal ids

Greptile flagged that the backfill prices pre-migration cache-creation tokens
at the ordinary input rate. That is correct. Those tokens were summed into
input_tokens and never stored separately, so the split is not recoverable and
any correction would be a guessed ratio presented as data. Documented in the
file header and surfaced at runtime with a count of affected rows, so the
backfilled figure cannot be read as exact. cost_cents is unaffected.

Also removes internal tracker ids from three comments, per CONTRIBUTING.
The service already returned subscriptionRateCardCents and the API already
shipped it, but no production component read it. Every dashboard therefore
still printed $0.00 for subscription-included runs, which is the failure this
change set exists to fix: the heaviest token consumers looked free and sorted
cheapest.

Adds one shared RateCardEquivalent tag, used on the by-agent list, the biller
card, and the provider card plus its per-model rows. It renders only when there
is subscription usage, and is always labelled "rate card" with an explainer on
hover, because this figure is right for comparing agents and wrong for an
invoice.

Also fixes the per-model share split, which divided by costCents and so showed
0% on every row of a subscription-only agent. It now falls back to the
rate-card basis when there is no cash to divide by.
BillerSpendCard's header showed the biller-wide rate-card equivalent, but
both of its breakdowns aggregated only costCents. For a subscription-only
biller that meant the card totalled correctly while every row beneath it
read $0.00 — the billing-type row labelled "Subscription included" most
of all. The spend was visible but not attributable to a provider.

Both aggregations now carry subscriptionRateCardCents, and both sort on
cash plus rate card so a subscription-only provider is not pinned to the
bottom of the list at a notional zero.

Tests render the component rather than asserting on fixtures: four of the
five fail against the previous component. The header repeats the same
totals, so each assertion is scoped to the section under test — an
unscoped one passes while the section itself still renders $0.00, which
is how this survived the first round.
The by-agent model breakdown chose between cash and the rate-card
equivalent from the agent's own total. That works for an agent whose
usage is entirely one kind and fails for any agent holding both: because
the aggregate was positive, every subscription row rendered "$0.00 (0%)"
with no tag, hiding the subscription half of a mixed agent in the one
view built to surface it.

Each model row already carries exactly one billing type, so the choice
belongs to the row. Percentages now share a cash + rate-card basis, so a
mixed agent's rows still sum to 100% instead of running past it.

Extracts the rule into ui/src/lib/agent-model-share.ts with tests, so
"which rows get a rate-card tag" is pinned rather than restated inside
JSX — this is the fourth review round to catch a variant of the same
mistake, each one a layer further in.

Also promotes SUBSCRIPTION_BILLING_TYPES to @paperclipai/shared. The SQL
that decides which rows feed subscriptionRateCardCents and the UI that
decides which rows get a tag were about to hold separate copies of that
answer; they now read one.

Closes the last instance of the class in By project, which rendered a
bare $0.00 for subscription-funded projects and sorted them last on cash.
@phattbeats
phattbeats force-pushed the fix/cost-event-rate-card branch from d363a5d to 483ae48 Compare August 8, 2026 22:10
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.

2 participants