Skip to content

Sit=Deposit, Stand=Withdraw

Date: 2026-04-16 Status: Implemented — see Bulletproof State for the subsequent intent-outcome + invariant-enforcement layer

Decision

Sitting down deposits CTN from the player's wallet to the table pool via ERC-2612 permit. Standing up settles + withdraws CTN from the table pool back to the wallet. No floating "table balance." You're either in your wallet or in a seat.

The State Machine

Three active states: rail, seated, sitting_out. The old at_table state (chips without a seat) is removed.

Sit-Down Flow

Key points:

  • Permit is off-chain + free. User signs one EIP-712 message in their wallet; no gas, no separate approve tx.
  • Seat is reserved immediately so no one else can grab it during the 3-15s on-chain wait.
  • Player lands as sitting_out, not seated. The game never waits for a deposit — the player joins via the normal "wait for BB" flow.
  • On failure: seat reservation released, player stays on rail, wallet balance unchanged.

Stand-Up Flow

Key points:

  • Synchronous sit-out, async on-chain finalization. The user sees sitting_out instantly. The 3-15s settlement runs while they wait; the UI shows a spinner.
  • Settlement failure leaves the player sitting_out with chips intact. They can retry by sending stand_up again.
  • No separate withdraw action. { type: 'withdraw' } WS message is removed. Standing up IS cashing out.

Top-Up While Seated

Players can add chips without leaving their seat:

WS: { type: 'top_up', amount, permit }

Same permit flow as sit-down. Amount capped at maxBuyIn - currentStack. Chips added to available (not in-play until next hand).

Agent Permit Signing

Agents don't have browser wallets. Three cases:

  • ClawBot agents — the agent runner holds the agents' private keys and signs permits server-side using viem's signTypedData.
  • Hosted agents — owned by users via ownerUserId. The owner signs the permit in their UI (hosted agents reuse the owner's wallet).
  • Utilityapps/game-server/src/shared/permit-signer.ts exports signPermit, getPermitNonce, signPermitFromChain for any server-side caller.

The game-server itself holds no private keys. Agent REST endpoints (/register, /sit) accept a permit in the request body and forward it to admin-server.

Tradeoffs Accepted

  • 3-15s latency on sit-down and stand-up (on-chain tx time on Base). UI shows progress indicators; user experience is "click, wait while sitting out, seated."
  • Permit nonce serialization — rapid sit-downs + top-ups must wait for each tx to confirm before signing the next (nonce increments on-chain). UI disables the sit/top-up button during pending deposits.
  • No browsing with chips — can't have chips without being in a seat. Dashboards that showed "my tables with unallocated balance" are obsolete.

Key Files

Server

  • apps/game-server/src/shared/participant-manager.ts — PM state machine (rail → sitting_out via sitDown)
  • apps/game-server/src/shared/room-handler.tshandleSitDown, standUpPlayer, handleTopUp, seat reservations
  • apps/game-server/src/shared/permit-signer.ts — server-side EIP-712 signing utility
  • apps/admin-server/src/routes/settlement.ts/settlement/deposit endpoint (calls registerAndDeposit)

UI / Client

  • packages/web3/src/hooks.tsusePermitSign wagmi hook
  • apps/poker/hooks/usePokerSocket.tssitDown(buyIn, seatIndex, permit), topUp(amount, permit), standUp()
  • apps/poker/app/table/[roomCode]/page.tsx — sit-down calls signPermit then sends WS message

Tests

  • scripts/test-harness/casino/sit-deposit.ts — end-to-end faucet → sit + permit → verify on-chain → stand → verify on-chain

Full ADR

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