Appearance
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_playersrow withplayer_id = 'house'andnet_chips = rake. Sum of allnet_chipsper 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 computeschain = 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 thewithdrawForPokerpattern — Engine's backend wallet (the game signer) calls it. The originalcollectRake()is kept for direct rake-collector calls. - Audit chain across reconciliations. Each
rake_reconciliationsrow storesprevReconciliationHash, creating an unbroken history of every collection the casino has ever made.
Key Changes
packages/contracts/contracts/facets/WithdrawalFacet.sol— addedsettleHouseRakeandcollectRakeForpackages/db/src/schema.ts— addedrakeReconciliationstable andreconciliationIdcolumn onpokerHandPlayersapps/admin-server/src/routes/settlement.ts—/unreconciled-rake,/reconciliation-history,/reconcile-rakeendpointsapps/admin/app/rake/page.tsx— rebuilt rake page with three summary cards, dry-run + reconcile buttons, history tableapps/game-server/src/parties/poker/game-flow.ts— removed in-memory winner-based rake attribution. Player settlement receipts now always sendcumulativeRake = 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[].rakein the OHH JSON, displayed on the player history page - Accounting closure: the house row in
hand_playersmakes the sum ofnet_chipsper 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
settleAndCollectfunction, 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_lockto 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-9e09f5c7c23aswept 1 unclaimed hand (2 NUMERO rake) - Chain hash:
0xb71d99d2f4050a4c3d7f655027fb4c810ab8218f675be8b3c7849bea51a8e8c5 - Settle tx:
0x1f4fbcd9fbc39022d22974843582b2b9f8dbaa40c771aa4e8a8b9a41c6fafccb - Collect tx:
0x49f9aeda555c84b0a19df3394c8afe73363136ecd8aaa65ad8153e8a9e25739a - Rake collector wallet
0x09df...747dreceived 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.