ZK Texas Hold'em โ fully on-chain, with zero-knowledge card shuffling
No server holds the deck. No operator can see your cards. Every shuffle is a zero-knowledge proof.
Status โข How It Works โข Security Model โข Roadmap โข Get Started
Showdown is an EVM poker engine โ standard Solidity, portable across EVM chains. A working prototype is deployed and playable on Lisk Sepolia testnet.
| Live demo | texas-seven.vercel.app |
| Demo video | YouTube |
| Deployments | Lisk Sepolia (original prototype) ยท Avalanche Fuji (live โ full hand played on-chain) |
| Next target | Avalanche C-Chain โ see Roadmap |
| Stage | Prototype. Testnet funds only, not audited, not for real-money play. |
- Game Factory:
0xe86553AE8f33924b5B7174F64ceaCbeff473548D - Reveal Verifier:
0x49cFFa95ffB77d398222393E3f0C4bFb5D996321
- Game Factory:
0xb84672727349ec69F5BCf4FB0b35532d74eDbbE0 - Reveal Verifier:
0x9BBf0251BB9CD128c7dcE0474cF016D618D5749C
An earlier Fuji deployment (factory
0xBf3c326Cโฆ, verifier0xAE5d214eโฆ) is superseded. It deployed the rawRevealVerifierrather than theZgRevealVerifierwrapper thatGame.solcalls, so card reveals reverted. Games created against it cannot decrypt their cards.
Point the frontend at Fuji with NEXT_PUBLIC_CHAIN_ID=43113. To redeploy:
cd packages/contracts
forge script script/Deploy.s.sol --rpc-url fuji --broadcast # reads PRIVATE_KEY from .envThe script deploys a fresh RevealVerifier on any chain other than Lisk Sepolia, then GameFactory,
linking the QuickSort and TexasPoker libraries automatically.
- Trustless card shuffling โ ZK-SNARK proofs (Zypher Network's ZK Shuffle SDK) prove each shuffle is a valid permutation without revealing card order
- Real stakes โ smart contracts hold and distribute the pot; no IOU accounting
- Strategic showdown โ choose 3 of 5 community cards, so hand construction is a skill decision rather than automatic
- Anti-griefing โ 120-second action timeout with permissionless
forceFold() - Auto-decryption โ cards reveal as soon as all reveal tokens are submitted
- Readable errors โ raw revert data translated to plain language
Showdown is honest about which parts are trustless today and which are not.
Trustless (verified on-chain):
- Card revealing via ZK proofs
- Hand evaluation (
TexasPoker.sol) - Pot custody and distribution (immutable contract logic)
Not yet trustless:
- Shuffle verification runs client-side. Players generate valid ZK shuffle proofs, but those proofs are not currently checked on-chain.
ZgShuffleVerifier โ the contract that would verify shuffle proofs on-chain โ compiles to
27,002 bytes of runtime bytecode. EIP-170 caps
deployed contracts at 24,576 bytes. It overshoots by 2,426 bytes:
$ forge build --sizes
| Contract | Runtime Size (B) | Runtime Margin (B) |
| ZgShuffleVerifier | 27,002 | -2,426 |
| VerifierKeyExtra1_52 | 16,454 | 8,122 |
| VerifierKeyExtra2_52 | 16,450 | 8,126 |
| GameFactory | 15,369 | 9,207 |
| Game | 14,037 | 10,539 |
| RevealVerifier | 6,964 | 17,612 |
Error: some contracts exceed the runtime size limit (EIP-170: 24576 bytes)
Two things worth stating plainly, because both are commonly gotten wrong:
- This is not a Lisk limitation. EIP-170 is enforced identically by every major EVM chain โ Base, Optimism, Arbitrum, and Avalanche C-Chain included. Changing chains does not fix it.
- Compiler settings do not fix it either. Rebuilding with
optimizer_runs = 1yields 26,974 bytes โ still 2,398 over.
Profiling the vendored verifier turned up two specific causes, both addressable:
| Change | ZgShuffleVerifier runtime size |
Margin |
|---|---|---|
| As shipped | 27,002 | โ2,426 |
Externalize the inlined VerifierKey_52 table |
24,735 | โ159 |
Drop the unused verifyGenericProof entry point |
20,755 | +3,821 โ |
PlonkVerifier.verifyProof is private and reached from two public entry points that differ only
by a constant bool. The optimizer specializes the ~6KB verifier body for each, so the contract
carries two near-identical copies โ and the generic one is never called by this project, which only
verifies shuffle proofs. Removing it saves 6,247 bytes and brings the verifier comfortably under
EIP-170 on its own.
The VerifierKey_52 externalization is independent and worth 2,267 bytes. The mechanism already
exists here: VerifierKeyExtra1_52 and VerifierKeyExtra2_52 are deployed as standalone contracts
and staticcall'd directly into memory.
The verifier sources are byte-identical to zypher-game/uzkge.
Upstream's own reference deployment contract, ShuffleService, inlines both the 20- and 52-card key
tables and compiles to 31,072 bytes โ 6,496 over EIP-170. Upstream ships no real deployment
script (scripts/deploy.js is the stock Hardhat sample) and configures no live network; the Solidity
verifier is exercised only against an in-memory Hardhat node. Zypher's production answer is
zytron-precompiles, which provides PLONK and
shuffle verification as native precompiles on their own L2 โ going under EIP-170 rather than fitting
inside it.
So the client-side shuffle here was never a shortcut. On any EIP-170 chain it is the only option the SDK offers as shipped.
test/ShuffleVerify52.t.sol deploys the
size-reduced verifier and runs it against upstream's own 52-card proof vector, lifted verbatim
from uzkge/contracts/solidity/test/plonk_52.js โ Zypher's proof, not one generated to match this
implementation.
[PASS] test_verifierFitsUnderEip170() ZgShuffleVerifier runtime size: 20,755 / 24,576
[PASS] test_verifiesUpstream52CardProof() valid proof accepted
[PASS] test_rejectsTamperedProof() corrupted proof rejected
[PASS] test_reportVerificationGas() 2,383,293 gas
test/ShuffleVerifyLive.t.sol closes the loop
from the other side, running the verifier against a proof captured from a live run of this app's own
/api/get-masked-cards โ /api/first-shuffle pipeline:
[PASS] test_verifiesProofGeneratedByThisApp() 2,382,928 gas
The shuffle Showdown produces today is one the contract can verify on-chain. The remaining work is
wiring Game.sol to call verifyShuffle.
Proving is the slowest part of the system by a wide margin, measured against the dev server with routes warm:
| Operation | Warm |
|---|---|
/api/generate-key |
0.01s |
/api/get-masked-cards |
14.3s |
/api/first-shuffle |
46.7s |
Every player shuffles, so a heads-up hand spends roughly 110 seconds generating proofs before the first bet. This dominates the ~2.38M gas of on-chain verification in practical terms and is the main obstacle to the game feeling playable. Reducing it โ proving in parallel, moving generation to the client, or a faster proving backend โ is ahead of any gas concern.
On-chain shuffle verification costs ~2.38M gas per shuffle. Every player shuffles, so an n-player hand pays that n times.
Against Avalanche C-Chain fees that is affordable. At a spot base fee of ~0.061 gwei
(60,946,345 wei, sampled from api.avax.network in July 2026), one verification costs roughly
0.000145 AVAX โ a fraction of a cent, so a heads-up hand adds well under a cent of verification
cost. Congestion raises this, and the figure is a point sample rather than a benchmark; producing
proper numbers across real gameplay is part of milestone 1.
Until this lands in the deployed game: testnet play only. Treat the shuffle as fair-by-convention, not fair-by-proof.
The engine is chain-agnostic Solidity, so the migration is mostly mechanical โ with one genuine piece of engineering.
1. Turn on on-chain shuffle verification
Sizing and correctness are already proven (see Security Model): the verifier
fits at 20,755 bytes and accepts upstream's real 52-card proof, at ~2.38M gas. What remains is
integration and economics โ deploy the verifier, wire Game.sol to call verifyShuffle at shuffle
submission, benchmark real cost per hand at C-Chain fees, and decide where verification stays
affordable. This removes the last trust assumption in the protocol.
1b. Upstream the fix
The EIP-170 fix applies to every game built on Zypher's SDK, not just this one. Worth a PR to
zypher-game/uzkge.
2. Deploy to Avalanche โ Fuji done, C-Chain remaining Fuji is live: the contracts are deployed (see above) and a full hand has been played and settled on-chain โ create, join, shuffle with ZK proofs, bet, reveal, showdown, payout. What remains is promoting the same deployment to C-Chain mainnet and publishing gas benchmarks for a full hand at mainnet fees.
3. Session-key "buy-in" UX (the wallet-popup fix) A full hand currently makes each player sign ~15 wallet transactions โ join, shuffle, a bet per round, a reveal-token submission per round, chooseCards, settlement. That's the single biggest thing standing between this and something that feels like a game. The fix is a per-user in-app wallet:
- On first connect, the user signs one message; that signature deterministically derives a dedicated game wallet (non-custodial, recoverable on any device by re-signing, nothing stored server-side).
- The user buys in once โ funding that wallet with chips in a single transaction from their EOA.
- Every subsequent game action is signed silently by the game wallet: the deterministic housekeeping (shuffle, reveal tokens, chooseCards) automatically, and each bet the moment the user clicks it. Signing loses the popup, not the decision, and the blast radius is capped to the chips deposited โ never the user's main wallet.
Net effect: per-hand wallet popups drop from ~15 to zero. The only wallet interactions left are buying in when chips run low and cashing out when leaving โ front-loaded, not per-action.
Game.sol already supports this with essentially no access-control change: every write function
gates only on msg.sender matching the recorded player address, with no EOA-vs-contract assumption
anywhere, so the game wallet simply registers as the player. The one contract change is placeBet,
which today requires msg.value in native AVAX โ moving bets onto an internal chip balance is what
lets the buy-in happen once instead of per bet. Testnet uses faucet chips so onboarding costs
nothing. Production graduates the derived wallet to an ERC-4337 smart account with scoped session
keys and a paymaster, making housekeeping gasless.
4. Tournament mode Sponsor-funded prize-pool contracts, Elo ratings, scheduled multi-table events.
5. Mainnet & first players Mainnet launch, plus a community game night with the Team1 Nigeria and Web3Bridge builders โ a skill-based tournament with a sponsored prize pool (free entry, no player wagering, so it's an esports-style prize, not gambling) โ driving 100+ unique players onto Avalanche.
The game runs an 8-stage flow from shuffle to payout:
Shuffle โ Ante โ Pre-Flop โ Flop โ Turn โ River โ End (Choose Cards) โ Winner
Every player shuffles the deck before betting opens.
- Player 1 generates a masked deck + public key commitment + SNARK proof, submits on-chain
- Players 2โN fetch the on-chain deck, shuffle, generate a SNARK proof, submit
- Play begins only when all players have shuffled
Each shuffle is cryptographically proven to be a valid permutation without revealing card order. (See Security Model for where that proof is currently checked.)
| Round | Cards Revealed | Action Required |
|---|---|---|
| 1. Ante | None | Initial pot contribution |
| 2. Pre-Flop | 2 hole cards per player | Bet + submit reveal tokens for opponents' cards |
| 3. Flop | 3 community cards | Bet + submit reveal tokens |
| 4. Turn | 4th community card | Bet + submit reveal tokens |
| 5. River | 5th community card | Bet + submit reveal tokens |
Bets are transfers of the chain's native token, sent with the transaction. Players must call or
raise the current high bet, or fold and forfeit their stake. Each action carries a 120-second
timeout; any player may call forceFold() once it expires.
- Submit reveal tokens โ derived from your secret key, these let others decrypt specific cards without ever exposing the key itself
- Auto-decryption โ once all tokens for a card are in, it decrypts automatically (2-second refresh); no further action needed
- Privacy โ only you can decrypt your hole cards until the End round; community cards decrypt once all tokens are submitted
After River betting, each player picks 3 of the 5 community cards and submits via
chooseCards().
[Hole Card 1] [Hole Card 2] [Community] [Community] [Community]
โ โ โ โ โ
fixed fixed your choice your choice your choice
Your hole: [Aโ Kโ ]
Community: [Qโ Jโ 10โ 2โฆ 7โฃ]
1 2 3 4 5
Pick 1, 2, 3 โ [Aโ Kโ Qโ Jโ 10โ ] = Royal Flush ๐
Standard Hold'em picks your best five automatically. Showdown makes you do it โ hand reading becomes a skill the player exercises, not a function the client calls.
TexasPoker.solevaluates each player's 5-card hand- Each hand receives a weight โ Royal Flush 9000+, High Card 0โ999
- Highest weight wins; the pot transfers immediately
claimWinnings()remains available as a fallback if the automatic transfer fails
Hand rankings (see TexasPoker.sol): Royal Flush 9000+ ยท Straight Flush 8000+ ยท Four of a Kind 7000+ ยท Full House 6000+ ยท Flush 5000+ ยท Straight 4000+ ยท Three of a Kind 3000+ ยท Two Pair 2000+ ยท Pair 1000+ ยท High Card 0โ999
- Smart contracts โ Solidity, Foundry
- ZK โ
@zypher-game/secret-engine, Groth16 / Plonk over BN254 - Frontend โ Next.js, Tailwind CSS, shadcn/ui
- Web3 โ wagmi, viem, web3modal
- Backend โ Hono
Turborepo monorepo. apps/www is the Next.js application; packages/contracts is the Foundry project.
# Clone with submodules (forge-std, openzeppelin-contracts)
git clone --recurse-submodules https://github.com/oderahub/showdown.git
cd showdown
pnpm install
pnpm devContracts:
cd packages/contracts
forge build --sizes # note: ZgShuffleVerifier exceeds EIP-170, see Security Model
forge test![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Built with Zypher Network's ZK Shuffle SDK.









