Skip to content

feat(levelcode): denominate the customer-facing balance in credits, not dollars#380

Merged
ndemianc merged 2 commits into
developfrom
feat/credits-display
Jul 24, 2026
Merged

feat(levelcode): denominate the customer-facing balance in credits, not dollars#380
ndemianc merged 2 commits into
developfrom
feat/credits-display

Conversation

@ndemianc

Copy link
Copy Markdown
Member

Balances are now shown in credits: $1 = 100 credits, so $12.79 left reads as 1,279 credits and the $100 Ultra plan is a 10,000-credit allowance. The reason is behavioural, not technical — a shrinking DOLLAR balance reads as money being lost, so people ration it; an allowance reads as something to spend.

PRESENTATION ONLY. The ledger, metering, Stripe prices and invoices all stay in micro-dollars, because dollars are what providers charge and what we actually bill; re-denominating stored balances would put Stripe reconciliation at risk for no gain, and would be painful to undo. Nothing about the economics moves.

The mapping needed no new maths: budget_micros == price × CREDIT_COGS_RATIO and retail_micros divides that ratio back out, so a plan's retail budget already IS its price — which makes the credit allowance exactly the price in CENTS. Pro 2,000 / Pro+ 4,000 / Max 6,000 / Ultra 10,000, verified against the running app.

  • CREDITS_PER_DOLLAR + MICROS_PER_CREDIT: the unit, defined once beside the margin constant it derives from.
  • micros_to_credits / plan_credits: conversion + the derived allowance.
  • roster_for gains per_turn_micros — one turn's cost at RETAIL, the same unit as the balance, so the dashboard can print "N credits/turn" beside "≈ turns left" using a single conversion. Extracted per_turn_cost_micros so that figure and turns_left are computed from the SAME cost and cannot drift.
  • Plan feature copy now leads with the credit allowance.

Verified: 968 examples, 0 failures. New specs pin the allowance to price_cents (the numbers the marketing copy prints), prove it survives a CREDIT_COGS_RATIO change, and assert balance ÷ per_turn_micros reproduces turns_left for every model on the plan. That last one is mutation-checked: dropping the routing fee from per_turn_micros so it diverges from turns_left fails the suite.

…ot dollars

Balances are now shown in credits: $1 = 100 credits, so $12.79 left reads as
1,279 credits and the $100 Ultra plan is a 10,000-credit allowance. The reason is
behavioural, not technical — a shrinking DOLLAR balance reads as money being
lost, so people ration it; an allowance reads as something to spend.

PRESENTATION ONLY. The ledger, metering, Stripe prices and invoices all stay in
micro-dollars, because dollars are what providers charge and what we actually
bill; re-denominating stored balances would put Stripe reconciliation at risk for
no gain, and would be painful to undo. Nothing about the economics moves.

The mapping needed no new maths: budget_micros == price × CREDIT_COGS_RATIO and
retail_micros divides that ratio back out, so a plan's retail budget already IS
its price — which makes the credit allowance exactly the price in CENTS. Pro
2,000 / Pro+ 4,000 / Max 6,000 / Ultra 10,000, verified against the running app.

- CREDITS_PER_DOLLAR + MICROS_PER_CREDIT: the unit, defined once beside the
  margin constant it derives from.
- micros_to_credits / plan_credits: conversion + the derived allowance.
- roster_for gains per_turn_micros — one turn's cost at RETAIL, the same unit as
  the balance, so the dashboard can print "N credits/turn" beside "≈ turns left"
  using a single conversion. Extracted per_turn_cost_micros so that figure and
  turns_left are computed from the SAME cost and cannot drift.
- Plan feature copy now leads with the credit allowance.

Verified: 968 examples, 0 failures. New specs pin the allowance to price_cents
(the numbers the marketing copy prints), prove it survives a CREDIT_COGS_RATIO
change, and assert balance ÷ per_turn_micros reproduces turns_left for every
model on the plan. That last one is mutation-checked: dropping the routing fee
from per_turn_micros so it diverges from turns_left fails the suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 00:30

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.

Pull request overview

This PR updates Levelcode’s customer-facing display to denominate balances and plan allowances in “credits” (where $1 = 100 credits), while keeping all internal accounting/metering/Stripe reconciliation in micro-dollars.

Changes:

  • Introduces credit denomination constants and helpers (CREDITS_PER_DOLLAR, MICROS_PER_CREDIT, micros_to_credits, plan_credits).
  • Adds per-turn retail cost (per_turn_micros) to the roster payload to support dashboard “credits/turn” display.
  • Updates plan feature copy and adds specs to pin allowance-to-price and per-turn/turns-left invariants.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/levelcode.rb Adds credit conversion + plan allowance helpers and exposes per-turn retail cost in roster output; updates plan feature strings.
spec/models/levelcode_plans_spec.rb Adds specs for credit conversion, allowance derivation, and per-turn economics invariants.
spec/requests/api/levelcode/v1/account_spec.rb Extends API request coverage to assert per_turn_micros is present and reasonable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/levelcode.rb Outdated
Comment thread lib/levelcode.rb Outdated
…n (PR #380 review)

Both review points were real, and the first was worse than it looked.

1. per_turn_micros was ROUNDED RETAIL while turns_left still came from COST
   micro-$ via turns_in_budget. The two therefore rounded independently, so
   "balance ÷ credits-per-turn" did not reproduce "≈ turns left" — the dashboard
   prints them side by side and a user divides one into the other by eye.
   Measured before fixing: 3,440 disagreements in 28,000 sampled balances (~12%).
   My spec had asserted a SINGLE balance and passed anyway, which is the actual
   lesson — one value proves nothing about a rounding boundary.

   There is now one definition of the per-turn figure (retail_per_turn_micros),
   and both turn counts divide it via turns_in_retail_budget. They agree by
   construction rather than by coincidence. The margin ratio cancels in the
   division, so the reported counts are unchanged. Re-measured after: 0
   disagreements in the same 28,000 combinations.

   turns_in_budget (the COST-domain variant) is gone rather than left unused —
   keeping a second helper that computes the same thing in a different unit is
   exactly how this drift would come back.

2. per_turn_cost_micros returned a Float (integer cost x the float routing fee)
   despite a name promising micro-dollars. Now rounded to an Integer like
   cost_micros, so the imprecision cannot propagate into the counts.

Verified: 969 examples, 0 failures. The invariant spec now sweeps 400 balances
across every model on the plan instead of one, and a new spec pins the Integer
types end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ndemianc
ndemianc merged commit d10ea43 into develop Jul 24, 2026
3 checks passed
@ndemianc
ndemianc deleted the feat/credits-display branch July 24, 2026 01:03
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