veratown-prototype

Shared Economy

The economy is a shared application capability for casino and approved party games. Its canonical indivisible unit is credits. Casino commands may temporarily display the compatibility label chips; both labels refer to the same balance. Credits have no real-money value.

Ownership and scope

src/core/economy/economy-port.ts defines the transport- and database-neutral EconomyPort. EconomyService enforces approved source/reason combinations; MongoEconomyRepository owns the shared account and ledger collections. The composition root constructs one service and injects it into casino and independently gated party-game consumers. Features do not import one another or write account records.

An account is keyed by BC memberNumber and is global across rooms using the same selected runtime database. Production and test remain separate, and the source database is never an economy target.

Operations and invariants

Operation Behavior
Balance Returns available and reserved credits; creates only a zero account when absent.
Grant once Applies an approved bounded positive award once per stable operation ID. Casino entry grants also enforce an atomic rolling cooldown.
Reserve Atomically moves available credits to reserved credits; insufficient balances do not write a reservation.
Adjust reservation Truth/Dare-only atomic bid adjustment reserves or releases only the difference; lowering to zero refunds and closes the reservation.
Settle Consumes the reservation and applies the specified total return exactly once.
Refund Returns the full reservation exactly once.
Forfeit available Hot Seat-only operation that clears available credits without changing any reservation.

The shop uses its own shop source: purchases reserve and settle/refund credits with stable order IDs, while completed favors receive a stable favor_payout. Stopping or voiding a favor forfeits only its unpaid reward; it never invokes the Hot Seat-only available-balance forfeiture.

Amounts and balances are safe integers. Source games and reason codes are allowlisted in EconomyService; there are no player-to-player transfers, operator grants, or real-money operations. The shop is the only purchase consumer and uses recipient consent plus reserved-credit settlement; see the shop guide. Operation IDs are bounded stable identifiers derived from persisted session/event IDs, never player text. Account and ledger mutations use Mongo transactions. A reservation and its adjustment history have no expiry until they settle or refund; finalized records expire after 60 days. MongoDB TTL cleanup is asynchronous.

The economy provides approved operations to consumers; it does not by itself enable gameplay. Casino slots and roulette were separately operator-qualified and approved for activation on 2026-10-15. The operator also reported Truth or Dare live qualification and activation approval on that date; its source gate is true, while each room configuration remains disabled by default. Anonymous Whisperer remains disabled until its adults-only room configuration and separate qualification are approved. Dilemma requires enabled room configuration, adults- only room policy, reviewed prompts, and per-round consent; Hot Seat requires its room-scoped configuration to be enabled. Hot Seat bluff rewards may include a code-validated available-balance ceiling, applied atomically with the idempotent grant. Each feature’s consent and release gates remain independent. See the Anonymous Whisperer guide, Truth or Dare guide, Dilemma guide, and Hot Seat guide.

Collections

Collection Owner Retention
prototypeEconomyAccounts Economy repository No TTL; one account per member in the selected runtime database.
prototypeEconomyLedger Economy repository 60 days after finalization; unsettled reservations, adjustment history, and the cutover marker remain durable.
prototypeCasinoWallets Legacy casino compatibility / rollback No TTL; retained through the rollback window.
prototypeCasinoLedger Legacy casino compatibility / rollback 60 days after grant or settlement; unsettled reservations remain durable.

Technical flow

flowchart LR
    Root["start.ts composition root"] --> Service["EconomyService"]
    Service -. "implements" .-> Port["EconomyPort"]
    Casino["Casino"] --> Port
    PartyGames["Party-game services"] --> Port
    Service --> Repository["MongoEconomyRepository"]
    Repository --> Accounts[("prototypeEconomyAccounts")]
    Repository --> Ledger[("prototypeEconomyLedger")]
sequenceDiagram
    participant Game as Game service
    participant Economy as EconomyService
    participant Repo as MongoEconomyRepository
    participant Mongo as MongoDB transaction
    Game->>Economy: reserve / settle (stable operation ID)
    Economy->>Repo: validate and apply operation
    Repo->>Mongo: check idempotency key and update account + ledger
    alt New valid operation
        Mongo-->>Repo: commit
        Repo-->>Economy: durable result
    else Same operation already recorded
        Mongo-->>Repo: prior result
        Repo-->>Economy: replay without a second mutation
    else Conflict or insufficient balance
        Mongo-->>Repo: no mutation
        Repo-->>Economy: typed rejection
    end
    Economy-->>Game: result

Casino migration and rollback

The economy repository refuses startup until the migration marker exists. This prevents a zero-balance economy account or an entry grant from being created before legacy balances are copied.

Run the migration against the intended RUNTIME_TARGET; it reads and writes only that target’s fixed database. It never opens the legacy source database or changes the configuration-copy allowlist. Stop the runtime during cutover.

npm run migrate:casino-economy
npm run migrate:casino-economy -- --apply

The default is a dry run. It validates the complete legacy wallet and ledger shapes, checks destination conflicts, and prints only counts and aggregate available/reserved credit totals. Apply uses stable per-wallet and ledger IDs, does not delete the legacy collections, and is safe to resume after an interrupted run. Legacy ledger expiry dates are extended to preserve the approved 60-day retention. The final durable cutover marker is written only after wallet, ledger, and aggregate economic source data remain unchanged and all imports verify. The script changes only legacy ledger expiresAt values to preserve rollback retention.

For an approved rollback, stop the runtime and inspect the inverse dry run:

npm run migrate:casino-economy -- --rollback
npm run migrate:casino-economy -- --rollback --apply

Rollback restores shared account balances and entry-grant timestamps to the legacy casino wallet collection. It is limited to 60 days after cutover and fails closed if any post-cutover economy activity occurred outside casino entry grants; those operations cannot be safely represented by the legacy casino wallet. A rollback-compatible deployment must keep wagering and new games disabled. Since slots and roulette are now activated, stop gameplay before considering rollback; any activity beyond entry grants makes this legacy rollback unavailable. Resume the prior runtime only after verifying the aggregate totals.

References