openvibers/openvibe.contracts Commits
ADR-012: economic classification (MONEY, CREDIT, ENTITLEMENT, LOYALTY, COSMETIC, GAME-STATE, LEGACY, EXTERNAL) and the rules Billing, Tips and VIP follow
Changes
nnew file mode 100644@@@ -0,0 +1,72 @@++# 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_orders` has 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 allow `recycle`, 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++++1. **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.++2. **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.++3. **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.++4. **MONEY → CREDIT (recycle) stays allowed** as an explicit journal entry, or is retired by product decision; it is never an unrecorded balance swap.++5. **LOYALTY never converts to MONEY or CREDIT, is never bought, and is not transferable between people.** Network's `network.coins.transfer` grant is revoked; the endpoint stays only for staff tooling. Loyalty ledgers keep idempotency keys that are deterministic per event (no `Date.now()`/random keys).++6. **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.++7. **Game state never bridges to MONEY, CREDIT or LOYALTY** without a new ADR.++8. **Legacy balances are never shown as live.** Frozen "gold" stays for a future claim flow and is removed from live displays.++9. **Test money is marked, not mixed.** Test transactions carry a test flag in Billing and never count toward creator or platform totals.++10. **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.++11. **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_orders` row as a provider receipt (status preserved; duplicates by provider reference reconciled and reported), every `transactions` row as historical journal entries marked `imported`, and **opening balances** per account equal to the current Live columns. Any difference between the replayed history and the columns is booked to an explicit `import_adjustment` entry and listed for review; nothing is silently absorbed.++- Test-era rows before `stats_vibes_reset_at` are 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.@@@ -13,3 +13,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22 || [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) || [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |++| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10 |
new file mode 100644
new file mode 100644
@@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
+# 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_orders` has 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 allow `recycle`, 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
+
+1. **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.
+2. **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.
+3. **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.
+4. **MONEY → CREDIT (recycle) stays allowed** as an explicit journal entry, or is retired by product decision; it is never an unrecorded balance swap.
+5. **LOYALTY never converts to MONEY or CREDIT, is never bought, and is not transferable between people.** Network's `network.coins.transfer` grant is revoked; the endpoint stays only for staff tooling. Loyalty ledgers keep idempotency keys that are deterministic per event (no `Date.now()`/random keys).
+6. **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.
+7. **Game state never bridges to MONEY, CREDIT or LOYALTY** without a new ADR.
+8. **Legacy balances are never shown as live.** Frozen "gold" stays for a future claim flow and is removed from live displays.
+9. **Test money is marked, not mixed.** Test transactions carry a test flag in Billing and never count toward creator or platform totals.
+10. **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.
+11. **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_orders` row as a provider receipt (status preserved; duplicates by provider reference reconciled and reported), every `transactions` row as historical journal entries marked `imported`, and **opening balances** per account equal to the current Live columns. Any difference between the replayed history and the columns is booked to an explicit `import_adjustment` entry and listed for review; nothing is silently absorbed.
+- Test-era rows before `stats_vibes_reset_at` are 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.
@@ -13,3 +13,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
@@ -13,3 +13,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22 |
| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22 |
| [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
| [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |
| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |
+| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10 |