# RAIN Risk Markets — Arbitrum One Mainnet Deployment

**Date:** 2026-07-27 · **Chain:** Arbitrum One (42161) · **Deployer/Governance:** `0x74a435596b7C76d99320eF960FbF6e4750257aC2`

## Contract Addresses

| Contract | Address | Deploy Tx |
|---|---|---|
| MarketRegistry | `0x076149f4293549b78C909315B3465805922fDf70` | `0x230e35364f16b6c36a4f3fc20d20eeb6eb1bdfb175422610e6e2d701662118b9` |
| RiskVault | `0x9eDCB60620Ad062C4EbA71d5B457eA08fc1EadE8` | `0x2b7c852008cce967db1cf19b5bc730ba30ec180b4ba8bb212681959448eb4945` |
| FeeSplitter | `0x0dd5cE18cFdc7345f0caA212B94747c052EfA947` | `0x0cc14e085989c7c95ecf1add6f8604e42fb35cf2e5bf92cff99de6b01517fd38` |
| VRFAdapter | `0x2afFca69f8aa481afDCE3c15669225246dC2008B` | `0xcd846c122f822d0548a33ec943ea0104185c915a16861b7cfba79f90a01c9676` |
| RiskEngine | `0x16700BA3141Dd73ea0139B1e6663B6b40268d99d` | `0x752f9c20610479d2a73ef71fa886dabbe8fbab43c28fa66331b688a047b89758` |

## External Addresses (verified)

- USDC (native Arbitrum): `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` — decimals()==6, symbol "USDC" verified on-chain
- LINK: `0xf97f4df75117a78c1A5a0DBb814Af92458539FB4` — **NOTE:** the address given in the task brief (`0xf97f...45d02c95`) has no code on Arbitrum; the correct address above was taken from Chainlink's official VRF v2.5 supported-networks docs and verified (symbol "LINK").
- VRF Coordinator v2.5: `0x3C0Ca683b403E37668AE3DC4FB62F4B29B6f7a3e` (Chainlink docs)
- Key hash: `0x8472ba59cf7134dfe321f4d61a430c4857e8b19cdd5230b09952a92671c24409` (30 gwei lane)

## Wiring Txs

- `vault.setEngine`: `0x24b96bee53fca0a7fa6d31dc9bc693b81026506676ab65591d38068866e531a7`
- `splitter.setEngine`: `0x5eee2df8dec4bbf6b3c5303875590717b6f87df42a178182630e2111e57e3e3f`
- `vrf.setEngine`: `0x1c409e114a8fd3fcb4c6945c26f1ccb906b623c2e144bef31927cc7ee75009aa`
- 5× `listMarket` (even-money 1.975x, 4x, 10x, 36.5375x, ~100x @ EV 0.9875):
  `0xb0a88a4d…eebdab`, `0x3b41ae78…840120a`, `0x06f12eaf…7ec48d`, `0x9212ca49…dca230`, `0x61ee9913…44b6b7`

## Chainlink VRF v2.5 Subscription

- **Sub ID:** `89227813476920981096392676009477736066099074454582175463472230943458630375262`
- createSubscription tx: `0x07692ab24e16404fb31e5ded64ba63405e1faaeb09994daff25f948f98b44a56`
- Funded with **3.0 LINK** via `LINK.transferAndCall(coordinator, 3e18, abi.encode(subId))`: `0xa4844276a326a46828cc9333fb42a637f8563f66d803533ef2acf848f2db9f39` (chose LINK funding — VRFAdapter requests with `nativePayment:false`, so LINK is what actually gets billed)
- addConsumer(VRFAdapter): `0xfe30f10106c8b14ccb60d1b54a5c5a96b84f23b32e7e0431fd5b95f23c4ea733`

## ETH→LINK Swap

- LiFi (Kyberswap route): 0.014 ETH → 3.1257 LINK, tx `0x0d816df5c47e78b787989b9127f1b44498fd16db202d357d0bf79039db77aa86`
- (First send attempt failed on a transient "max fee < base fee" race; re-quoted and retried successfully.)

## Safety Config

- RiskVault TVL cap: **2,000 USDC** (`tvlCap = 2_000_000_000`, verified on-chain)
- All governance roles: deployer wallet

## Smoke Test (read-only, all ✅)

- `vault.totalAssets()` = 0; `vault.tvlCap()` = 2,000e6
- `registry.marketCount()` = 5
- `vault.engine()` = RiskEngine; `vrfAdapter.engine()` = RiskEngine; `vrfAdapter.subId()` = sub ID
- `coordinator.getSubscription(subId)` → balance 3.0 LINK, owner = deployer, consumers = [VRFAdapter]
- No USDC deposit test performed (wallet holds no USDC, per instructions)

## Frontend

- CONFIG updated in `rain-risk-markets/app/app.js` (chainId 42161 already set; 4 protocol addresses filled in)
- Pushed to rainsandbox-xyz repo (agentbase workflow `b086e87d-01e0-4c20-9b0b-3e73df8a0cd1`), commit `cce81e24`, redeployed → **live**
- Verified https://rainsandbox.xyz/app/app.js serves the real addresses; `/app/` already existed on the site (LAUNCH APP links unchanged)

## Balances After Deployment

- ETH: **0.02007** (well above the 0.012 floor)
- LINK in wallet: **0.1257** · LINK in VRF sub: **3.0**

## Deviations & Notes

1. **LINK address correction** (see above) — task brief's LINK address was invalid on Arbitrum; used official Chainlink-docs address.
2. **Arbiscan verification skipped** — no `ARBISCAN_API_KEY` (or ETHERSCAN key) in the environment. Contracts are unverified; can be done later with `forge verify-contract` once a key is available.
3. Used the existing `Deploy.s.sol` unmodified (it is fully env-driven): `USDC`, `VRF_COORDINATOR`, `VRF_KEY_HASH`, `VRF_SUB_ID`, `TVL_CAP=2000000000` passed via env. `--skip-simulation` used to save time; all txs confirmed on-chain.
4. Agentbase repo update done via `update-code.cjs`/`redeploy.cjs` (skill's supported path) rather than a manual git clone/push — same result, verified live.

---

# DEMO MODE (Play-Money) Deployment — v1.3.0

**Date:** 2026-07-27 · **Chain:** Arbitrum One (42161) · Deployer: `0x74a435596b7C76d99320eF960FbF6e4750257aC2`

## RUSD + Demo Protocol Stack

| Contract | Address |
|---|---|
| RUSD (RISK Dollar, 6 dec, 100M supply) | `0x260c7019E760763988843Bc5b873d749b5937469` |
| MarketRegistry (demo) | `0xADf3B1270961231ff925C8885843c597502A3d44` |
| RiskVault (demo, asset=RUSD, cap 100M) | `0xEcF918c851d9259DDaEc48d51D8363CBaDe3A18F` |
| FeeSplitter (demo) | `0x46F8478aeDbE189ffa85CE06BFF767c57b7d403c` |
| VRFAdapter (demo) | `0x17fb4Fc8778aF721D28846F64A391F7D7374d47f` |
| RiskEngine (demo) | `0x4fd901d0f18d738142F86A82AC1895D4B0C6EBa5` |

- Deployed via `script/DeployDemo.s.sol` (single broadcast: token + stack + wiring + 5 canonical markets + 10M RUSD house-bankroll deposit).
- Same 5 markets as mainnet stack (1.975×, 4×, 10×, 36.5375×, ~100×).
- VRF: reuses the existing v2.5 subscription (3 LINK). addConsumer(demo VRFAdapter): `0x05460cc52296d0c3133f7613f0743bede9e8b2209dcb654326c85fd185b6fa5a`. Sub now has 2 consumers (live + demo adapters).
- Sanity checks ✅: totalAssets = 10,000,000 RUSD; nextMarketId = 5; vault.engine & vrf.engine = demo RiskEngine; consumer registered.

## Faucet (REDEEM CODE)

- Faucet wallet: `0x488F62E6e11fA7DC70bea1A4D4D642aB2ba81D5B` (key in `faucet-wallet.json`, chmod 600). Funded: 0.004 ETH (`0x63aeaf18…23a1f6`) + 5,000,000 RUSD (`0xaf16aec6…0703bc`).
- Backend: `POST https://api.rainsandbox.xyz/faucet` `{address, code}` → sends 0.0003 ETH + 1,000 RUSD. Code `RAIN2026`, 24h per-address limit (in-memory), kill switch `FAUCET_ENABLED`.
- Backend env (via agentbase redeploy): FAUCET_PRIVATE_KEY, FAUCET_CODE=RAIN2026, FAUCET_ENABLED=1, ARB_RPC, RUSD_ADDRESS. CORS: `cors()` open (allows rainsandbox.xyz).
- Test claim ✅ to `0x7B61dc9eE9875Cde4d0D9b1deE3dDa6C92FAA79F`: ETH tx `0xa9293d20fefbe21346ce45d612d4d5dcb367cf7de1a16c6d6d8fc839169adf50`, RUSD tx `0x3260ec88d5e04ab8bc82e20b19cabefbadba7d92ae020ac63b028da5ab8de089`. On-chain confirmed (0.0003 ETH + 1000 RUSD). Repeat claim → 429; wrong code → 403. ✅

## Frontend v1.3.0

- DEMO/LIVE toggle (header, defaults to DEMO, persisted in localStorage `rainMode`).
- DEMO mode auto-creates an in-browser burner wallet (ethers.Wallet, obfuscated in localStorage `rainDemoBurnerV1`), signs via public RPC — no extension needed. Banner shows address + demo-wallet note.
- 🎁 REDEEM CODE button → POSTs to the faucet, refreshes balances.
- Market game names in list + position feed: Coin Flip / Even Money, 4× Multiplier, 10× Multiplier, Roulette — Single Number, Long Shot 100×.
- Deployed via agentbase (frontend commit `86a38dfc`, backend commit `6fe26323`), both `live`; verified `v1.3.0` served at rainsandbox.xyz/app/app.js.

## Balances After Demo Deployment

- Deployer ETH: **0.01590** · Deployer RUSD: 85,000,000 (100M − 10M vault − 5M faucet)
- Faucet: 0.0037 ETH + 4,999,000 RUSD (after test claim)

## User Flow

1. Open https://rainsandbox.xyz/app/ — DEMO mode is default; a demo wallet is created automatically in the browser.
2. Click **🎁 REDEEM CODE**, enter `RAIN2026` → receive 1,000 RUSD + 0.0003 ETH gas.
3. Trade any market or deposit into the vault — real on-chain txs, VRF settlement, play money.
4. Switch to **🔴 LIVE** for the real USDC stack (browser wallet / WalletConnect / Coinbase).

---

# ROULETTE — v1.7.0

**Date:** 2026-07-27 · New page https://rainsandbox.xyz/app/roulette.html (+ `roulette.js`), full European single-zero roulette on the DEMO stack (RUSD), shared burner wallet with the main app. Deployed commit `80aa0d8f`.

## New Markets on Demo MarketRegistry (`0xADf3B1270961231ff925C8885843c597502A3d44`)

All two-outcome (win/lose), EV = 987,499 ppm (≤ 98.75% spread floor):

| ID | Bet type | prob (ppm) | payoutX | tx |
|---|---|---|---|---|
| 5 | Even-money (red/black/even/odd/1-18/19-36), 18/37 | 486,486 | 2.029863× | `0xb2e11c97b0d16ee6853a631a9671cee98c5c30fe5d9d4a206f6dc261e4b47ed0` |
| 6 | Corner (4-number quad), 4/37 | 108,108 | 9.134384× | `0xd7c0b3c2caf8eb565797a24973f289515607577de998d0573ad5109ea74a8285` |
| 7 | Dozen / Column, 12/37 | 324,324 | 3.044794× | `0xc7654f5870694088e24eebde33e52d6b279c3364bae3586893dafb37192226e7` |

- Straight bets reuse existing market **#3** (prob 27,027 = 1/37, 36.5375×) — verified via `getMarket(3)`.
- All verified on-chain via `getMarket` (active, EV 987,499). `nextMarketId` = 8.
- Mapping recorded in `app/roulette.js` `MARKETS` config.
- Wheel presentation: win/loss decided on-chain by Chainlink VRF; displayed pocket derived client-side consistent with the verified outcome (covered number on WIN, non-covered on LOSS), seeded by settlement tx hash. Honest "ⓘ provably fair" note on page.
- Deployer ETH after registrations: **0.01589**.

## Roulette v1.9.0 (2026-07-27)

**Batch betting decision — NO periphery contract deployed.** RiskEngine.placePosition
attributes positions to `msg.sender` (`positions[id].trader = msg.sender`) and
settlement pays `p.trader` directly (also void refunds → `p.trader`). There is no
owner/onBehalf/recipient parameter. A `RouletteTable` periphery calling placePosition
would become the trader: all VRF payouts and timeout refunds would flow to the
periphery, requiring escrow bookkeeping + an extra claim tx per user (worse UX, new
custody risk) — clean attribution is impossible without modifying the core engine.
Instead the frontend now broadcasts all placePosition txs **in parallel** (Promise.all
fan-out with sequential explicit nonces, single approval, no inter-send waits).

Other v1.9.0 changes: RAIN logo drawn into wheel hub (circular clip, canvas);
wheel comes to a complete stop on the result (new "stopped" phase, zero residual
drift); persistent results sidebar (last 50 spins, localStorage, colored number
chips + MULTI chip, net win/loss per spin).

## Roulette v2.0.0 — Standalone RouletteTable (2026-07-27)

**One VRF draw per spin.** New self-contained contract `protocol/src/RouletteTable.sol`
(demo stack is single-slot onlyEngine, so the table has its own house bankroll and
replicates the fee waterfall by direct transfers).

| Item | Value |
|---|---|
| RouletteTable | `0x38a01415C7171608D96aD929cd67A63a593e1D01` |
| Deploy tx | `0x79936d58a42aa6840843d39964dee2b49b407002185f813170b354668e41894f` |
| addConsumer (VRF sub) | `0xff22e7fe4d1c6960e7e45088fa1a1473106746985d552e15f6cede9f1fb56835` |
| RUSD approve 5M | `0xafa9163281b50e4f3a1c5a04d683d8f34ca079ffd085192e4e401ba8db7f3b7f` |
| depositFloat 5,000,000 RUSD | `0x798c25ac09ca1f78dbaa73cd5c4a7e51ce12b07d2c9426abcbb8cd1eb3479728` |

- Mechanics: `placeBets(BetInput[])` (max 10, single transferFrom) → one Chainlink VRF
  v2.5 request → `number = word % 37` settles all bets atomically. Events:
  SpinRequested / SpinResult / BetSettled / SpinVoided.
- Economics mirror the engine: 1.25% spread (netStake = 98.75%), fee ppm split
  4_000 burn (1_500 protocol + 2_500 unattributed brand) / 1_700 rebate / 500 standby /
  6_300 residual → demo RiskVault (`0xEcF9…3A18F`). All fee sinks = deployer (as on the
  demo FeeSplitter). Multipliers identical to registry markets #3/#5/#6/#7 (verified
  via getMarket): straight 36.5375× · even-money 2.029863× · corner 9.134384× ·
  dozen/column 3.044794×.
- Fees held in escrow until fulfillment; timeout void (1h, mirrors engine default)
  refunds FULL gross stake, anyone can call `voidSpin`.
- Exposure rule: worst-case total payout over the 37 numbers ≤ 2% of free float
  (balance − lockedEscrow). Owner deposit/withdrawFloat can never touch escrow.
- **Corner mapping (canonical, mirrored in frontend):** param k∈0..21: c=k>>1,
  dr=k&1, anchor a=3c+dr+1, quad {a, a+1, a+3, a+4}. k=0→{1,2,4,5}, k=1→{2,3,5,6},
  …, k=21→{32,33,35,36}.
- Forge tests `test/RouletteTable.t.sol`: 12/12 green (classification of all
  types/params/numbers, corner mapping, payout math, worst-case exposure,
  multi-bet settlement + value conservation, zero-loses-outside, void refund,
  escrow-protected withdrawal, pause, bad-bet validation, conservation fuzz).
- Frontend v2.0.0 (commit `e36f4302`, agentbase redeploy → live): single placeBets tx,
  ball always lands on the real drawn number, per-bet breakdown from BetSettled
  events (JS covers() mirror as fallback), no MULTI ever; roulette-logic.js
  superseded (kept, not loaded). app/tests.js now verifies JS↔contract corner &
  covers parity (7/7 pass). Headless check: zero console errors, v2.0.0 served.
- Deployer ETH after all txs: **0.012840** (above 0.005 floor). Contract unverified
  on Arbiscan (no API key, same as prior deploys).

---

# BLACKJACK — v2.1.0

**Date:** 2026-07-27 · New page https://rainsandbox.xyz/app/blackjack.html (+ `blackjack.js`), single-player blackjack vs house on the DEMO stack (RUSD), standalone bankroll contract (RouletteTable pattern), shared burner wallet.

## Contract

| Contract | Address |
|---|---|
| Blackjack | `0x38579152b8c8CAA90719A4dA1732bC5644157Da5` |

- Deploy tx: see `protocol/broadcast/DeployBlackjack.s.sol/42161/run-latest.json`
- addConsumer to VRF sub: `0xdb5ae5dc047257ff0cf9519df22b232f7d6da6934c832c003695e78324807ee3` (sub now 4 consumers)
- Bankroll funded **5,000,000 RUSD**: approve `0xf0b7515504650e66b9bbbb61dd26e727454407f8655ec9df963feffc6eb44a4e`, depositFloat `0xa919b27b73151c7d203c5b92de3d5f0fad439751b91282d342a94144531808f5`

## Rules & Economics

- S17 (dealer stands on ALL 17s), BJ pays 3:2, double any two cards, split pairs ONCE (split aces: one card each, auto-stand), dealer peeks on A/10-value up-card, no insurance/surrender.
- **Continuous-shuffle (infinite shoe):** card i = `keccak256(seed, i) % 52`, dealt with replacement. The single Chainlink VRF word (seed) commits the ENTIRE card stream before any player action — provably fair; player actions only reveal predetermined cards. (Honest ⓘ note on the page. Demo caveat: post-fulfill the seed is readable on-chain; bounded by the 2% exposure cap, play-money only.)
- **Spread 1.25%** on every gross stake unit at bet time: at-risk `netStake = stake × 98.75%`. Win returns 2×net, BJ 2.5×net, push returns net. House edge = rules edge + spread.
- Fee waterfall on total gross volume at settlement (void-safe, escrowed until then): burn 4,000 ppm → deployer(burnSink), rebate 1,700 ppm, standby 500 ppm, residual 6,300 ppm → demo RiskVault. Total = 12,500 ppm.
- Exposure cap: worst-case payout (split + double both, win both = 8×unit-net) ≤ 2% of free float. Escrow accounting; owner depositFloat/withdrawFloat can never touch escrow.
- Timeouts: VRF > 1h → `voidHand` (anyone, FULL gross refund); player inactive > 30 min mid-hand → `forceStand` (anyone, settles per current cards).

## Tests

`protocol/test/Blackjack.t.sol` — 25 tests, all green (naturals 3:2 / BJ-push / dealer-BJ, S17 soft-17 stand + hit-to-17, win/push/lose payouts, bust-loses-first, double (escrow + payout + 2-card-only), split (pair-only, once-only, aces auto-stand), fee waterfall conservation on 1× and 2× gross, exposure cap, escrow-safe float withdrawal, VRF-void full refund, forceStand, pause, auth; fuzz: 256-run invariants over seeds×strategies — payout bounded, escrow always released, token conservation; fuzz dealer-final ∈ [17,26]). Full suite 60/60 green.

## Live Sanity (mainnet hand #0)

startHand 100 RUSD `0xce10dcd1…f43b9b` → VRF dealt [3♦,5♣ v 9♦+hole] → hit `(card 10)` → stand `0x620072b6…4442ef` → settled: player 18 v dealer 22 BUST, outcome WIN, payout 197.50 RUSD (2×net ✅), fees split on-chain (vault residual 0.63 ✅), lockedEscrow back to 0 ✅.

## Frontend v2.1.0

- `app/blackjack.html` + `app/blackjack.js`: dark felt table w/ RAIN logo watermark, CSS playing cards (unicode suits, court-face frames, deal slide-in + hole-card 3D flip, 420ms suspense stagger), chip betting (1/5/25/100/500) → DEAL, contextual HIT/STAND/DOUBLE/SPLIT (contract `availableActions` + balance check), split two-hand layout w/ active-hand glow, soft totals shown as e.g. 7/17, gold "BLACKJACK! 3:2 🃏" overlay + win confetti / loss / push fx, hands history sidebar (localStorage), balance bar + faucet + tx pill + RPC pool + shared burner wallet, mobile responsive.
- Approval is `stake × 4` up front so double/split never need a second approval.
- Nav: 🃏 BLACKJACK buttons added to index.html + roulette.html (patched from LIVE copies fetched after roulette v2.0.0 was deployed — no clobber; roulette.js untouched).
- Deployed via agentbase update-code (4 files) + redeploy → live; verified 200 + v2.1.0 served; headless chromium: zero console errors (local + live).

## Balances After Blackjack Deployment

- Deployer ETH: **0.01259** · Deployer RUSD: 80,000,119 (85M − 5M blackjack bankroll + hand-#0 flows)

---

# BLACKJACK V2 — v2.2.0 (Fee-on-Loss + S17 Verification)

**Date:** 2026-07-28 · Replaces Blackjack v1 (`0x3857…7Da5`, now PAUSED and drained).

## Issue 1 — "Dealer hit on 17" bug report: verdict

- Repo source of `Blackjack.sol` is S17-correct (`if (dt >= 17) break;` in `_dealerPlay`, stands on ALL 17s incl. soft).
- **Deployed v1 bytecode verified byte-identical to repo source** (compiled `deployedBytecode` vs `cast code`, 0 non-immutable diffs). The on-chain contract could NOT have hit on 17.
- Frontend audit: dealer totals computed by the same `totalOf` mirror; hole card hidden until settlement — a plausible source of the report is the dealer's **soft total transitioning** (e.g. A+6 displayed after more cards from an earlier draw sequence) or the reveal animation showing dealer draws that happened when dealer was <17 (e.g. soft 16 → draw → 17+). Audited all 10 v1 on-chain hands: **no hand shows a dealer card drawn at ≥17**. E.g. hand 9 dealer A? sequence [24,40,14,20] = 12,2,2,8 → totals 12→14→16→(draw)→hard 24? — all draws happened below 17. No contract or display bug found; rule enforced correctly.
- New forge tests added anyway: `test_dealerStandsOnHard17`, `test_dealerStandsOnSoft17` (A+6), `test_dealerStandsOnMultiCardSoft17` (A+2+4), `test_push17_fullRefund` (player 17 v dealer 17 = push, dealer takes no card).

## Issue 2 — Fee model redesign (V2): fees ONLY on losses

- **Old (v1):** 1.25% spread deducted from every gross stake upfront → win on 100 paid 197.50, push returned 98.75. Bad UX.
- **New (v2):** exact payouts — win **2× gross** (100 → 200), push **full refund**, blackjack **2.5× gross** (100 → 250). No haircut ever on wins/pushes.
- **Fee choice (documented):** 1.25% (12,500 ppm) of the **lost gross stake** (per losing hand; a doubled losing hand contributes 2 units) routed through the same waterfall sinks and ppm split as v1 / the demo FeeSplitter: burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 → demo RiskVault. Fee is funded from house winnings, never from the player's payout. House edge = rules edge (~0.6% S17 infinite shoe); fee is a revenue split of losses, not a player levy.
- **Exposure cap recomputed:** worst case = split + double both + win both = 8 × **gross** stake (was 8 × net). Cap unchanged at ≤ 2% of free float → max stake exactly 12,500 RUSD on 5M float (test `test_exposureCap`).
- Escrow / voidHand (VRF >1h, full gross refund) / forceStand (30 min) identical to v1.

## Contract

| Item | Value |
|---|---|
| Blackjack V2 | `0x096382Af1E2B6CC5D900E22eF7DCe53eeF4F2808` |
| Deploy tx | `0x96bac6dca15b3cb7385f3f69c323b4f30cc669bff4785388120aaa6b02b32a67` |
| v1 setPaused(true) | `0xbafbb0458257a6f4b227271f432a191c4316524e91315f548069475ccabf7ec2` |
| v1 withdrawFloat (4,999,876.5625 RUSD free float) | `0x0c701c07a779f4979b56a8f4ccaa7919d361acfbf01b5bdbaa36988649864094` |
| approve 5M RUSD | `0x594cce6e080a1a9d0a913f95da92297a36f01121b02bc8cf48b9ee4176c87154` |
| depositFloat 5,000,000 RUSD | `0x5338613f6f9554f19a068a9c0d714da42bd896530880a9b07a041332ffb814f0` |
| addConsumer (VRF sub) | `0x6b0a92202983eb3e81518eaab8d82c6dc5d91c93b4f847ca7b1d76ee8a036b2f` |

Same deployer, VRF sub, RUSD, sinks (deployer ×3 + demo RiskVault) as v1.

## Tests

`test/Blackjack.t.sol` updated to v2 math + new tests: 32/32 green, **full repo suite 67/67 green**. New: hard-17 stand, soft-17 (A+6) stand, multi-card soft 17 stand, 17v17 push full refund w/ zero fees, exact 2×/2.5× no-fee payouts, fee-on-lost-gross (1× and doubled 2×), split mixed-outcome fee base, exposure cap at exactly 12,500.

## Live Sanity (V2 hands #0 and #1)

- Hand 0: start `0xac03e7ba…3c08c` → player 5+5=10 v dealer 10-up + hole ACE = dealer natural → LOSS. Contract bal +98.75 RUSD net (house kept loss minus 1.25 fee routed to sinks ✅).
- Hand 1: start `0x2832de4e…a0924` → player 9+4=13, hit `0x4abcad61…81e571` (card 10 → 23 BUST) → LOSS. Contract bal +98.75 again ✅. lockedEscrow 0 after both ✅. Fee-on-loss math confirmed on-chain.

## Roulette upfront-fee check (report only, unchanged)

**Yes — RouletteTable (`0x38a0…1D01`) has the same v1 UX problem:** 1.25% spread is taken from every gross bet upfront (`netStake = 98.75%`), so wins pay on net (e.g. even-money 100 → 202.99 instead of "clean" 202.99×gross… i.e. payouts are multiplier × net, and any refund path also nets). Same fee-on-loss redesign would apply. NOT changed per instructions.

## Frontend v2.2.0

- Base = live blackjack.js (verified identical to repo copy; `makeAddrCopyable()` click-to-copy preserved).
- New contract address, removed net-stake math from result display (net = payout − gross), PUSH overlay now "full stake returned / no fee on a push", ⓘ note: "no fee on wins or pushes; the 1.25% table fee is taken only from losing stakes".
- Deployed via agentbase update-code + redeploy on workflow `b086e87d-…cd1` → **live**; verified rainsandbox.xyz serves v2.2.0, new address, and copy feature.

## Balances After V2

- Deployer ETH: **0.01674** (floor 0.005 respected) · Blackjack V2 float: 5,000,197.50 RUSD (5M + two lost hands net of fees).
- v1 (`0x3857…7Da5`): paused, ~0 free float (only residual dust from spread rounding remains).

---

# BLACKJACK V3 — v3.0.0 (TRUSTLESS Per-Action Randomness, Pyth Entropy)

**Date:** 2026-07-28 · Replaces Blackjack V2 (`0x0963…F2808`, now PAUSED and drained).

## The V2 bug fixed

V2 committed the ENTIRE card stream (dealer hole card + all future draws) to a single
Chainlink VRF word stored in `seed` the moment the deal landed — any on-chain reader
(bot) could derive every hidden card and play perfectly. V3 makes every hidden card
**unknowable to anyone (house included)** at decision time.

## Oracle — Pyth Entropy (verified)

- Entropy contract (Arbitrum One): `0x7698E925FfC29655576D0b361D75Af579e20AdAc` — verified live on-chain (`getDefaultProvider`, `getFee` calls succeed).
- Default provider: `0x52DeaA1c84233F7bb8C8A45baeDE41091c616506` (returned by `getDefaultProvider()`; matches Pyth docs mainnet default).
- Fee at deploy time: ~0.000077–0.0000937 ETH per request (dynamic; contract reads `getFee(provider)` before every request).
- Flow: `requestWithCallback` → provider commit-reveal → `_entropyCallback(uint64,address,bytes32)` (guarded `msg.sender == entropy`). Latency observed: ~1–3 s.

## Design (per-action entropy)

- `startHand(stake)`: escrow gross, entropy #1 → callback deals **player 2 cards + dealer UP card only**. The hole card does not exist yet (dealer array length 1 on-chain — verified live).
- `hit` / `doubleDown` / `split`: escrow extra stake first (double/split), then fresh entropy; callback reveals the drawn card(s) (split: 2 cards from one word — both go to the player immediately, no decision in between).
- `stand` / hand complete / player natural: one **final** entropy → callback draws hole card + all dealer S17 draws from that word and settles. Safe: player has no further decisions. All-bust hands settle instantly with NO dealer cards.
- **Rule variant: ENHC (European no-hole-card)** — documented in contract natspec + frontend ⓘ. No dealer peek; a dealer natural revealed at settlement beats ALL non-natural hands **including doubled/split additional stakes** (player natural still pushes). Other rules unchanged: S17, BJ 3:2, double any two, split once (aces one card each), infinite shoe.
- Economics EXACTLY V2: win 2× gross, push full refund, BJ 2.5×; 1.25% of LOST gross → burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 ppm → demo RiskVault; sinks = deployer ×3 + `0xEcF9…3A18F`.
- **Entropy fees paid by the contract** from its own ETH float (simpler UX than msg.value; players pay zero extra). `startHand` requires float ≥ 8× current fee so a hand can never strand for lack of fees; owner tops up by plain transfer, `withdrawEth` reclaims.
- Safety rails kept: 2%-free-float exposure cap (max stake 12,500 RUSD on 5M), lockedEscrow accounting, `voidHand` after 1h pending entropy (FULL gross refund incl. double/split escrow, works in ALL pending states incl. mid-action), `forceStand` after 30 min idle (stands remaining hands, requests final entropy on house-paid fee), pause, escrow-safe withdrawFloat, nonReentrant + CEI, entropy-only callback auth, stale-callback no-op after void.

## Contract & txs

| Item | Value |
|---|---|
| BlackjackV3 | `0x2E7747E9c4c18511A4F9e79C6bD38061e1443F7A` |
| Deploy tx | `0x9639a4f9e239c236e6bb0f83a7cfd589f09fe26d474a3a1b4df479ebfb418c71` |
| V2 setPaused(true) | `0x9c2e0440c5c084451278ab5c77bd099fb29930cb3d87e61940e82ca606d15b40` |
| V2 withdrawFloat (5,000,172.1875 RUSD) | `0x443dd93215946d2b23bd57351f1f60f6802adc7b3872cf7feadd85e34ed25f1d` |
| approve 5M RUSD | `0x29e700a95946e9886f5828de131c4484a8bb756a90954cce7379884514a5a000` |
| depositFloat 5,000,000 RUSD | `0xa1079fa407c386f36f37878c9d1211745c6514f8a8aea730b7650cfbd518f22f` |
| fund entropy fee float 0.003 ETH | `0xf8c03fb7e89bd7924d24f231cd132e19761a90ab63ce2cef75733ac6871f23fe` |

## Tests

New `test/BlackjackV3.t.sol` (MockEntropy commit/fulfill): 28 tests — deal reveals only 3 cards
(no hole in storage), async hit/double/split with pending-state guards, ENHC dealer-natural beats
doubled stake, S17 (soft/hard 17 stand, hits to 17), naturals 3:2 / BJ push, push full refund,
fee-on-loss waterfall (single + doubled + split-mixed conservation), exposure cap 12,500,
escrow-safe withdrawal, FeeFloatLow guard, entropy-timeout voids (deal + mid-double, full gross
refund, stale-callback no-op), forceStand, callback auth, owner ETH withdraw, 2 fuzz invariant
suites (256 runs: settlement, escrow release, payout bound, token conservation, dealer 17–26).
**Full repo suite: 97/97 green** (V1/V2 blackjack, roulette, protocol, fuzz all kept passing).

## Live hands (Arbitrum One, real Pyth Entropy)

- **Hand #0** (100 RUSD, with hit): start `0x43afde05…d4a761` → deal callback **~3.0 s** (player 4+10=14 v dealer 3-up, hole nonexistent) → hit `0x987aba17…8273db` → draw callback **~3.0 s** (drew 6 → 20) → stand `0x504f3aa3…fda333e` → final callback **~0.9 s** (dealer 3+A+Q+Q = 24 BUST) → **WIN +100 RUSD** paid, lockedEscrow 0 ✅.
- **Hand #1** (100 RUSD): start `0xfca10f19…82fe9` → deal **~1.6 s** (K+9=19 v 8-up) → stand `0xea270059…898cc` → final **~1.6 s** (dealer 8+8+8 = 24 BUST) → **WIN +100 RUSD** ✅.
- **Observed per-action latency: ~1–3 s** (well inside the 1–4 s target).
- **Cost per action:** gas ≈ 0.0000033–0.0000058 ETH (tx) + entropy fee ≈ 0.00008–0.000094 ETH (paid by house float). **Cost per hand:** ~0.00019 ETH house entropy fees + ~0.00001 ETH player gas for a stand-only hand; ~0.00028 ETH entropy + ~0.000015 ETH gas with one hit (≈ $1 total at $3.5k ETH). Contract fee float after 5 requests: 0.00258 ETH remaining of 0.003.

## Frontend v3.0.0

- `app/blackjack.js` + `blackjack.html`: new address + V3 ABI (state machine with DrawPending/FinalPending), per-action pending pill ("fresh entropy…") + card reveal on callback, facedown hole placeholder that only materializes at settlement (removed on all-bust), player-natural async resolution, ⓘ panel rewritten (trustless per-action Pyth Entropy + ENHC rule), footer "PER-ACTION PYTH ENTROPY".
- Deployed via agentbase update-code + redeploy (workflow `b086e87d-…cd1`) → **live**; verified https://rainsandbox.xyz/app/blackjack.js serves `v3.0.0` and `0x2E77…3f7a`.

## Balances after V3

- Deployer ETH: **0.01363** (floor 0.005 respected) · V3 RUSD float **4,999,800** (5M − two 100-RUSD player wins) · V3 ETH fee float **0.002582**.
- V2 (`0x0963…F2808`): paused, drained to dust.

## Residual risks / notes

- Pyth Entropy provider liveness: if the default provider stops revealing, hands stall — mitigated by 1h `voidHand` full-refund path (works in every pending state). Contract unverified on Arbiscan (no API key, consistent with all prior deploys).
- ENHC increases house edge slightly vs peek (~+0.11%); disclosed in UI.
- userRandomNumber is derived from prevrandao/timestamp — fine because Pyth's commit-reveal makes the provider contribution unpredictable regardless.

---

# RAIN CHANNELS — v4.0.0 (State-Channel Blackjack)

**Date:** 2026-07-28 · **Chain:** Arbitrum One (42161) · Implements `research/rain-channels-spec-v1.md` with all 9 founder decisions RESOLVED (see spec §9).

## Contracts (mainnet, deployed & wired)

| Contract | Address | Deploy Tx |
|---|---|---|
| ChannelManager | `0x82698e4C14DC5D144bdA2a028aDe60d886681AFb` | `0x7a88df721f177503da40faa109bfbb650bd154f7bba59cad76ba4bd2b75e2976` |
| HouseBond | `0x37E41E1d1EE2F96cE0203af06d639448547ade84` | `0x4f3cd867a7f0e9b423258e28b0e593ac8c2f2949bda200395a17cca6b205d502` |
| BlackjackRulesVerifier | `0xFdcCE2DE2F8A28cBBdBaB01131C3bab138Eb9293` | `0x5be5d4c64e446c3832af0ee6ccbd4ad6c9f15f6ec72c5cab5f3d66113b04e1dd` |

Wiring: bond.setManager `0x6cbd84…04c50`, setOperator(deployer) `0x6f9fa2…3eac38`, 100k RUSD bond posted `0x8ec754…d569b6`, 0.01 ETH entropy fee float `0x86aa18…ac7a0d`. Second operator (production node key `0xbc3Aca2Fb930D4Ca6a7691B40e0e077B12623F29`): setOperator `0xd6c118…841392`, 100k RUSD bond, 200k RUSD float + 0.01 ETH funded.

## Design (per founder decisions)
1. **Pyth Entropy sessionSeed at open — ON.** One `requestWithCallback` per channel; fee paid from ChannelManager ETH float; `voidOpen` full-refund after 1 h if the seed never lands.
2. **180 s player grace**, then house `forceClose` → after challenge window the open hand's gross is forfeited (fee taken on it, no extra fine). UI copy: "Disconnected? You have 3 minutes to reconnect and resume."
3. **House fault:** open hand voided in player's favor + compensation `max(2× open gross, 5 RUSD)` from HouseBond **+ equal penalty BURNED to burnSink** (never vault).
4. **Generic operators:** HouseBond registry (7-day delayed withdrawals, slashable incl. pending), `approvedOperators` map; foundation-run only for now.
5. **Auto checkpoints** by house node when |session P&L| ≥ min(500 RUSD, 5% channel size); checkpointed nonce is an on-chain dispute floor.
6. **Tiered challenge windows:** ≤ 10,000 RUSD (deposit+allocation) → 10 min; above → 24 h. One shortened (5 min) restart after a successful challenge.
7. **Statistical fairness:** transparency only (house node WAL keeps full signed history; no on-chain statistical slashing in v1).
8. **Fee waterfall at close:** 1.25 % of cumulative lost gross, split burn 4000 / rebate 1700 / standby 500 / residual 6300 ppm — executed on-chain in cooperative AND forced close.
9. **Game-agnostic ChannelManager:** pluggable `IRulesVerifier`; blackjack module = exact V3 rules (ENHC/S17, win 2×, push refund, BJ 2.5×) with `card_k = keccak(pRev_k, hRev_k, sessionSeed, channelId, k) % 52`, house-reveals-first off-chain ordering, hash-chain (N=4096) pre-commitment, on-chain reveal verification (`submitReveals`/`demandReveal`) and `proveBadTransition` fraud proof.

EIP-712 states (domain "RAIN ChannelManager" v1); burner **session keys** sign states but funds always pay out to `playerOwner` wallet (open authorized by wallet-key EIP-712 signature). Exposure caps: Σ allocations ≤ 2 % vault float, single ≤ 0.25 %; `8×maxUnitStake ≤ allocation` enforced (no-stall-profit sizing).

## House node (production)
- Deployed inside **rainsandbox-api** (agentbase backend, workflow `b086e87d-…cd1`) — WS + faucet share one service: `wss://api.rainsandbox.xyz` (WS), `POST /faucet`, `GET /health` (reports openChannels/operator). Bundled via esbuild (`services/house-node/dist/house-node.cjs` + `deploy/server.js`); commit `f2a810b5`.
- Env keys (Render env vars, never in code): `OPERATOR_PRIVATE_KEY` (0xbc3A…3F29), `HOUSE_SESSION_PRIVATE_KEY` (0xDC32…3563), `CHANNEL_MANAGER`, `DEFAULT_ALLOCATION=2000 RUSD`, plus existing faucet vars.
- Guarantees implemented: write-ahead fsync persistence BEFORE countersigning, house-reveals-first, 180 s grace timers → forceClose, auto-checkpointing, reconnect/resume replay.
- ⚠️ Caveat: `DATA_DIR=/tmp/rain-channels` on Render is **ephemeral** — a node restart loses session WAL (players can always forceClose on-chain from their own signed states; funds are never at risk, but the house would rely on checkpoints). Production-grade needs a persistent disk or DB.

## Tests
- Forge: **132/132** green (97 pre-existing + 35 new in `test/Channels.t.sol`: bond slash/withdraw-delay/comp-first, open/seed/voidOpen, exposure caps, coop close + waterfall exactness, conservation enforcement, stale-state challenge, checkpoint floor, player-forfeit, house-comp+burn, comp floor, tiered windows, reveal chains, demandReveal, bad-transition fraud proof both directions, fuzz conservation + cross-channel replay). All 97 pre-existing tests stay green (via_ir enabled).
- Integration (Anvil, `services/house-node/test/integration.ts`): **14/14** — happy path 8 hands (hit/double/split-capable), waterfall verification, stale-close attack beaten by newer state, player-stall forfeit with exact payout math, checkpoint-floor rejection.

## E2E on Arbitrum One mainnet (real Pyth Entropy)
- **Happy path** (`test/e2e-mainnet.ts`): open 5.3 s (tx `0x5fe6265b…5502f8`, incl. entropy seed ~4.7 s) → 3 hands with hits + auto-checkpoints (txs by node) → cooperative close 4.6 s, gas 105,592 (tx `0x23518fbb…496d3`). **Per-action latency: avg 39 ms, max 49 ms** (local WS). Session P&L −150 RUSD settled exactly.
- **Forced-close dispute drill**: channel `0xf34576b9…`, played deal+2 hits, player `forceClose` from nonce 3 (tx `0x66036cd2…968674`, gas 197,873) → waited real 10-min window → `finalizeClose` (tx `0x08dbd039…02d9df`, gas 89,179) paid per last dual-signed state.
- **Production smoke over wss://api.rainsandbox.xyz**: open 11.3 s, 3 hands, **per-action avg 313 ms / max 420 ms** (internet RTT), close 5.0 s (tx `0xb7d35b5f…54563`), payout exact (+20 RUSD net).
- **Cost per session:** open ≈ 470k gas + Pyth fee (~0.00009 ETH, paid by mgr float) + close ≈ 105k gas ≈ **$0.10–0.15 total at typical Arbitrum prices — zero marginal cost per hand** (vs V3's ~0.0002–0.0003 ETH per hand).

## Frontend v4.0.0
- `app/channels-sdk.js` (new): burner session key, browser hash-chain, EIP-712 signing/verification, WS protocol with house-reveal verification, engine mirror, reconnect/resume, forceClose/challenge/finalize dispute helpers.
- `app/blackjack.js/html` v4.0.0: **⚡ CHANNEL mode default** (open session → instant hands → close & settle) with session bar (state + live session P&L), positive disconnect copy, rewritten ⓘ panel (commit-reveal, 3-min grace, house-bond compensation, fee-at-close); **⛓ PYTH toggle** keeps the entire V3 per-action flow as fallback. Deployed via agentbase → live; verified `v4.0.0` + SDK served at rainsandbox.xyz.

## Balances after deployment
- Deployer ETH **0.2591** · Deployer RUSD 74,600,194 (after 100k+100k bonds, 200k operator float, session flows)
- Operator node wallet 0xbc3A…3F29: 0.00999 ETH + ~200k RUSD · ChannelManager ETH float 0.00977 (~100 session seeds)
- V3 blackjack (0x2E77…3f7a) untouched & live as fallback.

## Honest caveats / remaining for production-grade
- **Unaudited** (~1,600 new SLoC; spec §7.3 estimates $60–150k audit). proveBadReveal is subsumed by submitReveals chain-verification on-chain; off-chain bad reveals are simply rejected + dispute path.
- Single house-node instance on Render starter (no hot-hot HA, ephemeral WAL, no HSM). Watchtower not built.
- Challenge/finalize automation for the HOUSE side of expired grace closes is timer-based in-process only (lost on restart until channels are re-loaded from WAL).
- 24 h tier window < Arbitrum force-inclusion delay concern only matters for real-value stakes (spec §4.4).
- Contracts unverified on Arbiscan (still no API key, consistent with all prior deploys).

---

# RAIN CHANNELS — v4.0.1 (2026-07-28, funds-recovery + session persistence)

## Incident: 1,500 RUSD abandoned in escrow
Player `0xe6baFC62c0FbBAbb8067d8da4F31F7395Fa512Da` opened 3 × 500 RUSD channels and never closed them — the v4.0.0 frontend created the burner session key + SDK chainSecret in memory only, so any page refresh made resume impossible and the escrow was abandoned.

**Recovery executed (all 3 + 1 more found in sweep):** the house WAL held the latest dual-signed states; recovered via operator-submitted `cooperativeClose` (instant exact payout, no challenge window needed since states were idle/dual-signed):
| Channel | Player payout | P&L | Tx |
|---|---|---|---|
| `0x94795018…` | 575 RUSD | +75 | `0x4dbd0511…642e837f` |
| `0xea1c98b8…` | 425 RUSD | −75 | `0xc876c5ff…fd461fb7` |
| `0xffd0e13c…` | 550 RUSD | +50 | `0xdd0da7d3…379694d84` |
| `0xc1ef2525…` (4th abandoned, found in sweep) | 850 RUSD | +350 | `0x90dca4a0…5f9e60e8` |
All verified on-chain: RUSD Transfer CM→player + CooperativeClosed events; channels now status Closed. Total returned to player: **2,400 RUSD**.

## Fixes deployed
**Frontend v4.0.1** (`app/blackjack.js`, `app/channels-sdk.js`):
- SDK `snapshot()`/`restore()` — full session material (burner session key, chainSecret, sessionSeed, houseChainRoot, hReveals, latest dual-signed state + sigs, game).
- `localStorage` persistence (`rainChSessionV1`) written at open and after every state advance; cleared on cooperative close. Burner-key-in-localStorage matches the existing `rainDemoBurnerV1` trust model (play-money demo).
- Page load auto-resume: reconnect + hello/resume from persisted snapshot; if the house answers "unknown channel" (WAL loss) but the channel is Active on-chain, the client submits `recover_close` with its dual-signed idle state.

**House node** (`services/house-node/src/server.ts`, bundled to `dist/house-node.cjs`, deployed in rainsandbox-api):
- **Abandoned-channel reconciliation**: boot-time sweep + 5-min interval — idle (phase IDLE, no pending) channels with a disconnected player > 30 min (`ABANDON_MS`) are settled by operator `cooperativeClose` from the latest dual-signed WAL state (exact payout, immediate). Open-hand stalls remain on the grace→forceClose path.
- `POST /admin/reconcile` (header `x-admin-secret`, env `ADMIN_SECRET`) — force-sweep on demand; returns per-channel report.
- `scheduleFinalize`: after any house forceClose, polls closeDeadline and auto-`finalizeClose` (previously finalize was manual/lost on restart).
- `recover_close` WS message: player-driven WAL-loss settlement — node re-verifies BOTH signatures against the on-chain channel record (its own houseSessionKey), checks conservation + idle phase, then `cooperativeClose`. Trustless for the house: only states it previously countersigned can settle.

## Tests
- Integration (Anvil): **24/24** — prior 14 + refresh simulation (snapshot → socket drop → new client restores, plays, closes exact), abandoned-channel reconciliation (force sweep pays exact balance, channel Closed on-chain), WAL-loss recovery (`recover_close` pays exact dual-signed balance).
- Forge: **132/132** still green (no contract changes).

## Honest caveats
- A 4th channel `0xdd64aa43…` (500 RUSD deposit) was opened by the same player at 18:12 UTC on the OLD v4.0.0 frontend, during this maintenance window — the backend redeploys wiped its `/tmp` WAL. It is Active on-chain with **no house-side dual-signed state available**, so the house cannot close it. `cooperativeClose`/`forceClose`/`checkpoint` all require dual signatures; `voidOpen` only applies in SeedPending. **Only the player can recover it** (their open tab still holds the signed state → SDK `forceClose`, or replaying via `recover_close` if they have the state). If the player's tab is gone, the 500 RUSD deposit + 2,000 RUSD allocation remain locked in the contract indefinitely — this is exactly the failure mode v4.0.1 persistence eliminates going forward.
- `DATA_DIR=/tmp/rain-channels` is still ephemeral on Render: node restarts lose WAL. v4.0.1's client-side persistence + `recover_close` now covers idle states, but an open-hand state at restart still relies on the player's snapshot. Production-grade needs a persistent disk/DB.
- Reconciliation `cooperativeClose` is chosen over `forceClose` for abandoned idle channels (instant, exact, no window). Force path is reserved for open-hand stalls.

---

# RAIN CHANNELS — v4.1.0 (2026-07-28, ChannelManager V2 migration + durable checkpoints + audit prep)

## ChannelManager V2 (reclaimStale) — deployed & live
- **V2:** `0xacbfCf3523DDA59419a181Afb2e17A80BE453fEb` (Arbitrum One, tx `0x65723a95…824169`, script `DeployChannelManagerV2.s.sol`). Adds `reclaimStale(channelId)`: permissionless stale-channel escrow recovery after **30 days of zero on-chain activity**, settling conservatively at the last on-chain checkpoint (or opening balances if none). This closes the "both WAL and player snapshot lost → escrow locked forever" failure mode permanently.
- Wiring (same run): `HouseBond.setManager(V2)` ✓ (verified on-chain), both operators approved ✓, 0.005 ETH entropy fee float ✓. Bonds untouched (2×100k RUSD still slashable).
- **Frontend v4.1.0** deployed: `CH.manager` → V2 (EIP-55 validated with ethers `getAddress`), verified served at rainsandbox.xyz. **Backend** redeployed with `CHANNEL_MANAGER=V2`; health OK at api.rainsandbox.xyz/health.
- **V1 (`0x82698e4C…1AFb`) drained & retired:** all 8 channels ever opened checked on-chain — 7 Closed, 1 stuck (below). No open escrow besides the stuck channel.

## Stuck V1 channel 0xdd64aa43… — final resolution
- Channel `0xdd64aa438f8d7a1b1dd8c3ec9b65fdaa7cbe7a2536d9d144b9258d68d105b720`: Active on-chain, 500 RUSD player deposit + 2,000 RUSD house allocation, **no dual-signed state exists on either side** (WAL wiped mid-maintenance; player snapshot pre-dates v4.0.1 persistence).
- Every V1 path re-audited: `cooperativeClose`/`forceClose`/`checkpoint` all require dual signatures (none exist); `voidOpen` requires SeedPending (channel is Active, seed landed); `demandReveal`/`submitReveals` only move reveal cursors, never settle; `proveBadTransition` requires a dual-signed prev; no owner/operator escape hatch exists in V1 (by design). **The 2,500 RUSD is locked in V1 permanently.** V1 has no reclaimStale — that is exactly why V2 exists.
- **Goodwill refund executed:** 500 RUSD sent from deployer treasury to player `0xe6baFC62c0FbBAbb8067d8da4F31F7395Fa512Da` — tx `0x89ff39e8d9c09511f0a41141431afccccb006b6feb12d7b6a18bb36b9fa5c19d` (status 1). Player is made whole on their deposit; the 2,000 house allocation is a treasury write-off inside V1.

## House-node durability (v4.1.0)
Bar: a backend redeploy mid-session must never destroy the only copy of a signed state. Three layers now:
1. **Per-hand on-chain checkpoints** (default ON, `CHECKPOINT_EVERY_HAND=0` to disable): every completed hand's dual-signed state anchored via `checkpoint()`. Measured **~130k gas** on Arbitrum (≈$0.01–0.02) — cheap enough to run always. Checkpointed nonce is the settlement floor for reclaimStale/forceClose.
2. **Checkpoint-on-disconnect:** idle dual-signed state anchored when a player's socket drops.
3. **SIGTERM/SIGINT flush:** before the process exits (Render redeploy sends SIGTERM, ~30 s grace) every open channel's latest un-checkpointed state is checkpointed.
Legacy ±500 RUSD threshold retained as fallback when per-hand mode is disabled. Integration tests updated (adversarial stale-state sections explicitly disable per-hand checkpoints so stale states remain closable): **24/24 green**. Forge: **140/140 green**.

## Audit prep
- **Sourcify exact_match verification (chain 42161):** ChannelManager V2 ✓, ChannelManager V1 (from commit `e7da239` source) ✓, HouseBond ✓, BlackjackRulesVerifier ✓. (Arbiscan API verification still blocked — no ETHERSCAN_API_KEY; Sourcify links render on Arbiscan/Blockscout regardless.)
- **`research/audit-scope.md`** written: contracts + SLoC, 10 invariants, threat model, accepted risks, addresses, coverage summary.
- `forge coverage` unobtainable: via_ir + coverage instrumentation → "stack too deep" even with `--ir-minimum`. Documented honestly in audit scope.
- No TODO/FIXME markers in channels contracts.

## Honest caveats
- V1 keeps 2,500 RUSD locked forever (documented above; player compensated). Do not re-point anything at V1.
- Per-hand checkpoints put hand-level P&L cadence on-chain (privacy = transparency tradeoff, fine for demo).
- Checkpoint TXs are house-node-signed; if the operator wallet runs dry of ETH the node degrades to WAL-only durability (log alerts on checkpoint failure).
- Coverage percentages unavailable (tooling), tests-to-code ≈1.9:1 by lines.

---

# RAIN CHANNELS — v4.2.0 (2026-07-29, ChannelManager V3: simulator-parity bet limits)

## Why
The RAIN simulator model sizes blackjack table limits at **1733 ppm of vault float** ($520,000/hand on $300M TVL). The demo vault float is ~5M RUSD → target max bet ≈ **8,665 RUSD/hand**, but V2's frontend/protocol capped bets at 100 RUSD. v4.2.0 brings the live demo to simulator parity.

## ChannelManager V3 — deployed & live
- **V3:** `0xe13DC056111906aA4d3659880EBDC67138c76DA7` (Arbitrum One, deploy tx `0xbcfe02b9a820192020121b7c6df80b8996b9ee737634621b7a9d1527ff32068d`, script `DeployChannelManagerV2.s.sol` re-run with V3 source). **Sourcify exact_match ✅** (creation + runtime).
- Constant changes vs V2 (immutable, hence new deploy): `EXPOSURE_SINGLE_PPM` 2,500 → **15,000** (1.5% of float per channel), `EXPOSURE_TOTAL_PPM` 20,000 → **100,000** (10% of float), `TIER_THRESHOLD` 10k → **100k RUSD** (mid-size sessions keep the 10-min challenge window). Everything else byte-identical: fee waterfall, 8×maxUnitStake ≤ allocation, reclaimStale (30d), challenge windows, bond slash math.
- Wiring (same broadcast): `HouseBond.setManager(V3)` tx `0x61bd1a03…577b08` ✓ (verified on-chain manager() == V3), operators approved (deployer `0x20dae140…`, node key `0x46f30fd6…`), **0.005 ETH** entropy fee float funded `0xebc3dbbd…`.
- **V2 (`0xacbfCf35…3FeB`) retired cleanly:** `totalAllocated() == 0` — zero open channels at migration, no stranded escrow (V1 incident not repeated).

## House node — dynamic float-derived terms
New `terms_request`/`terms` WS message; at open the node computes from live chain state:
- `unitCap = RUSD.balanceOf(riskVault) × 1733 / 1e6` (floored to whole RUSD, min 1)
- **Clamped FIRST** (before allocation is sized — the reorder fix) by: bond headroom `slashable/32` (contract requires slashable ≥ 2×2×8×maxUnitStake) and operator wallet liquidity `balanceOf(operator)/8`
- `allocation = min( max(2×deposit, 8×unitCap), vaultFloat × 1.5% )`
- `maxUnitStake = unitCap` (re-clamped so 8× ≤ allocation)
`open_request` now carries the client-signed (allocation, maxUnitStake) tuple; node re-validates against current caps (5% drift tolerance) before funding. Checkpoints/reconciliation/recover_close unchanged. `CHANNEL_MANAGER=V3` env. Rebuilt `dist/house-node.cjs` via esbuild.
- Current live sizing: vault float ~9.99M RUSD → raw unitCap ≈ 17,324 RUSD, **clamped by 100k bond → maxUnitStake = 3,125 RUSD/hand**, allocation 25,000 RUSD/channel. (To reach the full 1733 ppm ≈ 17.3k, operator bond must be ≥ 32×unitCap ≈ 554k RUSD.)

## Frontend v4.2.0
- Removed the hardcoded 100 RUSD client cap; SDK fetches house terms (`terms_request`) and signs over the exact tuple; `maxUnitStake` comes from `open_result`.
- Default deposit suggestion 500 → **1000 RUSD**; new 1K/5K chips; updated max-bet error copy (1733 ppm explanation).
- All configured JS addresses now checksum-validated at load with `ethers.getAddress()` (fail-fast — two past checksum bugs).
- audit.html + app/docs/audit-scope.md + research/audit-scope.md: V3 active, V2 retired.

## Deploys & verification
- Backend (agentbase workflow `b086e87d…`): update-backend-code `7a83ed5a` + redeploy with `CHANNEL_MANAGER=V3` → **live**; `https://api.rainsandbox.xyz/health` ok, channels:true.
- Frontend: update-code + redeploy → **live**; `rainsandbox.xyz/app/blackjack.js` serves `v4.2.0` + V3 address ✓.

## Mainnet E2E smoke (V3, real txs)
Open 2,000 RUSD deposit (house terms: allocation 25,000, maxUnitStake 3,125) — tx `0x12977f3713465d326ee2ce0e92345055dd19d59a32de2b6058de7305aad7133a`, channel `0x4670df5c…172f37`, open 7.1 s. **3 hands at 500 RUSD/hand** (>5× the old cap), per-action avg 327 ms / max 442 ms over WSS. Cooperative close 2.9 s — tx `0x8603368b60a0a0be7652937bc93c23a8f07acf6e85cd4fb3e79354e10435fd6c`, playerOut **1,500 RUSD** = deposit − 500 net loss, verified exact on-chain (status Closed, receipt success).

## Tests
- Forge: **143/143** green (new: single-cap boundary at 1.5%, total-cap 10% with 6×150k + revert on 7th, TIER_THRESHOLD 100k small-window; updated: single-cap revert at 200k, large-channel tier at 110k).
- Integration (Anvil): **24/24** green (conservation assertion generalized to deposit+allocation since allocation is now dynamic).

## Honest caveats
- The effective max bet today is **3,125 RUSD**, bond-limited, not the full 8,665–17,324 RUSD float-limited target. The clamp ordering (bond first) is deliberate: opens never revert on-chain. Raising bonds raises the table limit automatically — no redeploy needed.
- V2 remains on-chain (retired, empty). Never point anything at V1 or V2 again.
- A third-party player opened a live V3 channel during the smoke window (`0x0719…e0e7`, player `0x4736…5807`) — normal traffic, node manages it.

---

# RAIN CHANNELS — v4.2.1 (2026-07-29, self-healing session reconnect)

## Bug (user-reported)
Player opens a channel session, tab idles ~10 min (WS drops on tab sleep), reconciliation sweep (`ABANDON_MS`) cooperatively closes the idle channel → funds correctly back in the wallet, but the client kept the stale session (localStorage `rainChSessionV1` + in-memory `chSession`) and `chAutoReconnect` looped hello/resume against "unknown channel" forever: "Reconnecting… you have 3 minutes to resume" never recovered. Only a manual refresh + Open Session fixed it.

## Fix
- **SDK** (`channels-sdk.js`): `error` messages now carry `err.code` — explicit server `code` preferred, text fallback `/unknown channel/i` → `code="unknown_channel"`. No fragile string matching in app code.
- **House node** (`server.ts` → `dist/house-node.cjs`, redeployed): all "unknown channel" errors now include `code:"unknown_channel"` (hello/act/close_request). Verified live over WSS.
- **Frontend v4.2.1** (`blackjack.js`):
  - `chHealDeadSession()`: on unknown_channel → kill the reconnect loop instantly (`chReconnectGen` cancellation token), check channel on-chain (`channelInfo` read-only): Closed → honest toast "Previous session was settled back to your wallet after inactivity"; Active + dual-signed idle snapshot → `chRecoverClose` settles it. Then clear snapshot/session, session bar shows "SESSION SETTLED — reopening…", and **auto-reopen** the Open Session flow pre-filled with the previous deposit (wallet prompt shown, never auto-signed).
  - **Self-healing DEAL**: `chDeal` catches unknown_channel once → heal → reopen → re-places the pending bet automatically.
  - `chAction` also heals on unknown_channel; `chAutoReconnect` treats it as terminal (no more 3-min zombie loop).
  - **Proactive wake resume**: `visibilitychange`/`focus` → `chWakeCheck()` resumes (or heals) in the background so the session is live before the user clicks DEAL.

## Deploys & verification
- Frontend: update-code `c5077307` + redeploy → **live**; rainsandbox.xyz/app/blackjack.js serves v4.2.1 + chHealDeadSession/chWakeCheck/chReconnectGen; channels-sdk serves unknown_channel classification.
- Backend: update-backend-code (root `house-node.cjs`, a5c372cc; removed misplaced dist/ copy) + redeploy → **live**; /health ok; live WSS hello on a bogus channel returns `{"type":"error","error":"unknown channel","code":"unknown_channel"}` ✓.

## Tests
- Syntax: `new Function()` OK on both JS files; esbuild rebuild of dist clean.
- Unit (node, local mock house over ws): SDK `resume()` rejection carries `code="unknown_channel"` both with the server code and via text fallback — PASS/PASS.
- Live: WSS unknown-channel probe verified pre/post backend deploy (code absent → present).
- Honest caveat: full browser E2E (open real session → idle 30 min → reconcile → self-heal DEAL) not run headlessly; the terminal-error classification, loop cancellation, and recovery paths are unit/live-WS verified, and the heal path reuses the already-proven `chRecoverClose`/`chOpenSession` flows.

---

# ROULETTE CHANNELS — v5.0.0 (2026-07-29, roulette on RAIN Channels + frontend v2.0.0)

## Contracts (Arbitrum One, deployed & wired)

| Contract | Address | Sourcify |
|---|---|---|
| RouletteRulesVerifier | `0x5239969ff32e5733F319d3e80eD64D19adF4962f` | exact_match ✅ (creation+runtime) |
| GameMuxVerifier | `0x14155583112F87777B95AB0EA76786bf079c6A83` | exact_match ✅ |

- Script `DeployRouletteChannels.s.sol`: deploys both + `mgr.setVerifier(mux)` on ChannelManager V3
  (`0xe13D…6DA7`). Verified on-chain: `mgr.verifier() == mux`, `mux.blackjack() == 0xFdcC…9293`,
  `mux.roulette() == 0x5239…962f`. Blackjack verification untouched (mux passes actions 0..31 through).

## Design

- **Roulette channel protocol (2 actions, mux ids 32/33):**
  - `A_BET (32)`: IDLE→SPINNING. Player's bet batch (≤40 bets) is DUAL-SIGNED **before** any
    randomness is exchanged; gross debited from playerBal. Verifier enforces well-formed bets,
    balance, and **solvency** (worst-case payout + max fee ≤ gross + houseBal → `_spin` can never underflow).
  - `A_SPIN (33)`: SPINNING→IDLE. One mutual reveal pair; `number = keccak(pRev,hRev,sessionSeed,channelId,k)%37`.
    Exact casino payouts: straight 35:1 · split 17:1 · street 11:1 · corner 8:1 · six-line 5:1 ·
    dozen/column 2:1 · even-money 1:1. Losing stakes → house minus **1.25% fee on lost gross**
    (same waterfall at close as blackjack).
- **Canonical split mapping (param 0..59):** 0..2 zero-splits {0,p+1}; 3..35 horizontal {a,a+3} a=p−2;
  36..59 vertical {a,a+1}, i=p−36, a=3(i>>1)+(i&1)+1. Corner mapping identical to RouletteTable.sol.
- **Exposure mapping vs blackjack (documented + node-enforced):** blackjack worst hand = 8×maxUnitStake and
  ChannelManager requires `8×maxUnitStake ≤ allocation`; the roulette node enforces per spin
  `worstCasePayout(bets) ≤ 8×maxUnitStake` (max over all 37 numbers of Σ stake×(x+1)), so one spin can
  never risk more house funds than one blackjack hand. UI shows the even-money-equivalent gross cap (4×maxU).
- **House node (v5):** SessionRecord gains `gameType` ("blackjack"|"roulette", default blackjack for old WALs);
  `roulette-engine.ts` = exact TS mirror of the verifier (hash-parity vs Solidity proven in tests:
  identical `keccak256(abi.encode(RGame))`). All game-agnostic paths (grace, checkpoint, reconcile,
  recover_close, close) now use `phaseIdle()`/`stateFromAny()`. Blackjack protocol byte-for-byte unchanged.
- **SDK (`channels-sdk.js`):** roulette engine mirror + `ChannelSession` `gameType` option,
  `placeRouletteBets(bets)` / `spinRoulette()`, snapshot/restore carries gameType.

## Frontend — roulette v2.0.0 (rainsandbox.xyz/app/roulette.html)

- **CHANNEL mode default** with session bar (open/close & settle, session P&L, in-session balance),
  instant gas-free spins; **VRF fallback toggle** keeps the old RouletteTable per-spin flow
  (split/street/six-line blocked there with a clear toast — old contract doesn't support them).
- **Self-healing reconnect** (blackjack v4.2.1 pattern): unknown_channel heal + auto-reopen with same
  deposit, visibilitychange/focus wake resume, localStorage snapshot under **`rainChRouletteV1`**,
  recover_close from snapshot, reconnect loop with cancellation token.
- **Realistic wheel physics rewrite:** wheel spins ONE direction with exponential decay (never reverses);
  ball launched OPPOSITE direction at high speed on the rim, friction deceleration, inward spiral steered
  to the verified pocket, 2–3 damped fret bounces, then the pocket **tracks the wheel**. Final pocket
  always equals the channel/VRF outcome (target set only after the settled number is known).
- **Readable result overlay:** result now renders on a centered `.fx-card` (dark blurred panel, mint/red
  accent border, big amount, per-bet rows for multi-bets, tap to dismiss).
- **Full inside-bet zones:** splits (horizontal, vertical, 0-splits 0|1/0|2/0|3), streets, corners,
  six-lines — generous invisible circular hit areas (~38% of cell, min 16px) with hover cover-highlight
  and “SPLIT 8/11 — pays 17:1 · covers 8,11” toasts. Mapping parity JS↔verifier proven in app/tests.js.
- **Casino UX:** live **TOTAL BET** display, **MIN/MAX limits** (session-derived in CHANNEL mode, 12,500 cap
  in VRF mode), **REPEAT** (re-place previous spin's layout) and **DOUBLE** (double all placed bets), both
  validated against balance/limits; **drag-to-remove** chips (pointer events, ~34px downward threshold,
  chip ghost follows finger, trash affordance; tap still adds); **racetrack NEIGHBOURS panel**
  (wheel-order strip, X+2 neighbours each side = 5 straight-up chips, highlight + toast, plus
  Voisins/Tiers/Orphelins as straight-up approximations).
- **Nav:** prominent `← RISK MARKETS` (primary style), 🃏 BLACKJACK + 🛡 AUDIT links consistent.

## Tests

- **Forge: 163/163 green** (143 pre-existing incl. Channels suite + 17 new
  `RouletteVerifier.t.sol`: legal/illegal transitions, payout exactness for EVERY bet type (win+loss
  explored per type), 60-split mapping, corner/street/line coverage, conservation, insolvency rejection,
  reveal-count rejection, fraud proofs both directions (house underpay / player overclaim / bet tamper),
  256-run conservation fuzz, worst-case payout; + 3 new `GameMux.t.sol` dispatch tests).
- **Integration (Anvil): 38/38** (24 pre-existing + roulette: open/seed, multi-bet dual-sign, spin payout
  exactness vs independent recompute, conservation, second spin, 8× exposure rejection + state consistency,
  blackjack regression on the same node, cooperative close exact payout, snapshot→restore→spin→close).
- **Frontend unit (app/tests.js): 7/7** (covers/split/corner/street/line parity JS↔verifier, payout table,
  channel→VRF mapping coverage preservation, SDK engine conservation).
- Syntax: `new Function()` clean on roulette.js + channels-sdk.js; all addresses `ethers.getAddress()`
  checksum-validated at load (fail-fast alert).

## Deploys & live verification

- Backend: `update-backend-code` `2f7cd193` (house-node.cjs esbuild bundle) + redeploy → **live**;
  `/health` ok (operator `0xbc3A…3F29`). Frontend: `update-code` (roulette.html/js, channels-sdk.js,
  tests.js) + redeploy → **live**; curl-verified rainsandbox.xyz serves `v2.0.0`, `rainChRouletteV1`,
  `chHealDeadSession`, `placeRouletteBets`, CHANNEL/NEIGHBOURS/rtTotal markup.
- **Mainnet E2E (real Pyth seed, production WSS): 6/6** — open 7.5s
  (tx `0x0aaca240…a7e77f`), spin1 straight17+red+split8/9 **477ms**, spin2 black **469ms** (instant,
  gas-free), payouts exact vs independent recompute, conservation held, cooperative close 4.5s
  (tx `0x3b4115f3…aac5ef`) — **exact on-chain payout** (player-is-deployer sink flows accounted).
- **Blackjack regression on prod:** open → 3 hands (11 actions, avg 304ms) → close, exact payout
  (tx `0x55003ea8…4939da`). Node handles both game types concurrently (Anvil test).
- Deployer ETH after everything: **0.2436** (floor 0.005 respected — wallet was topped up before this task).

## Honest caveats

- VRF fallback mode can't place split/street/six-line bets (old RouletteTable has no such types) — UI
  explains and blocks. Call-bet buttons (Voisins/Tiers/Orphelins) are straight-up-chip approximations,
  not true split/corner call bets.
- The GameMux `finOf` tries blackjack decode first, then roulette; payload shapes are disjoint in practice
  (15-field tuple vs 7-field with struct array) but this is heuristic — the dispute path (`verifyTransition`)
  always dispatches by action id, which is unambiguous.
- Full headless-browser E2E of drag-to-remove / racetrack not run; logic is unit-tested and mirrors the
  proven placeChip path.

## v2.1.0 — 2026-07-29 — Stake.com parity pass (frontend only)

Benchmarked against Stake Originals Roulette (site geo-blocked; used YouTube gameplay thumbnails + published guides as reference).

Added:
- ⚡ TURBO mode toggle (persisted) — skips wheel animation, instant pocket snap + fast result
- 🔁 AUTOBET — repeat current bet layout N spins (or until stop), stops safely on limits/balance/failed spin
- Last-20 results ribbon (red/black/green dots) under the wheel, Stake-style
- 🔥 HOT / 🧊 COLD numbers panel from local spin history (10+ spins)
- 🔊 Subtle WebAudio sounds (chip click, win arpeggio, lose tone) with mute toggle (persisted)
- Chip visual polish (radial highlight + inner ring), felt gradient on table, result card sounds

Kept intact: CHANNEL mode, VRF fallback, racetrack/neighbours/call bets, drag-remove, split/street/corner/line zones, REPEAT/DOUBLE/UNDO/CLEAR, self-heal, history panel.
Verified: JS syntax (new Function), all 7 addresses getAddress()-checksummed, live curl shows v2.1.0, live screenshot visually checked.

## 2026-07-29 — Mobile-responsive pass (roulette v2.1.1 · blackjack v4.2.2)

Frontend-only mobile adaptation (≤640px), verified live at 390×844 and 360×800. No logic changes; CHANNEL flows untouched.

**Roulette (v2.1.1)**
- Betting table: horizontal-scroll container (`.rt-tablewrap`) with full-size 46px-row grid (min-width 600px) — all inside bets stay comfortably tappable; edge/split/corner hit zones enlarged to ≥22px on mobile (`positionZones`).
- Racetrack: single-row horizontal scroll strip with 40×44px cells.
- SPIN/AUTO/REPEAT/DOUBLE/UNDO/CLEAR: sticky bottom action bar (blurred, safe-area-inset padded), SPIN full-width 48px+.
- Chips 54px; TOTAL BET full-width; results panel capped at 240px.

**Blackjack (v4.2.2)**
- HIT/STAND/DOUBLE/SPLIT/DEAL: sticky bottom action bar, 50px min-height thumb targets.
- Bet box full-width, chips centered 50px, CLEAR full-width; felt rule text hidden on mobile (overlapped chip rail); compact session bar with full-width buttons.

**Shared (style.css)**
- Header nav → clean 2-column grid on mobile (mode-toggle/badge span full width, 44px buttons).
- Modals/result cards: 92vw, full-width choice buttons.
- iOS: 16px inputs (no zoom-on-focus), -webkit-tap-highlight cleanup, text-size-adjust, safe-area-inset bottom padding, full-width toasts.

**Audit page**
- Tables restored to real table layout with horizontal scroll + sticky first column (global mobile CSS was stacking them without labels); doc/test grids single column; toc pills 38px tap targets.

Deployed via agentbase workflow b086e87d (update-code + redeploy ×2, status live). Re-screenshot verified at both viewports.

## 2026-07-29 — Hotfix v2.1.2 — mobile page-level horizontal overflow

Real-device regression from v2.1.1 (Samsung/Chrome): whole roulette page scrolled/shifted horizontally — content pushed off-screen left, black void right. Root cause: the 600px-min-width table grid and racetrack strip sat inside CSS-grid column tracks defaulting to `min-width:auto`, so the grid item stretched to child intrinsic width instead of letting `.rt-tablewrap`'s `overflow-x:auto` clip; the overflow escaped to the page. (Headless Playwright happened to not reproduce it at initial load, but opening the racetrack did: scrollWidth 1696px vs 390.)

Fix (CSS only):
- `.rt-layout>*{min-width:0}` — grid items may shrink; scroll containers now actually contain.
- `.rt-tablewrap{max-width:100%}`, `.rt-track{max-width:100%;min-width:0;overflow:hidden}`, `.rt-track-row{min-width:0}`.
- Safety net in style.css mobile block: `html,body{overflow-x:hidden;max-width:100%}` + `.wrap{max-width:100%}` (all pages).

Verified live post-redeploy (workflow b086e87d, status live, APP_VERSION v2.1.2) with Android UA at 390×844 and 360×800 on roulette/blackjack/index/audit: `document.documentElement.scrollWidth == innerWidth` everywhere, including after opening the racetrack. Table strip & racetrack still scroll internally; desktop unaffected (changes are ≤640px media-gated except harmless `min-width:0` on grid items).

## 2026-07-29 — Roulette v2.2.0 — mobile GAME MODE (no-scroll, one screen)

Founder directive: on mobile the roulette must feel like a native casino app — the whole gameplay loop on ONE screen, zero scrolling; nav/audit links out of the way during play.

**Design (presentation-only — zero changes to game/channel/bet logic):**
- JS-activated `body.game-mode` (≤640px, or coarse-pointer landscape <500px tall). Existing DOM nodes (wheel, table, racetrack, chips, rail, actions, channel bar) are **moved** into a fixed 100dvh flex scaffold `#gmRoot` — all listeners survive; on resize back past 640px the nodes are restored to their original places (placeholder comments), so desktop is untouched.
- **Slim top strip (42px):** ☰ menu button + BALANCE + SESSION P&L + LAST result (mirrors of existing elements, updated every 600ms). ☰ opens the full original page (header nav, wallet bar, redeem, results history) as an overlay; ✕ returns to play.
- **Compact wheel (148px)** top-center in portrait; during a non-turbo spin the wheel takes over the screen fullscreen (`.gm-spin`, dimmed backdrop) and returns after the ball lands / fx shows — canvas resizes automatically.
- **Tabs TABLE | RACETRACK** replace the old NEIGHBOURS toggle (reuses the same `.rt-track.open` class; call-bet buttons VOISINS/TIERS/ORPHELINS intact). Table keeps its full-size 600px grid scrolling in place inside the tab pane — all split/street/corner/six-line zones and drag-to-remove unchanged. Racetrack becomes an 8-column grid of ≥40px cells, no horizontal scroll.
- **Bottom bar:** TOTAL BET + MIN/MAX on one line, full-width SPIN, then AUTO/REPEAT/DOUBLE/UNDO/CLEAR as a 5-column 40px-high row. Chip selector one horizontal row (44px chips) + TURBO/sound.
- **Landscape (<500px tall):** 2-column grid — wheel left (150px col), tabs+table right, chips row + bottom bar full width. Rotating mid-game just relayouts; portrait fully playable.
- Bug fixed on the way: `drawWheel` only compared canvas **width** when resizing the backing store; the default 300×150 buffer matched 150px@2x width so the canvas kept a squashed 300×150 buffer in game mode → now checks height too (1-line change).

**Live verification** (Android UA, after redeploy; workflow `b086e87d`, status live, serves v2.2.0):
- 390×844: scrollHeight **844 == 844** innerHeight, scrollWidth 390 == 390 (idle+chips, racetrack tab, spin overlay, menu overlay).
- 360×800: **800 == 800** / 360 == 360. 844×390 landscape: **390 == 390** / 844 == 844.
- Zero page scrolling in play; only the betting-table pane pans horizontally in place (deliberate — keeps ≥44px cells and full bet-zone hit areas).
- Blackjack unchanged this release (still v2.1.2-era layout, scrollHeight 1631 @844 — candidate for the same pattern next).

Deployed via agentbase update-code + redeploy (commit incl. cleanup of two stray root-path copies from the first push). Desktop layout untouched (game mode only activates via the JS toggle on small screens).
