Skip to content

Rake & Fees

Status: shipped 2026-07-03 (four implementation phases + a final hardening pass that fixed five collection-liveness gaps — see below). The operator-facing guide is at Rake & Fees.

What was decided

The house is just another account at the ledger level. Rake and tournament fees accrue to the existing player_id='house' ledger (cash: house rows written at every hand boundary; tournaments: the fee implied by the split), and money leaves a table vault as house revenue only through a program-verified collect_rake on numero-core:

  • the cage signs an ed25519 receipt over (table_id ‖ amount ‖ rake_chain_head ‖ destination ‖ deadline) — the settle pattern, destination bound in the message;
  • a rake_marker PDA seeded by the chain head makes each collection once-only on-chain (init fails on replay; the cage reconciles, never re-pays);
  • every collection is a claim-first rake_reconciliations chain link (EVM-format keccak chain binding hand hashes), so rake is re-derivable from hand history end-to-end.

The shared Phase 1 fix made the fee split real on both chains: prize pool = (entryFee − rakeFee) × seats via tournament-pool.ts (single source of truth, dust-to-first so payouts + fee == entries exactly, E-RAKE-1). This closed the live "$20+$2 pays $44" bug — the house's cut had been paid to winners since SnG shipped.

Key tradeoffs accepted

  • Program enforcement over a pure ledger debit. The original sketch kept collections off-chain ("just debit the rake account through the cage; only the hash chain goes on-chain"). Upgraded mid-design to EVM-style program enforcement, and Solana ended up stronger than EVM: destination-bound receipts + on-chain once-only markers vs EVM's accrue-then-drain counter.
  • Per-table collections (Solana) vs game-level accrual (EVM). Each Solana table has its own vault, so collections are per-table; rake_reconciliations gained table_id + kind columns while the hash chain stays globally ordered.
  • Retired the accrued-rake counter in settle. accumulated_rake is a dead layout field; the receipt format is unchanged.
  • Fee conservation measures unconfirmed payouts, not registrant equity (a hardening-pass correction): sng table_players CTN equity is distributed by the payouts — counting it double-counts and refused every production fee. Cash keeps the table_players gate, where chip_balance genuinely is the live claim.

The hardening pass

The earlier phases proved no-steal invariants; a follow-up review found the pipeline fund-safe but liveness-dead — five fixes, all regression-locked: the admin Collect button's request shape was 400ing; the fee-at-close was refused on every production tournament (gate double-count); one transient failure wedged the global collection chain forever (the sweep now resumes stuck claims first — the only retrigger a stuck tournament fee has); claim creation was raceable (per-table pg_advisory_xact_lock + guarded row-marking); and vault-less EVM tables sharing the environment made the E-RAKE-4 alarm permanently red (no_solana_vault, WARN). game-server now settles registrant rows as a tournament concludes (gone-at-settle).

Invariants

E-RAKE-1 (exact split) · E-RAKE-2 (collections bounded by the live player claim + the vault) · E-RAKE-3 (claim exactly once) · E-RAKE-4 (no table concludes with stranded house value — the conservation sweep auto-collects, closed tables included, and alarms on anything uncollectable).

Operational preconditions

HOUSE_DESTINATION_SOLANA env var must be set per environment (collections fail loud 503 until it is; its TSKPK ATA must exist) — moved from a game-setting 2026-07-03: a per-deploy fact belongs in the deploy's env, matching EVM's RAKE_COLLECTOR_ADDRESS. The sweep and reconcilers arm together via DEPOSIT_RECONCILE_INTERVAL_MS.