The platform has first-class Gold and Diamond wallets alongside the virtual-chip balance and tournament tickets.
Persistence
CurrencyCodeis the wallet currency enum:GOLDandDIAMOND.Walletstores spendablebalance, reservedlocked, and a per-rowversionfor each(userId, currency).Transaction.currencyandTransactionArchive.currencyidentify the wallet touched by each ledger row.CurrencyRatestores effective Diamond to Gold rate history.CurrencyConversionstores one-way conversion attempts and links its two ledger rows byconversionId.CurrencyPackagestores cashier package definitions for Gold and Diamond purchases.PokerTable.currencydeclares the wallet used by real-money table buy-ins and cash-outs.
User.balance is still dual-written for Gold. Treat Wallet(GOLD) as the source of truth for new code, but keep reads of the legacy column compatible.
Service Ownership
src/services/wallet.service.tsis the only service that should mutate Gold or Diamond wallet balances directly.src/services/currency-conversion.service.tsowns Diamond to Gold conversion, daily caps, idempotency, and rate snapshotting.src/services/economy-settings.service.tsowns operator controls:dualCurrencyEnabled,walletCutover,conversionEnabled,conversionMin,conversionMax,conversionDailyCapPerUser,diamondToGoldRate,goldWithdrawable,goldPurchasesEnabledanddiamondPurchasesEnabled.src/services/economy-reconciliation.service.tsverifies per-currency wallet totals against signed ledger deltas and continuity.scripts/migrate-dual-currency.tsbackfills Gold wallets fromUser.balanceand writesMIGRATION_GRANTledger rows.
Financial mutations must run in Serializable transactions. Debits use conditional wallet updates so concurrent spend attempts cannot both succeed.
Default conversion settings are a rate of 100 Gold per Diamond, a minimum of 1 and a maximum of 10000 Diamond per conversion, and a cap of 100000 Diamond per user per UTC day.
API Surface
Player cashier:
GET /api/v1/cashier/balancereturns legacy balances pluswallets.GET /api/v1/cashier/walletsreturns wallet totals, locked amounts, available spend, and conversion limits.GET /api/v1/cashier/currency-packageslists active Gold and Diamond packages.GET /api/v1/cashier/convert/previewpreviews Diamond to Gold conversion using the active rate.POST /api/v1/cashier/convertconverts Diamond to Gold with an idempotency key.
Admin economy:
GET/PUT /api/v1/admin/economy/settingsGET/POST /api/v1/admin/economy/ratesGET/POST /api/v1/admin/economy/packagesPATCH/DELETE /api/v1/admin/economy/packages/:packageIdGET /api/v1/admin/economy/summaryGET /api/v1/admin/economy/reconciliation
Settlement Rules
- Real-money tables debit and credit the table currency.
- Virtual-chip tables still use
User.virtualBalanceand virtual transaction types. - Tournament
USDis treated as Gold for wallet settlement.TICKETSremains a ticket entitlement.GOLDandDIAMONDsettle against the matching wallet. - Coupons and bonuses with
GOLDorDIAMONDcredit the matching wallet. LegacyUSDcontinues to map to Gold. - Withdrawal balance checks run against the Gold balance.
goldWithdrawableis an operator setting returned to clients byGET /api/v1/cashier/wallets; the withdrawal service does not read it. - Diamond to Gold conversion is one-way. Gold to Diamond is intentionally unsupported.
Reconciliation Invariants
- Every completed Gold or Diamond wallet mutation must have a same-transaction ledger row.
- Conversion writes exactly two completed ledger rows, one
CURRENCY_CONVERSION_OUTin Diamond and oneCURRENCY_CONVERSION_INin Gold. balanceBeforeandbalanceAfterrefer to the wallet currency on that transaction row.- Reconciliation compares wallet supply by currency against signed completed ledger deltas, with a tolerance of 0.01, and checks per-user continuity.
- Decimal amounts remain
Decimal(18,2). Conversion rates remainDecimal(18,8), and conversion payouts truncate toward zero at two decimal places.
Migration Verification
Use the migration script before cutover:
npm run db:generate
npx prisma migrate status
npx tsx scripts/migrate-dual-currency.ts --dry-run
npx tsx scripts/migrate-dual-currency.ts
After backfill, run economy reconciliation from the admin endpoint or service and verify Gold wallet totals match existing User.balance totals.