Appearance
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
approvetx. - Seat is reserved immediately so no one else can grab it during the 3-15s on-chain wait.
- Player lands as
sitting_out, notseated. 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). - Utility —
apps/game-server/src/shared/permit-signer.tsexportssignPermit,getPermitNonce,signPermitFromChainfor 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 viasitDown)apps/game-server/src/shared/room-handler.ts—handleSitDown,standUpPlayer,handleTopUp, seat reservationsapps/game-server/src/shared/permit-signer.ts— server-side EIP-712 signing utilityapps/admin-server/src/routes/settlement.ts—/settlement/depositendpoint (callsregisterAndDeposit)
UI / Client
packages/web3/src/hooks.ts—usePermitSignwagmi hookapps/poker/hooks/usePokerSocket.ts—sitDown(buyIn, seatIndex, permit),topUp(amount, permit),standUp()apps/poker/app/table/[roomCode]/page.tsx— sit-down callssignPermitthen 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.