Free, browser-based online 8-ball pool with on-chain (ENS) identity. Hosted
fully static on IPFS via billiard.eth / billiard.eth.link. The only always-on
service is a Cloudflare Worker that relays signed challenges and runs the
authoritative game simulation. No game smart contract — challenges and
results are wallet-signed messages. The only on-chain action is optional ENS
name registration.
┌─ Frontend (Next.js static export → IPFS) ────────────────┐
│ app/ landing+lobby, /play (canvas), /stats │
│ lib/wallet wagmi config — Reown AppKit (WC QR + more) │
│ lib/ens pinned ENS ABIs, resolve, commit/reveal │
│ lib/game deterministic physics + 8-ball rules │
│ lib/net WS client + shared protocol shapes │
│ lib/crypto challenge/result signing (viem) │
└───────────────────────────────────────────────────────────┘
│ WebSocket (wss)
┌─ Worker (Cloudflare) ─────────────────────────────────────┐
│ Lobby DO presence + signed-challenge matchmaking │
│ GameRoom DO AUTHORITATIVE sim (imports lib/game), turns │
│ stats.ts signed-result verification → KV leaderboard │
└───────────────────────────────────────────────────────────┘
A DO is a free-tier-friendly always-on coordinator that doubles as the stats
store. Crucially it is the single source of truth for shot outcomes: clients
send only {angle, power, spin}, the DO runs the one simulation that counts and
broadcasts {finalState, events}. JS float math isn't bit-identical across
engines, so client-side results can't be trusted to decide a match — the DO's
result is authoritative. Clients animate locally for instant feedback then snap
to the DO. A client-sent state hash is a diagnostic desync detector only.
lib/wallet/config.ts builds a wagmi WagmiAdapter from
@reown/appkit-adapter-wagmi, and app/providers.tsx calls createAppKit()
once at module load. The AppKit modal offers a WalletConnect QR code for all
mobile wallets (Rainbow, MetaMask Mobile, Trust, …) alongside injected/EIP-6963
extensions and Coinbase Smart Wallet. The connect UI is opened imperatively via
useAppKit().open() — the shared components/ConnectWallet.tsx button wraps it.
Set the Reown (WalletConnect) project id via NEXT_PUBLIC_WC_PROJECT_ID; it
defaults to the project's canonical id 43bdd1b8c477ac4d4a4264a14a8472f8. Create
your own at https://dashboard.reown.com.
Build note: WalletConnect's
pinologger optionally requirespino-pretty(dev only);next.config.jsaliases it tofalseto silence the webpack "Module not found" warning.
A connected wallet is enough to play, challenge, and rank. ENS just gives a
recognizable name + avatar. EnsPrompt is a soft, dismissible nudge — never a
gate. EnsRegister performs a real two-tx commit/reveal against the current
struct-based ETHRegistrarController, with ABIs/addresses pinned in
lib/ens/contracts.ts (fetched from ens-contracts deployments on 2026-06-06).
# Frontend
npm install
cp .env.example .env.local # set NEXT_PUBLIC_WS_URL after deploying the worker
npm run dev # http://localhost:3000
npm run build # emits ./out (static, IPFS-ready)
# Worker
cd worker
npm install
wrangler kv namespace create STATS # paste id into wrangler.toml
npm run dev # local DO + WS
npm run deploy # wrangler deployVisit /play directly (no match context) for a local hot-seat table that
exercises the physics + rules without the Worker.
npm run build→ pinout/to IPFS (Pinata / cluster), get a CIDv1.- Set the
billiard.ethcontenthash toipfs://<cid>via the ENS resolver. cd worker && wrangler deploy; setNEXT_PUBLIC_WS_URLto thewss://URL, rebuild, re-pin. Worker CORS already allows.eth/.eth.linkorigins.
- Wallet via Reown AppKit (WalletConnect QR + injected/EIP-6963 + Coinbase);
project id from
NEXT_PUBLIC_WC_PROJECT_ID. - No game contract; challenges/results are signed messages.
- Fully static frontend; all dynamic behavior in the Worker/DO.
- Deterministic physics, but the GameRoom DO is authoritative; hashes detect drift.
- ENS ABIs/addresses pinned from source; ENS optional, never required.