A fixed subscription with a metered money allowance.

Interactive companion to the prepaid usage RFC · team-docs/priorities/billing/2026-07/prepaid-usage.md

The customer pays a fixed recurring amount every period: the allowance ($30 in the examples below, at $1 per unit). Usage is rated to money at report time and draws a visible balance down. At renewal the invoice bills the full allowance again: the unused allowance expires with the period, while top-up money carries over until spent. Everything else is bookkeeping that keeps this correct under concurrency, refunds and dunning.

🧭 Update, Aug 4 (v2): the team decided both open directions: the ledger is money-denominated (the stable-currency requirement), and the renewal arithmetic is fixed billing: every period bills the full allowance, the unused allowance expires, and only top-up money carries over. This page is updated to v2; the $ wallet section maps the decided model.
Guided tour

Allowance = the recurring price

No new plan fields. With flat-rate pricing the item quantity is the included unit count, and the allowance is billed as a fixed recurring charge every period.

allowance = price x quantity

Grants are the credit side

A paid invoice item mints a money-denominated UsageGrant row; usage is rated at report time and debits the pool. The balance is always computed, never trusted.

balance = active grants - rated usage

Fixed billing, top-up carry

Every renewal bills the full allowance. The unused allowance expires with the period (breakage); unused top-up money carries over until spent.

carry = persistent remainder
One period on one screen allowance $30/period · flat $1/unit
momentinvoicegrant ledgerbalance
purchase$30.00 paid upfront (the allowance)+$30.00 (period-invoice)$30.00
a month of usagenothing extra: usage is prepaid17 reports rated at $1.00, snapshotted$13.00
top-up$5.00 ad hoc invoice, paid+$5.00 (top-up, persistent)$18.00
settlement + renewalthe full $30.00 billed again (fixed)$13.00 allowance expires (breakage) · carry-over +$5.00$35.00 once paid
The whole design in one sentence: a fixed subscription with an included money allowance: usage burns it at your current rates (expiring money first), the unused allowance dies with the period, and top-up money stays until spent.
Decision map click a node
fixed-N derived pending Prepaid usage Refill-to-N Grant ledger Grant on payment DB row lock invariant 6 postpaid gates ▲ Decision log ⏳
⏳ awaiting decision with a closure criterion
▲ risk hotspot most of the implementation risk
✕ rejected alternative, kept with its rationale
A decision tree, not a network: every decision keeps its satellite, the rejected alternative. You see not only what was chosen but what it was chosen over.
The RFC in 6 cards: what it solves, the key decisions, where the risk lives.

What it shipsPrepaid usage (v2): the customer pays a fixed recurring money allowance, rated usage draws down a visible balance, exhaustion blocks, top-ups extend; at renewal the invoice bills the full allowance again, the unused allowance expires, and only top-up money carries over.

✓ scope closed
The backbone is a new money-denominated, order-scoped grant ledger (UsageGrant): paid prepaid invoice items mint grants (amount = the paid item amount), rated usage draws the balance down, refunds revoke exactly the refunded money. The deliverable is merchant-complete: plan configuration, upfront invoicing, balance API and app view, enforcement, notifications, manual top-up, settlement with the expiry split. Auto top-up is an explicit follow-up, designed to ride the same mechanism. → try it live in the simulator
"At renewal the invoice always bills the full allowance: the unused part of the allowance expires with the period, while unused top-up money carries over until consumed. The mental model is a phone plan: a fixed monthly fee with included consumption, plus extra credit you bought that stays yours."→ Summary (v2) · appetite: 6 weeks · the full loop in the Basic example table

Decision · 0Renewal arithmetic (v2): fixed billing. Every renewal bills the full allowance; the unused allowance expires; top-up money carries over.

✓ settled in team review (Aug 4)
Settled in the Aug 4 team review, superseding the refill-to-N arithmetic of v1: fixed billing gives predictable recurring revenue and the familiar subscription mental model, and the balance stays self-limiting because the allowance expires at settlement (breakage, reported per period). What survives for the customer: top-up money never expires and never pays twice, and the consumption order burns expiring money first. A side effect: the minimum-usage floor question is moot, the fixed bill is the revenue floor. → run the "Rolling a top-up surplus" scenario in the simulator
✕ Superseded: refill-to-N (v1; Alternative #1 in v2): the renewal bills only max(0, allowance - carryOver) and the whole balance carries. Remains a possible future plan option for strict pay-for-the-difference semantics. The accumulate-everything fixed variant (bill the full allowance and stack the whole remainder on top) also stays rejected: unbounded organic growth that would need cap machinery.
"At renewal the invoice always bills the full allowance: the unused part of the allowance expires with the period, while unused top-up money carries over until consumed."→ Summary (v2) + Alternatives #1 · resolved, no merchant-call dependency

DecisionA new UsageGrant ledger, because the derived balance does not hold.

✓ closed in review
The first draft hoped for a derived balance; review sank it on three facts: the issued InvoiceItem has no order item reference, Usage is keyed by (order, plan), and carry-over makes the balance cumulative across periods, so a derived read would aggregate everything since inception on every query. The ledger is append-only, money-denominated (v2), corrects through revocation, and every grant traces to a paid invoice item or a settlement's persistent remainder.
✕ Rejected: derived balance (#1) · an FK on InvoiceItem (#2, "fixes attribution but not the cumulative carry-over read") · a mutable counter as system of record (#9, at-least-once InvoiceWasPaid would need a dedup row, which is a grant row stripped of its fields).
"…with carry-over the balance is cumulative across periods, so a purely derived balance would aggregate every row since inception on every read. These facts together are why this design records grants explicitly."→ Motivation, constraint 2 + Alternatives #1, #2, #9 · Drawbacks: "the machinery is the honest cost"

InvariantCorrectness rests on the DB row lock, not the Redis mutex: the Redis lock is fail-open.

✓ closed in review
The per-order Redis mutex is best-effort: on acquisition failure the platform logs and executes anyway. So every balance-affecting write runs in a transaction with SELECT … FOR UPDATE on the order row (the pool is order-scoped in v2). The parent row, deliberately: locking grant rows cannot block concurrent INSERTs of new rows (the phantom problem). One owner: UsageGrantManager::withPoolLock; every caller goes through it. Consequence: writes serialize per order; the per-order throughput ceiling is measured against the postpaid baseline during phase 1, and the GA thresholds are derived from that measurement. → test yourself: quiz question 2
"This lock is best-effort: on acquisition failure the platform logs and executes anyway… Balance correctness therefore cannot rest on Redis; it rests on database-level locking."→ Motivation constraint 3 + Correctness under concurrent writes · RedisLock.php:60-69

DecisionMoney is granted when the invoice is paid, not when it is issued.

✓ closed in review
Honest prepaid: no credit before the money arrives. When the payment lands, the listener finds the order item the paid line belongs to via a deterministic key (order + root plan external id + period overlap); the match is unique because an order may hold at most one item per root prepaid plan (invariant 8). The listener runs inside the payment flow, so it never fails the payment: on ambiguity it skips the grant, alerts ops, and the usage-grants:settle replay mints it later. The cost: merchants whose customers pay invoices later (net terms, for example net-30) would leave their customers balance-less until payment, so they wait for the deferred issuance mode.
✕ Rejected (v1): pending grants minted at issuance and activated on payment (#8): trivial mapping, but an extra state machine and issuance-time writes inside InvoiceFactory, and it buys nothing while invariant 8 holds. It stays the designated fallback and the natural shape of the future net-terms mode.
"If pre-existing data still yields multiple candidates, the listener must not fail the payment… it skips the grant, logs an error with a metric and an ops alert… the uniqueness-invariant path is the committed design, not a preference."→ Detailed design, Grant on payment + Alternatives #8 · Unresolved questions #2

RiskSix postpaid paths must be gated: "this is where most of the implementation risk lives".

▲ risk focus
Lifting the plan invariant exposes prepaid items to code written under postpaid assumptions. The ugliest is gate 3: today's time-prorated cancellation credit is written for plain prepaid items; for prepaid usage the policy is an open v2 decision (#4: recommended, credit the unused top-up money, none for the expiring allowance). All six gates touch code running for every merchant; the safety net is the billing-timing guard pattern and the postpaid test suites passing unchanged. → test yourself: quiz question 4
"…today's prorated cancellation credits prepaid items by time remaining. For prepaid metered that refunds consumed units (prepay 30, consume 30 on day one, cancel mid-month, get half the money back)."→ Gating the existing postpaid paths, 1 to 6 · v2: the credit policy is decision #4

OpenSix decisions remain (churn credit, discounted funding, backdated rating among them); the phase 1 starting slice depends on none of them and can start.

⏳ decision log
Every open question is recorded in the RFC with a recommended default, an owner and a closure criterion. The v1 decisions 0 (renewal arithmetic) and 4 (balance bounds) are resolved by v2 (fixed billing; the allowance expires, persistent money is uncapped) and moved to Alternatives. The money-correctness phases wait; phase 1 (entity, migration, the three Usage columns, read-only fields) is deliberately decision-independent.
"The decisions below block the money-correctness phases of the rollout (phases 2 to 5); the phase 1 starting slice does not depend on any of them."→ Unresolved questions · Adoption strategy, 5 phases + the final invariant lift
Decision log · verbatim from Unresolved questions
#DecisionRecommended defaultOwnerClosed when
1Order generation scopingv1 on the new orders generation only: the aggregates this RFC modifies (RecurringOrder, UsageManager, OrderManager::renew) belong to it, and the legacy side duplicates the domain logic (LegacyRecurringOrder, LegacyOrderItemsFactory), so parity would double the risky surface. Legacy merchants adopt by moving to the new generation (new-orders flag), not by backportplatform / orders ownerrecorded as committed in this RFC; the /usage-top-ups controller framework and the webhook family split (classic versus orders-experimental-*) fixed; the legacy purchase guard (see Gating) specified
2Is grant-on-payment enough for GAyes; the grant-on-issuance mode for net-terms merchants is deferredproductGA release note states the limitation; an eligibility guard excludes net-terms configurations
3Refund distribution policyexact-amount FIFO revocation (v2: no unit conversion, no rounding rule); credit memos remain the precise pathproduct + financefinance sign-off on the FIFO distribution and the negative-balance presentation
4Churn credit for the unused balancecredit the unused persistent (top-up) balance; no credit for the allowance remainder (a period fee that expires at settlement anyway). Alternative on the table: add a time-prorated allowance credit on topproduct + financepolicy chosen and encoded in gate 3; finance sign-off
5Discounted funding amountthe grant follows the invoice item's list amount, not the paid amount: preserves the goodwill workaround (a fully discounted top-up still grants)product + financepolicy chosen; coupon applicability to ad hoc invoices verified in implementation
6Backdated report ratingrate at the report-time price and document it; the platform keeps no price historyproductrecorded in the API reference

Interactive simulator: try the mechanics

allowance $30/period (30 units at $1, so unit numbers are dollar numbers) · soft limit at $5 remaining · v2 rules: fixed billing, top-up carry

Period 1. The customer paid the $30 allowance (30 units at $1): the grant ledger holds $30 active. Consume away and watch what happens!
Controls you are the merchant's integration
Scenarios from the test matrix, narrated
Pick a scenario: the simulator steps through it on its own, explaining every move.
Balance the pool's composition (consumption order)
allowance = 30
30 units
period 1 · active grants: 30 · used: 0
allowance (burns first, expires) carry-over (persistent) top-up (persistent)
⛔ Balance exhausted or negative: usage reports bounce with 409
Why does it look like this? The coloring is the fixed consumption order of v2: expiring money (the allowance) burns first, then carry-over, then top-ups. Settlement is a pure function of aggregates under this order: that is what makes the expiry split well defined.
Balance timeline hover over the points
usage top-up renewal refund / 409
Money view: what does MRR see? 1 unit = 1 USD
Event log what your integration would see

Under the hood: tables, the ledger, the lock

The state you built in the simulator, as the database sees it. The ledger below still holds every grant your clicks minted, and writes keep flashing here while you play.

Database: what changes? writes flash live · click a table
NEW FK: orderItemId NEW table backed by linked at settlement Plan Order🔒 Invoice InvoiceItemuntouched OrderItem (Subscription) funding item / rate card CustomerBalance ✕ hands off Usage order, plan, quantity… + orderItemId · unitPrice · amount UsageGrant NEW TABLE source: period-invoice | top-up | carry-over amount · revokedAmount effectiveTime periodStart · periodEnd invoiceItemId (nullable FK) orderId (FK: the pool) 🔑 uniq(invoiceItemId, source)
Blue = the RFC's additive schema changes. The 🔒 shows when an operation locks the order row (the pool) with SELECT … FOR UPDATE: every balance-affecting write goes through it (withPoolLock).
Grant ledger append-only: a row is never deleted
GrantSourceQuantityRevokedPeriod
Race for the last unit why isn't Redis enough?
Client A
Client B
Suppose 1 unit remains and two clients report at exactly the same time. Start it and watch.

🎯 Before you press it…

Misconception quiz: the full bank all seventeen questions in one place · the guided tour serves them per step
API changes what lands in website/api-definitions · everything additive, zero renamed identifiers

The spec surface is deliberately small (v2): two plan schemas lose one sentence, the order read model gains a read-only money balance, the usage response gains its rating snapshot, one endpoint family is new, and the three usage-limit webhooks (both the classic and the orders-experimental variants) gain two payload fields. SDKs regenerate from the spec; no client code breaks.

Spec fileChangeWhat exactly
components/schemas/SubscriptionPlan.yaml
components/schemas/StorefrontPlan.yaml
modifiedThe sentence "Metered billing plans must be postpaid." is removed; the description gains the prepaid mode with its constraints (`sum` strategy; the funding item may be fixed-fee or flat-rate, consuming items must be flat-rate; brackets rejected).
components/schemas/Order schemas
components/schemas/SubscriptionItem.yaml
additiveThe order read model gains the read-only money `usageBalance` object: `allowanceAmount`, `grantedAmount`, `carriedOverAmount`, `usedAmount`, `remainingAmount`, `currency`, period window. Items gain per-item rated aggregates (`usedQuantity`, `usedAmount`); the funding item `quantity` description gains its included-units meaning.
components/schemas/UsageLimits.yamlmodifiedThe soft limit (amount denomination) documents the prepaid evaluation rule: the soft event fires when `remaining <= allowance - softLimit.amount`; quantity limits keep their meaning as per-item spend restrictions.
components/schemas/Usage.yaml
paths/usages.yaml
paths/usages@{id}.yaml
additiveThe usage resource gains the rating snapshot: read-only `unitPrice` and `amount`, plus `remainingAmount` on the report response, so the caller learns the money balance with every report (the request keeps `quantity`). A new `PUT /usages/{usageId}` accepts a client-generated id for safe retries: replaying the same id returns the stored record instead of double-recording.
paths/usage-top-ups.yaml
paths/usage-top-ups@{id}.yaml
components/schemas/UsageTopUp.yaml
newThe top-up resource, amount-denominated in v2: create (immediate ad hoc invoice for the requested `amount`), fetch, list. Create via `POST` (server id) or `PUT` with a client-generated id for safe retries: the same id-based idempotency shape as `/usages`, no separate key field. Tag and operationIds registered in openapi.yaml; code samples added.
webhooks/soft-usage-limit-reached.yaml
webhooks/hard-usage-limit-reached.yaml
webhooks/orders-experimental-*-usage-limit-reached.yaml
additivePayloads gain `grantedAmount` and `remainingAmount` (the money truth, next to the existing quantity fields). No new event types; both webhook families get the same fields.
Before · SubscriptionPlan.yaml:131

"Use metered billing when an exact quantity is unknown. Report usage during a service period and charge customers afterwards. Metered billing plans must be postpaid."

After (drafted, v2)

"Report usage during a service period. Postpaid plans charge for reported usage afterwards. Prepaid plans charge a fixed recurring allowance upfront, rate each report to money at your current price, and draw a visible balance down; unused allowance expires at renewal while top-up money carries over."

Plan config (POST /plans, single-product form)
{
  "pricing": { "formula": "flat-rate", "price": 1.00 },
  "billingTiming": "prepaid",
  "meteredBilling": { "strategy": "sum" }
}
Allowance at purchase (POST /orders, fragment)
{ "items": [
  { "planId": "plan_0YV...", "quantity": 100 }
] }
// allowance = 100 x $1.00 = $100 / period, billed fixed
Order read model (new usageBalance, money)
{
  "usageBalance": {
    "allowanceAmount": 100.00,
    "grantedAmount": 120.00,
    "carriedOverAmount": 0.00,
    "usedAmount": 91.00,
    "remainingAmount": 29.00,
    "currency": "USD",
    "periodStart": "2026-07-01T00:00:00Z",
    "periodEnd": "2026-08-01T00:00:00Z"
  }
}
Usage report (PUT /usages/usg_0YV...; request keeps quantity)
{ "subscriptionId": "sub_0YV...", "quantity": 3 }

{
  "id": "usg_0YV...",
  "quantity": 3,
  "unitPrice": 1.00, "amount": 3.00,
  "remainingAmount": 29.00
}
Top-up (PUT /usage-top-ups/utu_0YV... → 201; amount in v2)
{ "orderId": "ord_0YV...",
  "amount": 20.00 }

{
  "id": "utu_0YV...",
  "amount": 20.00, "currency": "USD",
  "invoiceId": "in_0YV...",
  "status": "pending"
}
Extended usage-limit event payload
{
  "subscriptionId": "sub_0YV...",
  "orderItemId": "...",
  "usageQuantity": 35,
  "usageAmount": 35.00,
  "grantedAmount": 120.00,
  "remainingAmount": 29.00
}
Cheat sheet second screen during the meeting
Formulas (v2)
balance = sum(amount - revokedAmount) of current-period grants - rated usage(period)
rating = amount = quantity x unitPrice snapshot at report time, never recomputed
renewal = bills the full allowance, every period (fixed billing)
settlement = usedFromAllowance = min(used, allowance) · the allowance remainder expires (breakage)
carry-over = the persistent remainder (top-up money) only, minted as a grant

Semantics
the report that empties the balance : 201 + hard-usage-limit-reached
the report that would exceed it : 409 (or clamped with acceptPartialQuantity)
why 409, not 402 : 402 is reserved with no settled semantics; exhaustion keeps the postpaid hard-limit contract
soft limit (prepaid reading) : fires when remaining <= allowance - softLimit.amount
consumption order : expiring money first (allowance), then carry-over, then top-ups
grant minting : on InvoiceWasPaid, amount = the paid invoice item (identity)
concurrency guard : DB row lock on the order row (the pool), not Redis
refund revocation : exactly the refunded amount, FIFO; never claws back consumed money
negative balance : only blocks; it never flows into an invoice
✅ decided · Aug 4 · v2: money denomination + fixed billing

The $ wallet direction: one pool of money per order

Decided on Aug 4 (v2): the pool is money, usage burns it through per-unit rates, the $ balance is the source of truth for limits and remaining, and the renewal arithmetic is fixed billing: every period bills the full allowance, the unused allowance expires, top-up money carries over. The machinery of the unit RFC (row lock, unique keys, level-triggered revocation, derived balance) survives unchanged; this section maps the decided model.

Entity map click any box for its role and the open questions attached to it
bills the fixed allowance InvoiceWasPaid → mint +$ refund → revokedAmount POST /usages: quantity in, rated at the item's price adds +$ burns -$ checked on every report: remaining · soft limit (amount) · hard stop Order (recurring) owns exactly one pool · pool currency = order currency v1 invariant: one funding item funds, any number of rate-card items draw. cross-order sharing: deferred (see Open) Funding item exactly one (v1) plan: Credits · $60 / period budget = its recurring price renewal bills the full budget (fixed) Rate-card item · Pages flat-rate $0.02 / unit · bills nothing usage burns the shared pool Rate-card item · Review flat-rate $0.10 / unit · bills nothing its own rate, same pool Invoice / InvoiceItem first + renewal invoices fund the budget top-up: ad hoc invoice linked to the order refund: revokes pool money UsageGrant $ denominated source: period-invoice | top-up | carry-over amount · revokedAmount · invoiceItemId period snapshot · same unique keys v2: expiryTime · priority Usage quantity: what the merchant reports unitPrice* · amount* (* = report-time snapshot) rated once, never recomputed Balance (derived, never stored) + Σ active grants of the period - Σ rated usage of the period hard stop at $0 · soft limit in $ optional cache under the row lock
Blue border = new or changed versus today · dashed green = derived, never a system of record · the lock, idempotency keys and reconciliation from the unit RFC apply unchanged.
$ is the source of truth · units are input and evidence click a stage
UNIT WORLD · input and evidence MONEY WORLD · the system of record Merchant metering counts calls, GB, transactions knows nothing about $ POST /usages { quantity: 1500 } contract unchanged Rating at report time 1500 × $0.02 = $30.00 the only unit → $ crossing Usage row · snapshot quantity · unitPrice · amount evidence, never recomputed $ ledger · balance grants - rated usage quantity appears nowhere why units stay: Rebilly must know what the quantity means in $ (Adam), and a price change must never require a merchant redeploy purely $: grants · balance · limits · renewal top-up · auto-reload · hard stop at $0.00
One sentence for the meeting: the books are $-only; units survive in exactly two places, as the reporting input and as evidence on the usage row.
One lifecycle, step by step the unit simulator's story, denominated in money

pool balance
$0.00
Period 1
sourceamountrevokednote
What changes vs the committed unit RFC the machinery survives; the denomination flips
aspectv1 RFC (units, superseded)v2 wallet (decided)
denominationN units, N = the item quantitymoney, in the order currency
renewal arithmeticrefill = max(0, N - carryOver), everything carriesfixed billing: the invoice is always the full allowance; the unused allowance expires (breakage); only top-up money carries
grant mintinginvoice item quantity mapped to unitsan identity: grant amount = the paid invoice item amount
usage report storesquantityquantity + unitPrice + amount, snapshotted at report time (a price change never rewrites history)
refund revocationpro-rata money-to-units, 0.001 floor roundingexactly the refunded amount; the conversion and the rounding rule disappear
formula constraintfixed-fee or flat-rateflat-rate required on rate-card items (a unit rate must exist to burn money); fixed-fee stays fine for the funding item
pool scopeone item = one poolone order = one pool; exactly one funding item (v1 invariant)
multi-productnoyes: every rate-card item burns the shared pool at its own price
top-up requestquantityamount
expiry / consumption orderdeferred into the allocation rabbit holetwo classes now: the allowance expires at settlement, persistent money does not; expiring money burns first; finer windows deferred
Open decisions for the meeting recommended defaults marked
  1. Resolved in v2. Denomination (money, in the order currency), pool topology (one order-level pool, exactly one funding item, rate-card items), rating (server-side snapshot at report time), ledger shape (UsageGrant plus a derived balance, no operation stream), renewal arithmetic (fixed billing) and carry bounds (the allowance expires; persistent money is uncapped).
  2. Churn credit (decision 4). Recommended: credit the unused persistent (top-up) balance, none for the expiring allowance; alternative: add a time-prorated allowance credit on top. Owner: product + finance.
  3. Discounted funding (decision 5). Does the pool receive the list amount or the paid amount? List preserves the goodwill workaround (a fully discounted top-up still grants). Owner: product + finance.
  4. Refund distribution (decision 3). Exact-amount FIFO over the invoice's grants; finance signs off the distribution and the negative-balance presentation.
  5. Backdated rating (decision 6). Rate at the report-time price and document it; the platform keeps no price history. Owner: product.
  6. Auto top-up shape. The Anthropic-style auto-reload: when the balance drops to $X, top up to $Y, charging max(0, Y - remaining). A follow-up, not v1, but the v1 top-up endpoint is its building block; ship it with safety caps. follow-up
Decided in the Aug 4 team review (v2), building on Adam's stable-currency requirement and the Jul 29 Slack thread. The v1 unit-based material elsewhere on this page is retained for history with superseded quotes marked.