hoodsup.poker
$HOODSUP is liveCA0xDbcc1ec04EBA22193410eA641024cF5A4ba86D5aExplorerToken page
How it works

One transaction per hand. Every card checkable.

hoodsup.poker is self-custodial No-Limit Texas Hold'em. Your money sits in a contract that only your wallet can withdraw from; we never hold it. The shuffle is committed before the deal and revealed after.

The rules

No-Limit Texas Hold'em. Two hole cards each, five community cards, best five-card hand wins.

A hand starts when at least two seated players are live, not sitting out, not leaving, and have a stack of at least one big blind. The button moves clockwise. Blinds are posted. Cards are dealt.

You have thirty seconds to act. If the clock runs out you check when checking is legal and fold otherwise. If your connection drops while it is your turn, you get one extra thirty seconds in that hand to come back before that happens. Sit through a whole hand without acting and the dealer sits you out until you press “I'm back”. Sitting out skips upcoming hands without giving up your seat or your stack.

Showdown uses full seven-card evaluation. Side pots are split correctly. Odd chips go to the first seat left of the button.

Two options from the table page. Straddle: with three or more players, the seat left of the big blind can post a live straddle of twice the big blind before the cards are dealt; action then starts left of the straddle and the straddler gets the last word preflop. Run it twice: when everyone still in the hand is all-in before the river and every one of them has run-it-twice on, the rest of the board is dealt twice and each pot is split half per board. Both are off until you turn them on, and both are dealt from the same committed deck, so both boards can be checked.

Rake is 2.5% of the pot, uncapped, and only taken when a flop was seen. A hand that ends preflop is not raked. The contract cannot see pot sizes, so it enforces a bound instead: rake can never exceed 2.5% of the stacks that were in play in the hand, which an honest 2.5%-of-pot rake always satisfies.

Tables and tokens

Every table is denominated in one token: ETH, USDG, or HOODSUP. Blinds, buy-ins, stacks and rake are all in that token, and the site shows a live US dollar figure next to each amount where a price is known. Each table has a small blind, a big blind, a minimum and maximum buy-in, and two to nine seats.

House tables are run by hoodsup.poker. Player tables are opened by anyone: pick one of those tokens (you must hold it), set the blinds, the buy-in range and the number of seats, and pay one transaction to create it. Player tables take the same 2.5% rake. You can have one open player table at a time; close it to open another.

A player table closes when its creator has been away for ninety seconds or it has sat empty for ten minutes, or when the creator closes it from the table page between hands. A closed table accepts no new players; anyone still seated keeps their stack and can leave whenever they like.

The dealer pays the gas to commit and settle every hand. If its ETH balance runs low, no new hands are dealt until it is topped up, and the lobby says so; hands already in progress still settle.

The shuffle

Before anything is dealt the dealer draws a random 32-byte seed and publishes its hash on chain. That hash is the deck commit. The seed itself stays secret until the hand settles, then it is revealed on chain. The contract refuses a seed that does not hash to the commit. The dealer therefore knows the cards as soon as a hand opens, which is why the dealer is not a player; what it cannot do is change a single card after the commit is on chain. A commit can only be used once, so a seed revealed at settlement can never describe a later hand.

The deck is a plain Fisher-Yates shuffle driven by keccak256. This is the contract's own function:

function deckFromSeed(bytes32 seed) public pure returns (uint8[52] memory deck) {
    for (uint8 i = 0; i < 52; i++) {
        deck[i] = i;
    }
    for (uint256 i = 51; i > 0; i--) {
        uint256 j = uint256(keccak256(abi.encodePacked(seed, uint8(i)))) % (i + 1);
        (deck[i], deck[j]) = (deck[j], deck[i]);
    }
}

function commitFor(bytes32 seed) external pure returns (bytes32) {
    return keccak256(abi.encodePacked(seed));
}

And the identical JavaScript that this site runs in your browser when you press verify:

import { keccak256, encodePacked } from "viem";

export function deckFromSeed(seed) {
  const deck = Array.from({ length: 52 }, (_, i) => i);
  for (let i = 51; i > 0; i--) {
    const h = keccak256(encodePacked(["bytes32", "uint8"], [seed, i]));
    const j = Number(BigInt(h) % BigInt(i + 1));
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  return deck;
}

Card encoding

Cards are numbered 0 to 51. id = rank × 4 + suit. Rank 0 is a two and rank 12 is an ace. Suit 0 is clubs, 1 is diamonds, 2 is hearts, 3 is spades. So 0 is 2♣, 51 is A♠, and Td is 8 × 4 + 1 = 33.

Deal order

Deck index 0 is the first card dealt. Hole cards go round-robin starting with the seat left of the button, in ascending seat order and wrapping, for two rounds. Then a burn card, three flop cards, a burn, the turn, a burn, the river.

  • With n players, seat k in deal order gets deck[k] and deck[k + n].
  • The flop is deck[2n + 1], deck[2n + 2], deck[2n + 3].
  • The turn is deck[2n + 5]. The river is deck[2n + 7].

Given the seed, the set of players (the playerMask in the HandOpened event) and the button, every card in the hand is determined. The hands page does this for you.

The contract

The HoodPoker contract holds every stack, and that is the whole custody story: there is no hoodsup.poker account, no balance with us, and no withdrawal queue. It has an owner who creates house tables, and a dealer address allowed to open and settle hands. It cannot move your money except through settlement, and settlement can only move money between the seats that were dealt into that hand. Buying in and cashing out are your own transactions, signed by your wallet, and nothing the owner or the dealer can do stops leave from paying you.

Be clear about what that means. Within a hand, the dealer chooses the result and the contract only checks that it balances. A dishonest dealer could hand a pot to an accomplice at the same table, and the contract would not stop it; what it does is make it visible, since every settlement is a public event with the seed, every seat's delta and the hash of the signed action log, and every deck can be rebuilt from the seed. Money that is not in a hand is never touched, a seat that has asked to leave is never dealt again, and the rake that can reach the house is capped at 2.5% of the money in play. This is the trust you place in the dealer, and it is the same trust you place in any poker room, made checkable.

Verified on chain

Nothing here is hidden. The contract is deployed on Robinhood Chain and its source code is verified on the block explorer, which means the explorer has compiled the published source and confirmed it matches the bytecode running at the address byte for byte. You can read the verified source on the explorer, check every function listed below against it, and watch every buy-in, settlement and cash-out land as a transaction.

  • The contract. The address is 0x8ca805c694c2f69c42641e6eb0e11fd5ae43cdea. If the explorer shows a green verified tick and this source, you are looking at the real thing. Anything else is not hoodsup.poker.
  • Your money. Search your own wallet on the explorer: every buy-in, top-up and cash-out is a transaction you signed, to and from that one contract, and the contract's balance is the sum of everyone's stacks plus unclaimed rake.
  • Every hand. Each settlement is an event on the same contract carrying the revealed seed and each seat's result. The hands page reads those events straight from the chain, re-derives the deck in your browser, and flags any hand where the dealer's cards and the chain's disagree.

Player functions

  • join(tableId, amount) — takes the first free seat. ERC-20 tables need one approval first; ETH tables take the buy-in as the transaction's value.
  • topUp(tableId, amount) — adds chips between hands at an open table, up to the table maximum.
  • leave(tableId) — pays your stack out and frees the seat. If you are in an open hand it flags you as leaving instead; call it again after the hand settles.
  • createCustomTable(token, smallBlind, bigBlind, minBuyIn, maxBuyIn, seats) — opens a player table on ETH or an allowed token you hold (USDG or HOODSUP). One open table per creator.
  • stay(tableId) — withdraws a leave request if you change your mind before the hand settles.
  • closeTable(tableId) — stops new players joining and new hands starting, between hands only. The owner can close any table; a player table can also be closed by its creator or by the dealer.
  • forceCancelStaleHand(tableId) — anyone can call this once a hand has been open for more than an hour. The hand is voided with no stack changes and every seat can leave. The table page offers this as a button when it applies.
  • claim(token) — collects a stack the contract owes you after your seat was vacated (see kick below). The lobby and table pages show a claim button whenever there is something to collect.

Owner functions

The owner creates house tables, decides which tokens tables may use (ETH is always allowed), sets the dealer and treasury addresses, withdraws accrued rake to the treasury, and can pause the contract. A pause blocks buy-ins, top-ups, new player tables and new hands; it never blocks cashing out or settling a hand already in play.

Dealer functions

  • kick(tableId, seat) — vacates a seat, between hands only, whose occupant has asked to leave or has no chips, so nobody can hold a seat against the table forever. The stack is not sent anywhere; it is credited to the player and collected with claim. The dealer waits ten minutes after a leave request, or five with an empty stack. The owner can call it too.
  • openHand(tableId, deckCommit, playerMask) — commits the shuffle and names the seats in the hand. Fails if a seat is empty or leaving. Used for the first hand at a table.
  • settleAndOpen(tableId, handId, deckSeed, deltas, rake, logHash, nextCommit, nextMask) — reveals the seed, applies one delta per seat, and commits the next hand's shuffle in the same transaction. This is the normal per-hand call.
  • settleHand(...) — the same settlement without opening a next hand, used when nobody is ready to play on.
  • cancelHand(tableId, handId) — voids a hand with no stack changes.

What every settlement checks

  • keccak256(deckSeed) == deckCommit.
  • Every seat outside the player mask has a delta of zero.
  • The table is open and the contract is not paused (for the next hand opened in the same call).
  • No stack goes below zero.
  • sum(deltas) + rake == 0. Chips cannot be created.
  • rake × 10000 <= rakeBps × stacksInPlay, plus rake <= rakeCap when a table sets one.

The deployed contract is 0x8ca805c694c2f69c42641e6eb0e11fd5ae43cdea on chain 4663.

Session keys

Poker needs fast actions and a wallet popup per action would be unplayable. So your wallet signs one message that authorises a fresh random key for up to twelve hours. That key stays in your browser until it expires and signs each action. It cannot move funds; the contract has never heard of it. You can revoke it from the table page at any time, and a new one is one signature away.

The authorisation is a standard Sign-In with Ethereum message (EIP-4361), so your wallet shows a proper sign-in prompt with the site's domain rather than a raw text warning. It reads:

hoodsup.poker wants you to sign in with your Ethereum account:
<checksummed wallet address>

Authorize session key <checksummed session address> to act for this wallet at hoodsup.poker tables for up to 12 hours. It cannot move funds.

URI: https://hoodsup.poker
Version: 1
Chain ID: 4663
Nonce: <random>
Issued At: <timestamp>
Expiration Time: <timestamp>
Resources:
- hoodsup:session:<checksummed session address>

The dealer accepts it only for its own domain and chain, for at most twelve hours from the time it was issued.

Every action the session key signs is exactly:

hoodsup.poker action
table: <tableId>
hand: <handId>
seq: <seq>
action: <fold|check|call|bet|raise|allin>
amount: <amount in token base units, "0" if none>

seq is the dealer's action counter for the hand, sent to you with each turn, so a signature can never be replayed. The dealer keeps the signed log for every hand and commits its hash as logHash at settlement.

Settlement

The first hand at a table starts with openHand, which emits HandOpened(tableId, handId, deckCommit, playerMask). Every hand after that ends and the next begins with a single settleAndOpen transaction, which emits HandSettled(tableId, handId, deckSeed, deltas, rake, logHash) for the finished hand and HandOpened for the next one.

The commit is mined before the first card is shown, and the settlement is mined before the next hand is dealt. Winnings stay in your stack at the table. You cash out whenever you like with leave.

To check a hand: take the seed from HandSettled, hash it, compare with the commit from HandOpened, then run deckFromSeed and deal it out. If the board or any shown cards differ from what you saw, the dealer cheated and the proof is on chain. The hands page and the verify button on every settled hand automate this.

Characters and profiles

The first time a wallet connects you pick a username and dress a hooded character. That is one wallet signature and no transaction. Usernames are unique, and opponents see your character at the table, in the lobby and in the hand record. Change it any time from the wallet menu.

Every wallet has a public profile: results per token (net after rake, hands, win rate, biggest win and pot, rake paid), how you play (VPIP, PFR, all-ins, showdown rate and showdown win rate, streaks), your best showdown hand, and your recent hands with boards, your own hole cards and the settlement transaction. The numbers are derived from the signed action logs at settlement, so they are exactly what settled on chain. The players page ranks everyone by net per token.