Skip to content

Real vs Play Money Split — two coexisting economies, per-table

Accepted + shipped 2026-07-19 · plan real-play-money-split (amends agent-pmf-beta D4)

Goal

One deploy hosts both economies: every table is play (playSKPK) or real (SKPK/tSKPK), users always see both balances, and real money is a switch-flip away — not a redeploy.

The decision

In the context of the beta running playSKPK-only with one chip per deploy (CHIP_TOKEN), facing a launch that needs the real economy ready-to-flip (a backing mainnet can actually ship, and real tables with the same fund-safety liveness paths as play tables), we decided for:

  1. tables.money_kind (play | real, typed column, default play) as the fund-path discriminator — the cage's single chipMint(kind) seam resolves the mint FROM THE ROW; the deploy-wide CHIP_TOKEN survives only as the kind-less default, folded by effectiveMoneyKind (a legacy single-economy tskpk deploy transacts tSKPK regardless of its rows).
  2. skpk_enabled — a game_settings global (fail-closed OFF, per-environment) gating real-table create + fund. Withdraw/settle/escape are never gated. Staging ON; prod OFF until launch.
  3. Canonical-USDC backing — a numero-token upgrade: init_real_backing(usdc_mint) binds an EXTERNAL canonical USDC mint (Circle devnet USDC on staging; mainnet USDC later) + a real_reserve of that mint; wrap/unwrap re-bound to it (1:1, substitution + foreign destination revert). The self-minted SKUSDC is retired from the chip path.
  4. One custody deployment per economy — a byte-identical numero-custody under a fresh program id (9azX3QdAmwfqP4JEkvGVs5e4pnYxaGusyyg4XC8jkE93; cargo feature real-economy swaps only declare_id; built by onchain/scripts/build-real-custody.sh). The cage routes custody by the entry's stamped kind, resolved from the OFF-CHAIN row BEFORE any on-chain read (the entry PDA derives from the program id).
  5. Two-balance UI — header shows playSKPK + the real chip (money green, --money); lobby play/real filter + real-row styling; the cage teller is the USDC⇄tSKPK window when the switch is on (flag read via an UNGATED /api/economy/flags — production must be able to read it at launch).

And against: a second deploy per economy (splits the lobby/userbase), keeping SKUSDC backing (mainnet structurally cannot ship it), per-mint custody vault PDAs (a riskier change to the fund-safety-hardest program than the established fresh-ID pattern), money-kind in settings JSONB (fund discriminators get typed columns), and deferring real-table custody (escape is a fund-safety liveness path).

Load-bearing invariants

  • The mint follows the TABLE, never the caller — no request field can redirect a deposit's economy; a mismatched funding attempt reverts whole (the table PDA never even inits).
  • The switch gates money IN, never money OUT.
  • A kind is validated, never defaulted — a typo'd moneyKind refuses (invalid_money_kind).
  • Custody kind resolves off-chain-firstfindCustodyEntryByOnchainId (newest solana row; entry ids reset per cage boot until the durable sequence) picks the deployment before the PDA read.

Verification

Onchain 34/34 (incl. adversarial backing conservation); dual-economy cage e2e 6/6 (both economies in one deploy with provably different vault mints; settle + withdraw exits in tSKPK; the fund gate; real escape → real custody → tSKPK claim); integration 5/5 (create gates); RTL (header/lobby/teller); poker 732 passing; typecheck 36/36. Devnet brought up + verified on-chain the same day.