Problem
server/src/services/costs.ts computes apiRunCount (billing_type = metered_api) and subscriptionRunCount (subscription_included + subscription_overage) in byAgent and byProvider (and the same pattern in byBiller). Every other billing type — credits, fixed, unknown — is counted by neither. Any agent billed purely via credits (e.g. GitHub Copilot/OpenRouter lanes, see #11421) shows apiRunCount: 0, subscriptionRunCount: 0 forever, even though costCents is correctly non-zero. Verified live: 528/529 cost events on the instance are correctly classified by billing_type, but both counters read 0 for agents on the credits lane.
Decision (VP Eng, AGE-352)
Ship the additive fix now: add creditRunCount alongside the existing two counters. Do not introduce a runCountsByBillingType map in this pass — that's a breaking API/UI change and isn't warranted for a 3-bucket problem. Revisit the map shape only when the UI is touched for other reasons.
fixed and unknown billing types have no confirmed production traffic today (grep BILLING_TYPES in packages/shared/src/constants.ts for the full enum) — fold them into creditRunCount's sibling: add otherRunCount covering fixed + unknown so the three existing buckets (api/subscription/credits) plus a catch-all account for 100% of billingType values with no silent drops. Do not add a fourth named counter per type — that's the option-1-grows-forever problem the report calls out.
Scope
server/src/services/costs.ts:
byAgent: add creditRunCount, otherRunCount (mirrors existing apiRunCount/subscriptionRunCount count-distinct-heartbeat-run pattern).
byProvider: same two fields.
byBiller: same two fields (not named in the original report but has the identical bug — same apiRunCount/subscriptionRunCount-only pattern).
byAgentModel: no run-count fields exist there today (confirmed) — out of scope, do not add net-new fields to that endpoint in this ticket.
packages/shared/src/types/cost.ts: add creditRunCount: number and otherRunCount: number to CostByAgent and CostByProviderModel (byProvider's return type) and CostByBiller.
- UI:
ui/src/pages/Costs.tsx (by-agent card run-count line), ui/src/components/BillerSpendCard.tsx, ui/src/components/ProviderQuotaCard.tsx — extend the existing api/subscription display pattern to also show credits/other counts when non-zero. Follow the existing row.apiRunCount > 0 ? ... : "0 api" idiom; do not restructure the layout.
storybook/stories/budget-finance.stories.tsx fixtures — add the two new fields (can default to 0 in existing stories, non-zero in at least one to visually confirm rendering).
Acceptance criteria (machine-checkable)
server/src/__tests__/costs-service.test.ts gains a test that inserts cost_events rows with billing_type = 'credits' only (no metered_api/subscription rows for that agent) and asserts byAgent(...) returns creditRunCount > 0 for that agent, apiRunCount === 0, subscriptionRunCount === 0. Same assertion shape for byProvider and byBiller.
- A second fixture row with
billing_type = 'fixed' or 'unknown' asserts otherRunCount > 0 on the same three endpoints.
pnpm --filter server test costs-service passes.
pnpm --filter @paperclipai/shared build (or the repo's type-check target covering packages/shared) passes with the new fields on CostByAgent, CostByProviderModel, CostByBiller.
- UI change is additive only — no existing snapshot/test for
Costs.tsx, BillerSpendCard.tsx, or ProviderQuotaCard.tsx breaks group of tests, if any exist; if none exist, a manual screenshot in the PR showing the new count segment rendering for a credits-only row is acceptable evidence.
- No changes to
byAgentModel's shape.
Out of scope / explicitly rejected
runCountsByBillingType map replacement (option 2 from AGE-352) — rejected for this pass, breaking change not justified yet.
- Changing the UI to show cost+tokens per billing type instead of counters (option 3) — rejected, throws away the existing UI contract for no benefit over the additive fix.
Ref: AGE-352 (Paperclip issue), parent AGE-344.
Problem
server/src/services/costs.tscomputesapiRunCount(billing_type = metered_api) andsubscriptionRunCount(subscription_included + subscription_overage) inbyAgentandbyProvider(and the same pattern inbyBiller). Every other billing type —credits,fixed,unknown— is counted by neither. Any agent billed purely viacredits(e.g. GitHub Copilot/OpenRouter lanes, see #11421) showsapiRunCount: 0, subscriptionRunCount: 0forever, even thoughcostCentsis correctly non-zero. Verified live: 528/529 cost events on the instance are correctly classified by billing_type, but both counters read 0 for agents on the credits lane.Decision (VP Eng, AGE-352)
Ship the additive fix now: add
creditRunCountalongside the existing two counters. Do not introduce arunCountsByBillingTypemap in this pass — that's a breaking API/UI change and isn't warranted for a 3-bucket problem. Revisit the map shape only when the UI is touched for other reasons.fixedandunknownbilling types have no confirmed production traffic today (grepBILLING_TYPESinpackages/shared/src/constants.tsfor the full enum) — fold them intocreditRunCount's sibling: addotherRunCountcoveringfixed+unknownso the three existing buckets (api/subscription/credits) plus a catch-all account for 100% ofbillingTypevalues with no silent drops. Do not add a fourth named counter per type — that's the option-1-grows-forever problem the report calls out.Scope
server/src/services/costs.ts:byAgent: addcreditRunCount,otherRunCount(mirrors existingapiRunCount/subscriptionRunCountcount-distinct-heartbeat-run pattern).byProvider: same two fields.byBiller: same two fields (not named in the original report but has the identical bug — sameapiRunCount/subscriptionRunCount-only pattern).byAgentModel: no run-count fields exist there today (confirmed) — out of scope, do not add net-new fields to that endpoint in this ticket.packages/shared/src/types/cost.ts: addcreditRunCount: numberandotherRunCount: numbertoCostByAgentandCostByProviderModel(byProvider's return type) andCostByBiller.ui/src/pages/Costs.tsx(by-agent card run-count line),ui/src/components/BillerSpendCard.tsx,ui/src/components/ProviderQuotaCard.tsx— extend the existing api/subscription display pattern to also show credits/other counts when non-zero. Follow the existingrow.apiRunCount > 0 ? ... : "0 api"idiom; do not restructure the layout.storybook/stories/budget-finance.stories.tsxfixtures — add the two new fields (can default to 0 in existing stories, non-zero in at least one to visually confirm rendering).Acceptance criteria (machine-checkable)
server/src/__tests__/costs-service.test.tsgains a test that insertscost_eventsrows withbilling_type = 'credits'only (no metered_api/subscription rows for that agent) and assertsbyAgent(...)returnscreditRunCount > 0for that agent,apiRunCount === 0,subscriptionRunCount === 0. Same assertion shape forbyProviderandbyBiller.billing_type = 'fixed'or'unknown'assertsotherRunCount > 0on the same three endpoints.pnpm --filter server test costs-servicepasses.pnpm --filter @paperclipai/shared build(or the repo's type-check target coveringpackages/shared) passes with the new fields onCostByAgent,CostByProviderModel,CostByBiller.Costs.tsx,BillerSpendCard.tsx, orProviderQuotaCard.tsxbreaks group of tests, if any exist; if none exist, a manual screenshot in the PR showing the new count segment rendering for a credits-only row is acceptable evidence.byAgentModel's shape.Out of scope / explicitly rejected
runCountsByBillingTypemap replacement (option 2 from AGE-352) — rejected for this pass, breaking change not justified yet.Ref: AGE-352 (Paperclip issue), parent AGE-344.