ADR-012: Billing vs loyalty, credits and currency classification
Status: Accepted 2026-09-22. Gates Wave 8 (OpenVibe.Billing), Wave 9 (Tips) and Wave 10 (VIP).
Context and current evidence
A code survey on 2026-09-22 found twenty economic units across Live, Network and Games (the full inventory with file references is docs/economic-inventory.md in OpenVibe.Billing). The findings that drive this decision:
- Vibes exist as two mutable columns on Live's
users: a spendable balance bought with real money (Stripe, PayPal, CCBill, NOWPayments, PowerChat) and a cashout balance that is paid out by hand over PayPal. A donation moves bought Vibes into the recipient's withdrawable balance; the recipient can also "recycle" cashout back into spendable. - The Vibes ledger (
transactions) is written best-effort after the balance changes, is mutable, and has no idempotency keys;payment_ordershas no unique provider reference; there is no refund, chargeback or dispute handling for any provider; a user deletion cascades away that user's provider receipts. The ledger's type CHECK does not allowrecycle, so recycling fails after the balances moved. - Card, PayPal and crypto rails are switched off in production (
payments_enabled=false); only PowerChat carries money today, at very low volume, and almost no balances are non-zero. The economy is close to dormant, which makes this the cheapest moment to move money onto a real ledger. - OpenCoins (Network wallet) already has a ledger with unique idempotency keys; it is earned by activity, never bought, and spent on media requests. A user-to-user transfer API exists but has no caller.
- Channel points are per-streamer, earned by watching and chatting, spent on streamer rewards and media requests; mutable balance, partial log, no keys.
- Cosmetics and chat tags are unlocks without balances. Arena levels are XP. Scraplandia bottle caps are items in the game world.
- Several legacy units are frozen (Live "gold", Camp Funds, Hobo Coins, the old Live game).
Decision
Every unit belongs to exactly one class. The class decides which system owns it and what it may ever be converted into.
| Class | Meaning | Units | Owner | |---|---|---|---| | MONEY | Real-money value received, owed or paid out | provider receipts and payment orders; Vibes cashout balances (creator payable); cashout requests; site-routed PowerChat donations; subscription payments; platform revenue (purchase spread, retained subscription share, site-route fee) | OpenVibe.Billing | | CREDIT | Prepaid value bought with money, spendable only on OpenVibe, never withdrawable by the buyer | Vibes spendable balance | OpenVibe.Billing (a liability until spent) | | ENTITLEMENT | A right granted by a payment (or by a plan), with a start and an end | channel subscriptions; future VIP perks | OpenVibe.Billing holds the entitlement truth; OpenVibe.VIP holds plans and perks | | LOYALTY | Earned by activity, no cash value, not bought, not withdrawable, not transferable between people | OpenCoins; channel points; Arena XP | Network (OpenCoins) and Live (channel points) until Billing's loyalty ledger exists; never mixed with MONEY/CREDIT accounts | | COSMETIC-ENTITLEMENT | An unlocked item, no balance, not tradable | global cosmetics; chat tags | the product that renders it (Live today) | | GAME-STATE | Gameplay truth inside a game world | bottle caps, inventory, reputation, unlocks, market stock | OpenVibe.Games | | LEGACY-ARCHIVE | Frozen; kept for reconciliation and claims only | Live openvibe_coins_balance ("gold"), Camp Funds, Hobo Coins, old Live game tables, hobo-quest backups | read-only in place | | EXTERNAL | Money that never touches OpenVibe | direct PowerChat tips to a streamer's own account | recorded as an interaction by Tips; no Billing liability |
Rules that follow
- Only Billing moves MONEY and CREDIT. Balances become ledger accounts in a balanced, append-only double-entry journal (
user_credit:<subject>,creator_payable:<subject>,provider_clearing:<provider>,platform_revenue,payouts_pending,refunds). No product keeps a mutable money column once Billing is authoritative. - Every provider event is recorded once. Webhooks land in Billing, are stored as immutable receipts keyed by
(provider, provider_event_id), and settle with idempotency keys; replays produce no second effect. Refunds, reversals and chargebacks are first-class journal entries that trace to the original transaction; a chargeback on credit that was already donated leaves the recipient's payable intact and books the loss against platform revenue, pending review. - CREDIT → MONEY happens only by giving it to someone else (a tip or paid interaction). The buyer can never withdraw it. Self-dealing is refused by subject, not by local id.
- MONEY → CREDIT (recycle) stays allowed as an explicit journal entry, or is retired by product decision; it is never an unrecorded balance swap.
- LOYALTY never converts to MONEY or CREDIT, is never bought, and is not transferable between people. Network's
network.coins.transfergrant is revoked; the endpoint stays only for staff tooling. Loyalty ledgers keep idempotency keys that are deterministic per event (noDate.now()/random keys). - Cosmetics are not tradable. The dormant "deactivate to game item for trading" path stays dead. A future paid cosmetic is an ENTITLEMENT bought through Billing.
- Game state never bridges to MONEY, CREDIT or LOYALTY without a new ADR.
- Legacy balances are never shown as live. Frozen "gold" stays for a future claim flow and is removed from live displays.
- Test money is marked, not mixed. Test transactions carry a test flag in Billing and never count toward creator or platform totals.
- Payouts are a separately controlled capability. Approving a cashout requires a payout reference from the provider; the escrow period is enforced by Billing, not only displayed.
- Freeze switch first. Billing ships with an economy freeze (writes refused, reads served) before any balance moves.
Alternatives considered
- One Network ledger for Vibes, OpenCoins and channel points (earlier platform plan): rejected; it mixes loyalty with money and puts a money ledger inside the identity service (roadmap anti-goal 4 and rule 11 of section 7.2).
- Treat Vibes as a single MONEY unit: rejected; bought-but-unspent Vibes are a prepaid liability with different rules (not withdrawable) from creator payables.
- Keep migrating balances as columns: rejected; the absence of atomicity, keys and refunds is the problem being solved.
Migration consequences
- Billing imports: every
payment_ordersrow as a provider receipt (status preserved; duplicates by provider reference reconciled and reported), everytransactionsrow as historical journal entries markedimported, and opening balances per account equal to the current Live columns. Any difference between the replayed history and the columns is booked to an explicitimport_adjustmententry and listed for review; nothing is silently absorbed. - Test-era rows before
stats_vibes_reset_atare imported flagged as test. - Live becomes a Billing client for checkout, donations, cashout, subscriptions and balance reads; its columns freeze (legacy, read-only) at cutover. Provider webhooks move to Billing's endpoints; the PowerChat webhook is re-pointed.
- Subscriptions become Billing entitlements; VIP later adds plans and perks on top.
- OpenCoins and channel points are not migrated into Billing in Wave 8. They stay where they are, keep their ledgers, and get deterministic keys; Billing's loyalty ledger is a later, separate step.
Rollback
Until cutover, Live remains authoritative and Billing only shadows (imports and reconciles). After cutover, rollback means freezing Billing, exporting its journal, and re-deriving Live's columns from it; the cutover runbook keeps the last Live snapshot.
Acceptance tests
- A duplicated provider webhook produces exactly one accounting effect; a replayed idempotency key returns the original result.
- A refund, reversal or chargeback traces to its original transaction and keeps history immutable.
- The journal balances (sum of all entries is zero) after every operation and after import.
- Credit cannot be withdrawn by its buyer; loyalty cannot be bought, transferred or cashed out.
- Creator payables reconcile exactly to the ledger; imported opening balances equal the Live columns at cutover, with every adjustment listed.
- The freeze switch refuses writes and serves reads.
- Entitlements are queryable with Live offline.