Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
토큰 없이 무료 텍사스 홀덤을 HoodFi 이름으로 로빈후드 체인에서 플레이, 봇과 대결하고 순위 올리기
No tokens to hold. No buy-ins. No real value — just free Texas Hold’em played under your HoodFi name from Robinhood Chain. Grab a seat, play solo against bots, and run up the leaderboard.
게임TypeScript · JavaScriptGitHub @RWA-ID온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Welcome to HoodPoker | No tokens to hold | No buy-ins | No real value — just the fastest free poker table on Robinhood chain | Grab a seat | stack virtual chips | and run up the leaderboard.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Browser-based multiplayer Texas Hold'em on Robinhood Chain. Your
hoodfi.eth name is your handle at the table, hands only start with 4+
players, and the chips are free and stay free.
Built by @ensgianteth · Play at
hoodpoker.fun (Cloudflare) or epoker.eth.limo
(IPFS) · Backend: https://worker.hoodpoker.fun
Disclaimer — Hoodpoker is an independent community project. It is not
affiliated with, endorsed by, or connected to Robinhood Markets, Robinhood
Crypto, ENS or ENS Labs in any way. "Robinhood Chain" is referenced solely
to describe the network this app runs on.README 전문 보기 ↗Browser-based multiplayer Texas Hold'em on Robinhood Chain. Your
hoodfi.eth name is your handle at the table, hands only start with 4+
players, and the chips are free and stay free.
Built by @ensgianteth · Play at
hoodpoker.fun (Cloudflare) or epoker.eth.limo
(IPFS) · Backend: https://worker.hoodpoker.fun
Disclaimer — Hoodpoker is an independent community project. It is not affiliated with, endorsed by, or connected to Robinhood Markets, Robinhood Crypto, ENS or ENS Labs in any way. "Robinhood Chain" is referenced solely to describe the network this app runs on.
Play chips only. Chips are virtual, have no cash value, and cannot be purchased, sold, transferred, withdrawn or redeemed. There is no buy-in, no wagering of tokens and no payout of any kind. The only wallet interaction in the entire app is a gas-free signature proving you own your address — the game never sends a transaction.
4663). Your hoodfi.eth subname becomes your handle and its
avatar record becomes your table picture; mainnet ENS names work too, so
nobody has to mint anything to sit down.epoker-eth/
├── frontend/ Next.js 15 App Router · static export · Tailwind + shadcn-style UI
│ ├── app/ / (lobby) · /table/?id= · /leaderboard · /profile
│ ├── components/ felt, seats, cards, action bar, chat, avatars
│ ├── lib/ Reown AppKit + wagmi/viem, identity, WS client, sounds, auth
│ ├── public/_headers security headers for the Cloudflare deploy (IPFS ignores it)
│ ├── wrangler.toml Cloudflare static-assets deploy → hoodpoker.fun + www
│ └── pin.mjs pins out/ to IPFS via Pinata
└── worker/ Cloudflare Worker + Durable Objects + D1
├── src/index.ts router: /tables /leaderboard /profile /claim /whitelist + WS forwarding
├── src/whitelist.ts House Pass whitelist: 2,222 spots + 1,111 waitlist, one per handle + wallet
├── src/table.ts TableDO — ONE Durable Object per table, all game logic
├── src/session.ts SIWE nonces + verification, HMAC session tokens
├── src/auth.ts legacy static-signature check (OFF: ALLOW_LEGACY_SIG=0)
├── src/handle.ts on-chain check that a claimed hoodfi name is really yours
├── src/chat.ts chat sanitizer (links stripped; `.eth` names preserved)
├── src/poker/deck.ts crypto.getRandomValues Fisher–Yates shuffle
├── src/poker/evaluator.ts 7-card hand evaluator (packed-score, fully commented)
├── src/poker/pots.ts side-pot settlement from total commitments
├── schema.sql D1 schema (players, tables)
└── migrations/ one-shot D1 migrations — run before deploying
Why Durable Objects? One DO instance = one table = one WebSocket hub. DOs are single-threaded, so a table's state is strongly consistent with zero locking, and Cloudflare places each object near its players. Lobby reads hit D1, not the DOs, so listing tables never wakes sleeping games.
Trust model: clients send intents ("raise to 400") over WebSocket and receive per-player sanitized snapshots. Shuffling, dealing, bet validation, side pots, hand evaluation and payouts all run server-side.
This is the part worth reading before changing anything.
A hoodfi.eth subname lives on Robinhood Chain and is reachable from mainnet
over CCIP — getEnsAddress('gm.hoodfi.eth') resolves fine from any viem
client. But that is forward resolution. The obvious way to label a player
is the reverse direction, getEnsName(address), and that returns null
for every hoodfi holder: a mainnet primary name requires writing to the
mainnet ReverseRegistrar, which an L2 name cannot do without ENSIP-19 L2
reverse resolution.
So names are found by enumerating the registry instead:
getLogs for ERC721 Transfer where to == player, from block 0. to is
indexed, so this is a cheap filtered query — ~125ms for the whole chain
on the public RPC, not a wide scan. (The registry is a plain ERC721, not
Enumerable, and token ids are namehashes rather than a sequence, so
tokenOfOwnerByIndex does not exist.)owner(node) for each hit. A Transfer log proves the address
received the name once, not that it still holds it — in the first real scan
6 of 25 names had already moved on.names(node) and the avatar text record in the same pass.The worker independently re-verifies the claimed handle with a single
eth_call before anyone sits down under it (src/handle.ts), because the
sign-in signature proves the wallet, not the name.
getEnsAvatarMeasured against real hoodfi names that have avatar records:
| call | result | time |
|---|---|---|
getEnsText(name, 'avatar') | ipfs://Qm… | 1.8–5.0s |
getEnsAvatar(name) | null | 29.6s |
getEnsAvatar resolves the ipfs:// URI through a public gateway before
returning, and that gateway times out on pins it has no reason to hold — so
the names with avatars are exactly the ones that render blank, slowly. The
app reads the raw record and builds its own ordered gateway chain
(lib/avatar.ts): the dedicated gateway first, with resize parameters, then
ipfs.io, with the <img> onError advancing the index.
Prereqs: Node 20+, a Cloudflare account, npx wrangler login.
# backend
cd worker
npm install
npx wrangler d1 create epoker # once — paste database_id into wrangler.toml
npm run db:local # schema for local dev
npm run dev # http://localhost:8787
npm test # chat sanitizer tests
npm run test:session # SIWE + session tokens (15 checks)
npm run test:table # TableDO hand simulation (22 checks)
npm run test:whitelist # whitelist cap / no-repeat on real SQLite (17 checks)
# frontend (second terminal)
cd frontend
npm install
cp .env.example .env.local # fill in the vars below
npm run dev # http://localhost:3000
| Env var | Purpose |
|---|---|
NEXT_PUBLIC_REOWN_PROJECT_ID | Reown Cloud project ID (wallet modal) |
NEXT_PUBLIC_ALCHEMY_KEY | Alchemy mainnet key — mainnet ENS name lookups |
NEXT_PUBLIC_ROBINHOOD_RPC | Optional override for the chain-4663 RPC |
NEXT_PUBLIC_WORKER_URL | Worker origin — https://worker.hoodpoker.fun in production, http://localhost:8787 in dev. The build's CSP is derived from it |
PINATA_JWT | Only for node pin.mjs (IPFS deploy) — never bundled |
To test multiplayer, open four browser profiles with four wallets and join the same table — the hand starts automatically once four players are seated.
npm install)@reown/appkit-adapter-wagmi declares its wagmi peers as >= ranges, so a
plain npm install will happily pull a @wagmi/core major that the pinned
wagmi cannot use, producing duplicate-core type errors and missing modules.
The whole wallet stack is therefore pinned to exact versions in
package.json — wagmi, @wagmi/core, @wagmi/connectors, viem,
@reown/appkit and @reown/appkit-adapter-wagmi. Do not loosen them to
carets without re-verifying npm ls @wagmi/core reports a single deduped
version.
next.config.mjs also carries two related workarounds: externals.push ('pino-pretty') and resolve.alias.accounts = false.
The same static build is served from two places:
| Where | How | Release step |
|---|---|---|
| hoodpoker.fun + www | Cloudflare Workers static assets, worker hoodpoker-site (frontend/wrangler.toml) | npx wrangler deploy — live at once |
| epoker.eth | IPFS via Pinata | node pin.mjs, then set the contenthash by hand |
The game backend (worker/) answers on worker.hoodpoker.fun and on its
original epoker-worker.dmpay.workers.dev. The workers.dev URL must stay on
(workers_dev = true): every epoker.eth pin built before the move calls it.
A domain move adds a hostname; it never retires one.
On eth.limo the gateway sends the framing and security headers. On Cloudflare
nobody does unless we do, so frontend/public/_headers sets them
(frame-ancestors 'self', X-Frame-Options, HSTS, nosniff,
Referrer-Policy) plus immutable caching for hashed /_next/static chunks.
In frontend/wrangler.toml, routes must sit above [assets]. Below it,
TOML reads it as assets.routes, wrangler only warns "Unexpected fields", and
the custom domains are silently not attached.
A new frontend origin has to be added in three places, or sign-in breaks on
it: ALLOWED_ORIGINS in worker/wrangler.toml, Privy's Allowed domains, and
Reown's domain allowlist (domains only, never a path). The CSP needs nothing,
since it allows 'self'.
Order matters. The worker reads handle/avatar, which the live D1
database does not have until migrated, and the frontend's protocol must not
run ahead of the worker.
# 1. migrate D1 (once)
cd worker
npx wrangler d1 execute epoker --command "PRAGMA table_info(players);" --remote # check first
npx wrangler d1 execute epoker --file=migrations/001_handle_avatar.sql --remote
# 2. deploy the worker
npx wrangler deploy
# 3. build the frontend once, ship it to both homes
cd ../frontend
npm run build # static export → out/ (~12–15 min on a slow machine)
npx wrangler deploy # → hoodpoker.fun, live immediately
node pin.mjs # pins out/ to Pinata, prints the CID for epoker.eth
Migrations so far, each run once with --remote before the worker that needs
it: 001_handle_avatar.sql, 002_auth_nonces.sql, 003_whitelist.sql.
Pin out/ to IPFS and copy the CID (pin.mjs prints it).
In the ENS app, set the Content Hash record
of epoker.eth to ipfs://<CID>.
Confirm the new pin is actually what's being served before you rely on it:
curl -s https://epoker.eth.limo/ | grep -c "$(cat frontend/.next/BUILD_ID)"
1 means the contenthash propagated. A 200 alone proves nothing — a
gateway will keep serving the previous pin for a while, so fetch the old
CID's index.html as a control and check the two differ.
The site resolves at https://epoker.eth.limo. Repeat pin + contenthash
update on each release.
A fresh pin often 504s on _next/static/chunks/*.js until the CID propagates,
which surfaces as a ChunkLoadError and a blank page. Warm the files, or wait,
before concluding the build is broken.
Game logic is fully server-authoritative: clients send intents, the Durable Object shuffles, deals, validates every bet and pays out.
Sign-in is Sign-In with Ethereum (EIP-4361). The worker issues a one-time
nonce (D1 auth_nonces, 10 min TTL, deleted on use); the wallet signs a
message naming the site's domain, Robinhood Chain (4663) and an expiry; the
worker checks domain, origin, chain, expiry, signature and nonce, then returns
a 24h HMAC session token (SESSION_SECRET). API calls send it as
Authorization: Bearer, sockets as ?token=. The router derives the player's
address from the token and passes only that to the table. Rotating
SESSION_SECRET signs everyone out.
SIWE is now the only way in: ALLOW_LEGACY_SIG is "0" and the old
replayable static signature is refused. The flag exists for the changeover —
a worker that issues nonces can be deployed while the pinned frontend is still
the pre-SIWE build, because "1" keeps accepting that build's signature. Set
it back to "1" only if you ever have to serve an old pin again, and turn it
off the moment the contenthash points at a SIWE build.
Origins. CORS, the SIWE domain check and WebSocket upgrades all use the
ALLOWED_ORIGINS allowlist (wrangler.toml). Upgrades from any other origin
are refused (cross-site WebSocket hijacking).
Identity. Handles are verified server-side — hoodfi.eth names against the Robinhood Chain registry, mainnet names by forward resolution — and the avatar is read from that verified name's record, never taken from the client.
Abuse limits.
wrangler tail.NEXT_PUBLIC_* values (Alchemy key, Reown ID, Privy app ID) are public by
design — scope them to the frontend domains in their dashboards.
The order matters, and it is the reverse of what feels natural: the worker
goes first. A SIWE frontend calls /auth/nonce, so pinning it before the
worker exists means nobody can sign in. The worker, meanwhile, is safe to
deploy early precisely because ALLOW_LEGACY_SIG="1" still honours the old
pin's signature.
cd worker
npx wrangler d1 execute epoker --file=migrations/002_auth_nonces.sql --remote
openssl rand -base64 48 | npx wrangler secret put SESSION_SECRET # never echo it
npx wrangler deploy # legacy signatures still accepted
cd ../frontend && npm run build && node pin.mjs # → CID
# set the epoker.eth contenthash to that CID, then CONFIRM it is live:
# curl -s https://epoker.eth.limo/ | grep -c "$(cat .next/BUILD_ID)"
# a 200 alone proves nothing — gateways happily serve the previous pin.
# only once that greps 1:
# set ALLOW_LEGACY_SIG = "0" in wrangler.toml, then:
cd ../worker && npx wrangler deploy
Flipping the flag before the new pin is really being served locks out every browser still on the old one. Tokens already issued are unaffected either way — they are HMAC-verified and never touch the legacy path — so only new sign-ins are at risk during the window.
A membership NFT: 7,777 passes in seven tiers. The allowlist stages are free
(gas only); the public stage costs 0.001777 ETH, which pays for development,
infrastructure and maintenance. Minting on OpenSea from October 15, 2026, 5:00pm AST. Contract:
"HoodPoker House Pass" (HPASS), ERC721 on Robinhood Chain, deployed through
OpenSea Studio, 7.77% creator earnings, enforced. This repo holds the sign-up
side, the snapshot script, the metadata script, the landing-page section
(/#pass) and the perks page (/pass/).
Snapshots. The CCFF00 cut was taken early, on 2026-10-02 at block
78244737, and is published with the site as frontend/public/pass/ccff00-snapshot.json
(4,343 holders, the 3,333 longest picked) so the address checker on /#pass
and /pass/ can answer without a wallet. Whitelist and HoodFi names snapshot
on October 14, 2026, when sign-ups close (WHITELIST_OPEN = "0"). That run
must reuse the published CCFF00 list instead of re-ranking:
cd worker
node scripts/house-pass-snapshot.mjs --write --ccff00-from=../frontend/public/pass/ccff00-snapshot.json
A picked holder who sells after October 2 keeps their spot; a wallet that buys
CCFF00 after it isn't added. The allowlist CSVs get uploaded to OpenSea the same day,
October 14, before the mint. hoodfi.csv carries a mint limit of 2; the others 1.
Stages run back to back, times AST (UTC-4):
| Stage | Opens | Runs |
|---|---|---|
| 0. Team | Oct 15, 5:00pm | 30 min |
| 1. CCFF00 | Oct 15, 5:30pm | 3 h |
| 2. Whitelist | Oct 15, 8:30pm | 2 h |
| 3. HoodFi names | Oct 15, 10:30pm | 12 h |
| 4. Public | Oct 16, 10:30am | 77 d 7 h 7 min, to Jan 1 2027, 5:37pm |
| Stage | Who | Cap |
|---|---|---|
| 0. Team | One wallet (the founder's), up to 100, minted first for giveaways after the mint. Its allowlist is that single address | 100 |
| 1. CCFF00 | One per holder, however many they hold. 4,343 holders at block 78244737, so the 3,333 longest holders. Free | 3,333 |
| 2. Whitelist | One per person: repost the pinned post on X, then sign up on the site with your X handle + wallet (reposts checked at the snapshot). CCFF00 holders may join; those picked for stage 1 are dropped at the snapshot and the waitlist backfills | 2,222 |
| 3. HoodFi names | Two per owner of a name on the HoodFi registry 0xf2bABA012244bdD7445129597350054E1B3aEe5C. Last way in before public, including for CCFF00 holders who missed the cut | 1,111 |
| 4. Public | One per wallet, 0.001777 ETH | 1,111 |
The team's 100 come out of the 7,777, so the public stage gets whatever is left after stages 0–3. Allowlist stages rarely fill completely; in the 2026-10-01 dry run HoodFi names had 80 owners against 1,111 places.
OpenSea Studio has no per-stage supply cap, only a per-wallet limit, and those limits add up across stages. A stage's cap is therefore the length of its allowlist. Whatever a stage doesn't mint stays available to the stages after it.
CCFF00 is 0x505A22Ffed8d37ebE580FfD98d2Cdb0021189146 on Robinhood Chain.
frontend/lib/house-pass.json is the one source for tiers. The site renders it,
and worker/scripts/house-pass-metadata.mjs deals the collection from it (and
refuses to run if the supplies don't add up to 7,777).
| Tier | Supply | Chips every 12 h | One-time chip stack |
|---|---|---|---|
| Crown Jewel | 77 | 100,000 | 1,000,000 |
| The River | 111 | 60,000 | 500,000 |
| The Nuts | 333 | 40,000 | 250,000 |
| The Button | 777 | 30,000 | 150,000 |
| Royal Flush | 999 | 25,000 | 100,000 |
| All In | 2,222 | 20,000 | 50,000 |
| Pocket Rockets | 3,258 | 15,000 | 25,000 |
Every pass also gets the shared member perks (MEMBER_PERKS in
frontend/lib/housePass.ts). Perks follow the pass so a pass bought on the
secondary market is worth having (Hector, 2026-10-10):
cd worker
node scripts/house-pass-metadata.mjs --seed=<block hash> --image-base=ipfs://<art CID>/
Run it after the mint closes, seeded with the hash of a block announced in
advance by height. Dealt earlier, anyone holding the file could time a mint onto
a Crown Jewel id; until the reveal the collection sits behind OpenSea's
pre-reveal art. It writes metadata-out/<seed>/ (gitignored): one JSON per
token, a metadata.csv for Studio's bulk upload (check its header against
OpenSea's current template first) and assignment.json, the token → tier map
the worker will read.
worker/src/bonus.ts)For hoodfi.eth name owners, pass or no pass. Ledger = D1 bonus_claims
(migration 006); npm run test:bonus (21 checks on real SQLite).
POST /bonus/hoodfi-weekly). Weeks open Friday
00:00 AST; claim any time before the next. Direct names only
(label.hoodfi.eth; subnames are free to mint). Paid once per wallet and
once per name per week, so one name passed between wallets pays once.POST /bonus/widget) for registering a
name through the HoodPoker widget: the worker finds a PartnerRegistration
log on the router 0xB7Bd…4b5d with our partner 0x5f11…165b and this wallet
as buyer. Every sale since the router deployed counts.The pass perks are copy, not behaviour yet: the worker still grants every player the same daily chips and reads nothing about passes. They go into the worker once the contract address and the reveal exist.
2,222 spots plus a 1,111 waitlist: sign-ups stop at 3,333.
| Route | Auth | Returns |
|---|---|---|
GET /whitelist?address=0x… | none | { count, cap, waitlist, open, joined?, position? }, the live counter |
POST /whitelist | SIWE session | { ok: true, already, position, holdsCcff00 }, or { ok: false, reason } |
Refusals are answers the page shows, not errors: closed (403) and full
(409, all 3,333 places taken).
holdsCcff00 only changes
what the page tells a holder. It never decides whether they get in, so a
throttled RPC costs nothing but the hint.(created_at, address) in the worker and in the snapshot, so
the position shown at sign-up is the position the snapshot uses.whitelist_burst_20261004). Now: one per handle
(unique index on whitelist.x_handle) and one per wallet (primary key),
a Cloudflare Turnstile check that fails closed (TURNSTILE_SECRET), and a
per-IP limit. Reposts can't be read without X's paid API, so the snapshot
checks them: --x-reposters=<file> keeps only handles that reposted.whitelist_meta, kept exact by insert/delete
triggers (migration 004). It used to be COUNT(*) on every 15s poll from
every open tab, which ran out D1's free daily reads.INSERT OR IGNORE … SELECT … WHERE (SELECT v FROM whitelist_meta …) < 3333,
so two wallets racing for the last place can't both land. A count followed by
a separate insert could. The test suite races exactly that, against real
SQLite (node:sqlite), and fails if the in-statement limit is removed.WHITELIST_OPEN = "0" in worker/wrangler.toml
and deploy. The counter stays readable, and anyone already signed up still
sees their position.This table is the sign-up record, not the allowlist. The snapshot builds the allowlist from it.
# read the list in snapshot order
npx wrangler d1 execute epoker --remote --command \
"SELECT address, created_at FROM whitelist ORDER BY created_at, address"
cd worker
node scripts/house-pass-snapshot.mjs # report only
node scripts/house-pass-snapshot.mjs --write --rule=longest # → snapshot-out/<time>/
The script replays every CCFF00 and HoodFi registry Transfer since genesis,
then checks every token against ownerOf and refuses to write on any
unexplained disagreement. It reads the whitelist from D1 (dropping wallets picked for CCFF00, first 2,222
in sign-up order) and writes one CSV per
stage in OpenSea's template (Wallet address,Custom mint limit,Custom price,
every row 1,0), plus report.json with the full CCFF00 ranking so the cut
can be published. --rule is longest (held longest), largest (most held)
or random with --seed=<announced block hash>. Output is gitignored.
Robinhood Chain RPC traps the script handles: getLogs accepts about 5M
blocks per call, busy ranges can time out (split them), throttling returns 429
(wait; splitting only adds requests), and the node keeps no old state, so
ownerOf at the snapshot block fails within minutes. Verify at latest instead,
and explain any difference with transfers after the snapshot block.
SESSION_SECRET to revoke all).localStorage), so playing
from a second device defaults back to your shortest name.Built by @ensgianteth · github.com/RWA-ID/epoker-ens
Not affiliated with Robinhood Markets, Robinhood Crypto, ENS or ENS Labs.