veratown-prototype

Shared Economy And Wallet Proposal

Status: The shared credits contract, Mongo implementation, casino wallet cutover path, and migration tooling are implemented. Casino, Anonymous Whisperer, Truth or Dare, Hot Seat, and Dilemma are statically wired consumers. Truth or Dare’s source gameplay gate is enabled after operator-reported qualification; room-scoped party games still require their own enabled configuration and policy checks.

Archived: 2026-10-16 (UTC). See the live economy guide for current operations, retention, and migration guidance.

Purpose

Promotes the casino’s persistent chip wallet into one audited, feature-neutral economy that the casino and independently implemented party-game plug-ins can use without importing one another. The shared economy is a prerequisite for games that award, reserve, spend, or forfeit points.

Recommendation

The implementation has one shared balance per Bondage Club member in the already selected runtime database. Its indivisible unit is credits in the API and storage contract. During a compatibility period the casino may display “chips,” but chips and credits must resolve to the same account and balance; there must not be one casino wallet plus a second party-game points wallet. Credits have no real-money value.

Game sessions and rules remain owned by each game. Account balances, economic reservations, grants, and the economic ledger are owned by the shared economy. Each consumer receives a narrow economy port from the composition root; no feature imports another feature’s service or repository.

Original foundation and gaps addressed

Before this cutover, the casino had CasinoWalletService, a typed/Zod-validated MongoCasinoRepository, a transactional ledger, idempotent reservations and settlements, an entry grant, and per-game session recovery. Casino gameplay is independently controlled by CASINO_GAMEPLAY_ENABLED; the current gate value and operator-reported qualification are recorded in the casino guide and runtime architecture.

The cutover moves the wallet and economic ledger to prototypeEconomyAccounts and prototypeEconomyLedger; casino session records remain casino-owned. The legacy prototypeCasinoWallets and prototypeCasinoLedger remain only for the approved rollback window. Consumers do not depend on casino services or repositories. The single-document prototypeGameConfig is global, scalar-valued, and not a typed per-room game configuration system; it is not a substitute for the room-scoped settings proposed by the games.

Implemented boundaries

The economy service is a shared application capability, not a dependency between plug-ins. If it has no user-facing commands or subscriptions, it does not need a separate BotPlugin; the composition root initializes its repository before plug-in startup and closes no resource other than the shared Mongo client.

Shared economy records

Collection Proposed contents Ownership and lifecycle
prototypeEconomyAccounts One account per BC memberNumber; schemaVersion, availableCredits, reservedCredits, optional lastEntryGrantAt, and timestamps. Economy repository; no TTL. Unique _id; non-negative safe integers.
prototypeEconomyLedger Immutable entries with stable _id idempotency key, member number, operation/session IDs, source game, typed kind/reason code, available/reserved deltas, and dates. No chat, answer, secret, or prompt text. Economy repository; finalized entries and idempotency records have 60-day TTL retention. Unsettled reservations remain recoverable and are not TTL-deleted. The cutover marker is durable.
Feature session collections Game-specific participants, phase, bounded outcomes, prompt IDs/revisions, config revision, deadlines, and settlement state. Each game repository; no account balances or unbounded economic history.

Account identity is global to the configured BC runtime database, matching the current member-number wallet model. It is shared across rooms in that one environment, but never across production and test, and never across the legacy source database. A future non-BC channel would require a separately approved identity mapping; it must not reuse a numeric member number as if it were globally unique across services.

Economy operations and invariants

The proposed port should expose use cases rather than arbitrary balance mutation:

Operation Required behavior
Read balance Return available and reserved credits; do not create a non-zero balance.
Create account Initialize a missing account to zero.
Grant once Require a bounded positive amount, approved source/reason, and stable operation ID. Duplicate IDs return the original result without crediting again.
Reserve Atomically move available credits to reserved credits for a game/session. Reject invalid amounts, duplicate conflicting IDs, and insufficient funds.
Adjust reservation Truth/Dare-only bid adjustment changes one active reservation atomically by the difference; target zero refunds and finalizes it.
Settle Atomically consume or return a reservation and apply an approved reward; stable settlement ID makes retry safe.
Forfeit available Set available credits to zero for the Hot Seat rule without touching other sessions’ reserved credits; emit one ledger entry.
Refund Release a reservation exactly once after a void/cancel result.

Every amount is a safe integer within a hard code-level ceiling. Per-room configuration may lower limits and tune approved rewards, but may not disable validation, select arbitrary ledger reasons, set balances directly, or create unbounded liabilities. The economy receives immutable operation IDs derived from persisted session IDs and event types. It never accepts player chat text as an idempotency key or ledger metadata.

Balance and ledger writes stay transactional. Game outcome/session state and economy settlement may be separate durable steps: first persist the game outcome and a settlement_pending marker, then call the idempotent economy operation, then mark the session settled. A restart repeats only the same operation ID; it cannot credit twice. If the economy outcome is ambiguous, the session remains pending and reports an explicit failure for recovery rather than claiming success.

No player-to-player transfers, real-money purchases, arbitrary operator grants, or external treasury are included. Any change to those capabilities requires its own authorization, abuse, audit, and release review.

Casino compatibility and migration

  1. Keep casino game commands gated. Do not change the independent release gate as part of the refactor.
  2. Inventory both casino wallet documents and ledger records in each approved runtime environment. Do not assume test or production is empty.
  3. Add typed readers that can validate the current casino wallet shape while the new economy API is introduced. Keep casino round/session documents in the casino repository.
  4. Provide a dry-run-first, explicit---apply migration for the exact allowlisted wallet and ledger collections. It must print counts and aggregate credit totals, not account documents or player content; use stable migration IDs; reject a target outside the fixed runtime database mapping; and never read or write the legacy source database.
  5. Pause entry grants during cutover or prove the grant’s rolling timestamp remains atomic with the migrated balance. Resume only after source and destination counts/totals reconcile. Make a second apply a no-op or an explicit conflict, never a second credit.
  6. Route the casino balance/grant/reserve/settle use cases through the shared port. Retain a short-lived thin casino facade only if it maps to that same account and does not duplicate state.
  7. Remove compatibility readers only after test and production migration verification and an explicitly approved rollback window.

The implemented migration command is npm run migrate:casino-economy. It is dry-run by default, validates both source and destination before applying, prints aggregate balances and counts only, and uses stable migration IDs. Startup fails closed until its durable cutover marker exists. Apply retains the legacy wallet and ledger data and extends expiring legacy ledger entries to preserve the approved rollback period. During the 60-day rollback window, npm run migrate:casino-economy -- --rollback previews restoring shared balances to the legacy casino wallet; --rollback --apply performs that restore. Rollback is rejected after post-cutover economic activity that cannot be represented by the legacy casino wallet.

The migration must preserve the existing target/database isolation guard. It must not widen the Veratown config-copy script’s allowlist.

UML

Component diagram

flowchart LR
    Root["start.ts<br/>composition root"] --> Economy["EconomyService"]
    Root --> Casino["CasinoFeaturePlugin"]
    Root --> HotSeat["HotSeatPlugin"]
    Root --> Dilemma["DilemmaPlugin"]
    Root --> Dare["TruthDarePlugin"]
    Root --> Whisperer["AnonymousWhispererPlugin"]
    Casino --> Port["EconomyPort"]
    HotSeat --> Port
    Dilemma --> Port
    Dare --> Port
    Whisperer --> Port
    Economy --> Port
    Economy --> Repo["MongoEconomyRepository"]
    Repo --> Accounts[("prototypeEconomyAccounts")]
    Repo --> Ledger[("prototypeEconomyLedger")]
    Casino --> CasinoSessions[("casino sessions")]
    HotSeat --> HotSeatSessions[("Hot Seat sessions")]
    Dilemma --> DilemmaSessions[("Dilemma sessions")]
    Dare --> DareSessions[("Truth/Dare sessions")]
    Whisperer --> WhispererSessions[("Whisperer metadata")]

Class diagram

classDiagram
    class EconomyPort {
        <<core contract>>
        +balance(memberNumber)
        +grantOnce(operationId, memberNumber, amount, reason)
        +reserve(operationId, memberNumber, amount, sessionId)
        +adjustReservation(operationId, reservationId, targetCredits)
        +settle(operationId, reservationId, result)
        +refund(operationId, reservationId)
        +forfeitAvailable(operationId, memberNumber)
    }
    class EconomyService {
        +balance()
        +grantOnce()
        +reserve()
        +adjustReservation()
        +settle()
        +refund()
        +forfeitAvailable()
    }
    class EconomyRepository {
        <<persistence port>>
        +initialize()
        +applyTransaction()
    }
    class MongoEconomyRepository
    class EconomyAccount {
        +memberNumber
        +availableCredits
        +reservedCredits
        +lastEntryGrantAt
        +schemaVersion
    }
    class EconomyLedgerEntry {
        +operationId
        +memberNumber
        +sourceGame
        +kind
        +availableDelta
        +reservedDelta
        +reasonCode
    }
    EconomyService ..|> EconomyPort
    EconomyService --> EconomyRepository
    MongoEconomyRepository ..|> EconomyRepository
    MongoEconomyRepository --> EconomyAccount
    MongoEconomyRepository --> EconomyLedgerEntry

Use-case diagram

flowchart LR
    Player((Player)) --> Balance([Read shared balance])
    Casino([Casino game]) --> Reserve([Reserve credits])
    Casino --> Settle([Settle or refund reservation])
    PartyGame([Party game]) --> Grant([Award approved game credits])
    PartyGame --> Forfeit([Forfeit available credits])
    Operator((Authorized operator)) --> Migrate([Run dry-run / approved migration])
    Balance --> Account[(Shared account)]
    Reserve --> Ledger[(Immutable ledger)]
    Settle --> Ledger
    Grant --> Ledger
    Forfeit --> Ledger
    Migrate --> Account

Activity diagram: reserve and settle

flowchart TD
    Start([Game outcome or bid]) --> Validate{Valid bounded operation?}
    Validate -->|No| Reject[Return typed rejection]
    Validate -->|Yes| Tx[Start Mongo transaction]
    Tx --> Existing{Operation ID already recorded?}
    Existing -->|Yes, same operation| Replay[Return recorded result]
    Existing -->|Yes, conflicting| Conflict[Return conflict; do not mutate]
    Existing -->|No| Funds{Sufficient available credits?}
    Funds -->|No| Insufficient[Return insufficient funds]
    Funds -->|Yes| Update[Update account and insert ledger entry]
    Update --> Commit[Commit transaction]
    Commit --> Result([Return durable result])

State machine: reservation

stateDiagram-v2
    [*] --> Available
    Available --> Reserved: atomic reserve
    Available --> Available: insufficient funds / no write
    Reserved --> Settled: durable outcome + idempotent settlement
    Reserved --> Refunded: void / cancel / recovery policy
    Settled --> Settled: duplicate operation ID returns prior result
    Refunded --> Refunded: duplicate refund returns prior result
    Settled --> [*]
    Refunded --> [*]

Sequence / communication diagram

sequenceDiagram
    participant Game as GameService
    participant Economy as EconomyService
    participant Repo as MongoEconomyRepository
    participant Mongo as MongoDB transaction
    Game->>Economy: reserve(stableOperationId, member, amount, session)
    Economy->>Repo: validate and reserve
    Repo->>Mongo: read account + idempotency key
    Mongo-->>Repo: account and prior-operation result
    alt new valid reservation
        Repo->>Mongo: update account + insert ledger atomically
        Mongo-->>Repo: committed
        Repo-->>Economy: reserved
    else retry
        Repo-->>Economy: prior result (no second mutation)
    else insufficient or conflict
        Repo-->>Economy: typed rejection
    end
    Economy-->>Game: durable result
    Game->>Game: persist session transition / settlement marker

Approved decisions

  1. Unit and compatibility label: credits is canonical; casino may use “chips” as a temporary display alias for the same account.
  2. Account scope: one global-per-member account in each selected runtime database, shared across rooms. Test, production, and the source database remain isolated.
  3. Retention and idempotency: finalized economy ledger entries and their operation IDs are retained for 60 days. Unsettled reservations do not expire.
  4. Operations: all proposed economy operation categories are approved: awards, reservations, settlements, refunds, forfeitures, and entry grants. This approves economy operations, not release activation; each game still requires its own review and qualification.
  5. Migration and compatibility: the migration and 60-day rollback window are approved. /bot casino balance remains available and may display “chips” while using the shared credits account.

Release gates

References