HONEST STATUS: This is a demo deployment on Arbitrum One using play-money RUSD (faucet token). Contracts are pre-audit — Sourcify-verified, 163/163 forge + 38/38 integration tests green, but no external audit yet. Current max bet is 3,125 RUSD/hand, bond-limited (design target at $300M TVL: $520k–600k). One V1 incident is fully disclosed in the audit package. Credibility is the product — read the gaps before the claims.

BUILDER SDK — SHIP A GAME ON RAIN CHANNELS

Everything a third party needs: the on-chain rules-verifier interface, the browser client SDK, and the house-node operator path

ChannelManager V3 is game-agnostic. A game is three artifacts: (1) an on-chain IRulesVerifier — a pure referee for disputes, (2) a client-side engine mirror inside channels-sdk.js, and (3) house-node support so an operator can serve it. Blackjack and roulette are the two live modules; roulette (296 lines of Solidity, shipped in one release) is the worked example below.

Reality check: today the mux and verifier set are wired by protocol governance (mgr.setVerifier is owner-gated) and the only house node is foundation-run. Third-party games and bonded external operators are the designed path, not yet a permissionless one. Talk to us before building for production.

1THE IRulesVerifier INTERFACE

protocol/src/channels/IRulesVerifier.sol — 51 lines, the whole plugin contract

A rules verifier makes channel disputes objective: given two consecutive dual-signed states (plus the game payloads that hash to their gameStateHash fields, and any reveals), it decides deterministically whether the transition was legal.

interface IRulesVerifier {
    struct ChannelCtx {
        bytes32 channelId;
        bytes32 sessionSeed;      // Pyth Entropy seed fixed at open
        bytes32 playerChainRoot;  // h_0 of the player's hash chain
        bytes32 houseChainRoot;   // h_0 of the house's hash chain
    }
    struct FinFields {
        uint96 playerBal; uint96 houseBal; uint96 feeAccrued;
        uint96 lostGross; uint96 grossAtRisk;
        uint8  phase;     // 0 = settled/idle (closable), 1 = hand in progress
    }
    function verifyTransition(
        ChannelCtx calldata ctx,
        bytes calldata prevGame,   // abi-encoded state, keccak == prev ChannelState.gameStateHash
        bytes calldata nextGame,
        uint8 action,              // game-specific action id
        bytes32[] calldata reveals // (playerReveal, houseReveal) pairs consumed
    ) external view returns (bool valid, FinFields memory prevFin, FinFields memory nextFin);

    function finOf(bytes calldata game) external view returns (FinFields memory);
}

Design rules for your verifier:

  • Pure and deterministic. No storage, no oracle, no block data. The same inputs must verify identically forever — it's a fraud-proof court, not a game server.
  • Recompute financials, never trust them. FinFields is derived from the game payload; the manager cross-checks it against the signed ChannelState. Conservation must hold across every legal transition.
  • Randomness comes only from reveals. Derive outcomes as keccak(pRev, hRev, sessionSeed, channelId, k) % N — roulette uses % 37, blackjack % 52. Unbiased if either party is honest.
  • Enforce solvency at bet time. Roulette's _bet rejects any batch whose worst-case payout + max fee exceeds gross + houseBal, so _spin can never underflow. Your game needs the equivalent bound, and the house node must map your worst case into the manager's 8 × maxUnitStake ≤ allocation budget.
  • A legal transition must never be punishable. proveBadTransition slashes the signer of an illegal state — false positives are a critical bug. Differential-fuzz your Solidity verifier against your TS engine mirror (roulette's hash-parity is proven in tests: identical keccak256(abi.encode(RGame)) both sides).

Worked example — roulette: two actions. A_BET (mux 32): IDLE→SPINNING, the bet batch (≤40 bets) is dual-signed before any randomness exists, gross debited from playerBal, well-formedness + solvency checked. A_SPIN (mux 33): SPINNING→IDLE, one mutual reveal pair, number = keccak(pRev,hRev,sessionSeed,channelId,k) % 37, exact payouts 35/17/11/8/5/2/1:1, losing stakes to house minus the 1.25% fee. 17 dedicated forge tests cover every bet type's win and loss path, mapping parity, conservation fuzz and fraud proofs in both directions.

Registration: games plug in through GameMuxVerifier, a 45-line action-range dispatcher (0–31 blackjack, 32–63 roulette; a new game takes the next range) registered as the manager's single verifier. Dispatch isolation is tested: one game's payload can never validate under another's rules.

2CLIENT SDK — channels-sdk.js

Browser + node, depends on ethers v6 · session keys, hash chains, EIP-712 signing, snapshot/restore

The SDK (RainChannels) manages the whole session lifecycle. Real API from the live code:

const session = new RainChannels.ChannelSession({
  wsUrl: "wss://api.rainsandbox.xyz",
  chainId: 42161,
  manager: "0xe13DC056111906aA4d3659880EBDC67138c76DA7",
  gameType: "blackjack",              // or "roulette"
});

// OPEN — one wallet signature authorizes the burner session key;
// funds can only ever pay out to the wallet, never the session key.
await session.open({ playerWallet, deposit: 2_000_000_000n /* 2,000 RUSD */,
                     maxUnitStake, openNonce });

// PLAY — every action is an off-chain dual-signed state (~300ms, no gas)
await session.act(A.DEAL, 500_000_000n);   // bet 500 RUSD
await session.act(A.HIT);
await session.act(A.STAND);
// roulette: session.placeRouletteBets(bets); await session.spinRoulette();

// PERSIST — survive refreshes and tab sleep
const snap = session.snapshot();            // → localStorage
await session.restore(snap);                // resume from last dual-signed state

// CLOSE — cooperative close settles on-chain at the final state
await session.close();

What it does under the hood — all verifiable in the source:

  • Hash chain: generates a 4,096-deep keccak chain from a WebCrypto secret; publishes only the root at open; reveals preimages one per card/spin, verifying the house's chain the same way (keccak(h_k) == h_(k-1)).
  • Local rules engine mirror: a full TS/JS copy of the Solidity verifier validates every house-proposed state before countersigning. You never sign a state your own engine disagrees with.
  • EIP-712 signing: domain "RAIN ChannelManager" v1, bound to chainId 42161 and the manager address — states can't be replayed cross-channel or cross-chain (fuzz-tested).
  • Reconnect & self-healing: resume via hello replay; unknown_channel errors trigger on-chain state checks and recover_close from the local snapshot rather than zombie retry loops.
  • Dispute path: if the house vanishes, forceClose from the last dual-signed state; the challenge window and HouseBond compensation do the rest.

3HOUSE-NODE OPERATOR GUIDE

services/house-node — TypeScript WebSocket service; the operator is the party the protocol slashes
RPC_URL=... CHAIN_ID=42161 \
CHANNEL_MANAGER=0xe13DC056111906aA4d3659880EBDC67138c76DA7 \
RUSD=0x260c7019E760763988843Bc5b873d749b5937469 \
OPERATOR_PRIVATE_KEY=... HOUSE_SESSION_PRIVATE_KEY=... \
DATA_DIR=./data DEFAULT_ALLOCATION=5000000000 HOUSE_NODE_PORT=8787 \
node dist/house-node.cjs

Keys via env only. The OPERATOR key funds allocations and sends txs; the HOUSE_SESSION key signs channel states only. Durability guarantees the reference node implements (and any operator must match):

  • Write-ahead rule: every state is persisted (fsync + atomic rename) before any countersignature leaves the node — failover can never regress below a signature the player holds.
  • Per-hand on-chain checkpoints (~130k gas ≈ $0.01–0.02 on Arbitrum, default ON), checkpoint-on-disconnect, SIGTERM flush — three layers so a redeploy mid-session never destroys the only copy of a signed state.
  • Grace timers: 180 s player grace → forfeit path; reconciliation sweep cooperatively closes abandoned channels; recover_close settles from client snapshots.
  • House-reveals-first ordering: the node never sees a card before the player does — its stall option degenerates to refusable service, which is slashable.

4HOUSEBOND ONBOARDING — BOND → APPROVAL → RUN

  1. Bond. Deposit RUSD stake into HouseBond (0x37E41E1d1EE2F96cE0203af06d639448547ade84). Your bond is the ceiling on the limits you can offer: table limit ≤ slashable/32. The two live operators bond 100k RUSD each → 3,125 RUSD max bets. Withdrawals take 7 days and remain slashable while pending.
  2. Approval. Operator registration is governance-gated in v1 (single foundation-run node; HA/watchtower/HSM explicitly out of scope — see accepted risks). The bonded-external-operator program is the designed v2 path.
  3. Run the node. Deploy the house node with your keys, wire your allocation budget, and serve sessions. Liveness faults cost you max(2× open gross, 5 RUSD) compensation to the player plus an equal amount burned — per fault, automatically, from your bond.