Skip to content

Rake Reconciliation — The Casino-Boss Model

Decision

Rake reaches the chain through a separate batched flow, independent of player settlements. The casino admin clicks a button, the server walks all unclaimed house rows in deterministic order, builds a chain-of-custody hash, and pushes the total on-chain via two transactions: settleHouseRake (bumps the rake pool) and collectRakeFor (drains the pool to the rake collector).

Why Not Player Receipts?

Originally, rake was supposed to ride along on player settlement receipts — when a player stood up, the server would compute their cumulative rake contribution and include it in the EIP-712 receipt. The contract incremented accumulatedRake accordingly.

This worked for human players who eventually withdrew, but broke for eternal agent tables that never settle. Rake just accumulated in the contract's general balance, unattributed and uncollectable. Even for human tables, only winning players triggered any rake movement (the in-memory attribution was based on pot wins). A player who only lost across a session pushed zero rake on-chain.

The casino-boss model decouples rake from player behavior. Rake lives in the DB as player_id = 'house' rows in poker.hand_players from the moment a hand ends. An admin sweeps it whenever they want.

How It Works

  • House is a participant. Every poker hand writes a hand_players row with player_id = 'house' and net_chips = rake. Sum of all net_chips per hand = 0 (closed accounting).
  • Lazy chain. No chain hash is built during gameplay. At reconciliation time, the server walks unclaimed rows ordered (created_at ASC, hand_id ASC) and computes chain = keccak256(prev, tableId, unixMs, handHash, rake) per row.
  • Claim-first DB write. The reconciliation row is inserted with NULL tx hashes BEFORE the on-chain calls, marking the rows as claimed. If on-chain fails, the row is "stuck" (NULL tx hashes) and the next request returns 409 with recovery instructions instead of double-submitting.
  • Game signer triggers collection. collectRakeFor(gameId, token, destination) mirrors the withdrawForPoker pattern — Engine's backend wallet (the game signer) calls it. The original collectRake() is kept for direct rake-collector calls.
  • Audit chain across reconciliations. Each rake_reconciliations row stores prevReconciliationHash, creating an unbroken history of every collection the casino has ever made.

Key Changes

  • packages/contracts/contracts/facets/WithdrawalFacet.sol — added settleHouseRake and collectRakeFor
  • packages/db/src/schema.ts — added rakeReconciliations table and reconciliationId column on pokerHandPlayers
  • apps/admin-server/src/routes/settlement.ts/unreconciled-rake, /reconciliation-history, /reconcile-rake endpoints
  • apps/admin/app/rake/page.tsx — rebuilt rake page with three summary cards, dry-run + reconcile buttons, history table
  • apps/game-server/src/parties/poker/game-flow.ts — removed in-memory winner-based rake attribution. Player settlement receipts now always send cumulativeRake = 0.

Player Visibility

Rake is not hidden from players. Three visibility layers exist:

  • Hand-level total: poker.hands.rake
  • Per-pot breakdown: poker.hands.replay.pots[].rake in the OHH JSON, displayed on the player history page
  • Accounting closure: the house row in hand_players makes the sum of net_chips per hand equal zero

What's removed: per-player cumulative rake contribution across a session. That existed in memory only and never made it to the DB or UI. If you want it later, sum the house rows joined to the player's session.

Tradeoffs

  • Two on-chain txs per reconciliation instead of one. Could be combined into a single settleAndCollect function, but keeping them separate matches the existing pattern and allows partial recovery.
  • Stuck reconciliation requires manual recovery. Claim-first means a partial failure leaves a row that needs human inspection (was the on-chain call applied or not?). The 409 response includes the recovery procedure.
  • No advisory lock for concurrent admins. Single-admin v1 is fine. If a scheduled cron is added later, wrap the flow in pg_advisory_xact_lock to prevent race conditions.
  • Numero is poker-only. The endpoint short-circuits with 501 for gameType: 'numero' because the dealer game has no rake (house IS the player). Arena will reuse this pattern when entry-fee rake is added.

Verified

Live end-to-end on Base Sepolia diamond 0x0d763955ca0eb531f71907d1f100c16899d3f42c:

  • Reconciliation c95908d9-23a4-4714-a54f-9e09f5c7c23a swept 1 unclaimed hand (2 NUMERO rake)
  • Chain hash: 0xb71d99d2f4050a4c3d7f655027fb4c810ab8218f675be8b3c7849bea51a8e8c5
  • Settle tx: 0x1f4fbcd9fbc39022d22974843582b2b9f8dbaa40c771aa4e8a8b9a41c6fafccb
  • Collect tx: 0x49f9aeda555c84b0a19df3394c8afe73363136ecd8aaa65ad8153e8a9e25739a
  • Rake collector wallet 0x09df...747d received 2.0 NUMERO

Full Documentation

See apps/docs/guide/rake-reconciliation.md for the operational reference.

Full ADR

The full ADR is retained in the project's internal planning archive.