hoodsup.fun
How it works

One transaction per hand. Every card checkable.

hoodsup.fun is No-Limit Texas Hold'em. The money sits in a contract. 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. A disconnected player is treated the same way. 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.

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.fun and are always open. 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 HoodsUp contract holds every stack. 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.

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.

Nothing here is hidden. The contract is deployed on Robinhood Chain with its source verified, so 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.

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 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.fun 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.fun tables for up to 12 hours. It cannot move funds.

URI: https://hoodsup.fun
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.fun 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.

Blocks come every 100 milliseconds, so the commit lands before the first card is shown and the settlement lands 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.