Bringing South Asia's most popular card game to blockchain, with zero-knowledge proofs to eliminate the cheating that plagues existing platforms.
🎮 Play Live · 📜 Contract on Testnet
Seep (also called Sweep) is a trick-taking card game played by 100M+ people across India, Pakistan, and the South Asian diaspora. On Google Play, Seep apps have 100,000+ downloads - but dig into the reviews and you'll find a recurring theme:
⭐ "The bot always knows my cards" ⭐ "They bid 13 without even having a King" ⭐ "Rigged - the AI cheats every single time"
The core problem is information asymmetry. In physical Seep, you trust that your opponent can't see your hand. In digital Seep, the server sees everything. Bots exploit this by:
- Bidding cards they don't hold - a player bids 13 (King) when they have no King, but they know you don't have one either
- Building houses on phantom cards - creating a house of value 11 when they don't hold a Jack, knowing the remaining Jacks are buried in the deck
- Perfect information play - the server-side AI sees both hands and the deck order, making optimal plays that are statistically impossible for a fair player
ZK Seep solves this. By enforcing zero-knowledge proofs on-chain, every bid and every house-building move cryptographically proves the player holds the card they claim - without revealing what else is in their hand.
In Seep, there are two critical moments where a player claims to hold a specific card:
- Bidding - "I bid 11" means "I have a Jack (value ≥ 9) in my hand"
- House building - "I build a house of value 12" means "I have a Queen in my hand to claim it later"
Without ZK proofs, the server (or opponent in P2P) must trust these claims blindly. With ZK proofs:
┌─────────────────────────────────────────────────────┐
│ ZK Proof Circuit │
│ │
│ Private inputs: hand[] (12 card values), salt │
│ Public inputs: hand_hash, target_value │
│ │
│ Constraints: │
│ 1. Poseidon2(hand ++ salt) == hand_hash │
│ 2. ∃ i : hand[i] == target_value │
│ │
│ Result: Proof that target_value ∈ hand │
│ without revealing hand contents │
└─────────────────────────────────────────────────────┘
The player commits a Poseidon2 hash of their hand at the start of the game. For every bid and house move, they generate a ZK proof showing "my hand contains a card of this value" - verified against the committed hash. No one - not the opponent, not the server, not the blockchain - ever sees the actual hand.
Seep is a 2-player card game using a standard 52-card deck. The objective is to capture cards from the floor and score points.
📖 Want the full deep-dive? Check out SeepRules.md for exhaustive rules, advanced strategy tips (card memorization, the "last Jack" power play, Seep prevention), worked examples from real game situations, and common mistakes to avoid.
| Cards | Game Value | Score |
|---|---|---|
| A (Ace) | 1 | 1 point each |
| 2–10 | Face value | Spades: face value; 10♦: 6 pts |
| J (Jack) | 11 | Spades only: 11 pts |
| Q (Queen) | 12 | Spades only: 12 pts |
| K (King) | 13 | Spades only: 13 pts |
Total points in the deck: 100. A standard win requires capturing more than your opponent.
flowchart TD
A["🃏 Deal 4 cards to each player<br/>4 cards face-up on the floor"] --> B["💰 Bidding Phase"]
B --> C{"Player 1 bids a value<br/>9, 10, 11, 12, or 13"}
C --> D["🔐 Bid Move + ZK Proof<br/>Prove you hold the bid card<br/>without revealing your hand"]
D --> E["♠️ Play Phase — Alternate Turns"]
E --> F{"Choose a move"}
F --> T["Throw card<br/>to floor"]
F --> P["Pick up cards<br/>matching your card's value"]
F --> H["🏠 Build/Fix House + ZK Proof<br/>Lock cards for future capture"]
T --> G
P --> G
H --> G
G{"Hand empty?"}
G -->|"Yes"| R{"Cards left<br/>in deck?"}
R -->|"Yes"| RE["Deal 12 more cards each"]
RE --> E
R -->|"No"| S["🏆 Count captured cards<br/>Determine winner"]
style A fill:#1a1a2e,color:#eee
style B fill:#16213e,color:#eee
style C fill:#0f3460,color:#eee
style D fill:#533483,color:#eee
style E fill:#1a1a2e,color:#eee
style H fill:#533483,color:#eee
style S fill:#e94560,color:#fff
On each turn, a player plays one card from their hand. Depending on the floor state, they can:
| # | Move | Description | ZK Proof? |
|---|---|---|---|
| 1 | Throw | Place card on floor as a new loose pile | No |
| 2 | Build House | Card + loose piles → unfixed house (value 9–13) | 🔐 Yes |
| 3 | Cement | Card matches unfixed house → fix it | 🔐 Yes |
| 4 | Merge + Fix | Card + loose piles merge with unfixed house → fixed | 🔐 Yes |
| 5 | Add to Fixed | Card + loose piles → add to existing fixed house | 🔐 Yes |
| 6 | Direct Fix | Card directly onto fixed house of same value | 🔐 Yes |
| 7 | Pick Up | Card value matches pile/combo sum → capture all | No |
Houses are piles worth 9–13 that "lock" cards on the floor. Building a house of value X means you're reserving those cards to pick up later with a card of value X. The ZK proof ensures you actually have that card.
If you capture every card on the floor in a single pickup, that's a Seep - worth bonus points equal to the bid value. But three Seeps in one game cancels all your Seep bonuses!
graph TB
subgraph Browser["Browser - React + TypeScript"]
UI["Game UI<br/>ZkSeepGame.tsx"]
Engine["Local Game Engine<br/>SeepGame.ts"]
Sync["Sync Service<br/>BroadcastChannel / PeerJS"]
Session["Session Wallet<br/>Keypair in sessionStorage"]
ZkProof["ZK Proof Generator<br/>Noir / Barretenberg"]
OnChain["On-Chain Hook<br/>useOnChain.ts"]
end
subgraph Stellar["Stellar Network"]
ZkSeep["ZK Seep Contract<br/>Game state + proof verification"]
MockVerifier["Mock Verifier<br/>always returns true<br/>testnet"]
RealVerifier["UltraHonk Verifier<br/>full ZK verification<br/>localnet"]
GameHub["Game Hub Contract<br/>Points + leaderboard"]
end
subgraph Circuit["Noir Circuit"]
HC["hand_contains<br/>Poseidon2 hash + membership"]
end
UI --> Engine
UI --> Sync
UI --> OnChain
OnChain --> Session
OnChain --> ZkSeep
ZkSeep --> MockVerifier
ZkSeep -.-> RealVerifier
ZkSeep --> GameHub
ZkProof --> HC
OnChain --> ZkProof
Sync <-->|"BroadcastChannel (local)<br/>or WebRTC (cross-device)<br/>seed, bids, moves"| Sync
style Browser fill:#0d1117,color:#c9d1d9,stroke:#30363d
style Stellar fill:#1a1a2e,color:#eee,stroke:#533483
style Circuit fill:#16213e,color:#eee,stroke:#0f3460
style MockVerifier fill:#e94560,color:#fff
style RealVerifier fill:#00b4d8,color:#fff
| Component | Purpose |
|---|---|
ZK Seep Contract (contracts/zk-seep) |
On-chain game state, turn validation, ZK proof verification via external verifier |
Mock Verifier (contracts/mock-verifier) |
Always returns true - used on testnet where UltraHonk exceeds the 400M CPU instruction cap |
| Game Hub | Official testnet Game Hub — points tracking and leaderboard across all games in the Stellar Game Studio |
Noir Circuit (circuits/hand_contains) |
Poseidon2 hash commitment + card membership proof |
Game Engine (src/game/) |
Full Seep rule engine in TypeScript - move generation, validation, scoring |
| Sync Service | Dual-mode: BroadcastChannel (localhost) or PeerJS WebRTC (deployed/cross-device) |
| Session Wallet | Ephemeral Stellar keypair per tab - signs game transactions silently without wallet popups |
| On-Chain Hook | Fires start_game, make_bid, make_move, end_game to the contract during gameplay |
sequenceDiagram
participant P1 as Player 1
participant P2 as Player 2
participant PeerJS as PeerJS (WebRTC)
participant Contract as ZK Seep Contract
participant Verifier as Verifier Contract
participant Hub as Game Hub
P1->>Contract: start_game(session, p1, p2)
Contract->>Hub: hub.start_game(...)
P1->>Contract: commit_hand(hash(hand + salt))
P2->>Contract: commit_hand(hash(hand + salt))
P1->>PeerJS: bid(11)
PeerJS->>P2: bid(11)
P1->>Contract: make_bid(11, zk_proof)
Contract->>Verifier: verify_proof(public_inputs, proof)
Verifier-->>Contract: true ✓
loop Each Turn
P1->>PeerJS: move(idx)
PeerJS->>P2: move(idx)
P1->>Contract: make_move(type, card, proof)
Contract->>Verifier: verify_proof(...)
Verifier-->>Contract: true ✓
end
P1->>Contract: end_game(session)
Contract->>Hub: hub.end_game(winner)
The hand_contains circuit is written in Noir (pinned to Nargo 1.0.0-beta.9, bb 0.87.0):
fn main(
hand: [Field; 12], // private: card values (1-13, 0 = empty)
salt: Field, // private: random nonce
hand_hash: pub Field, // public: Poseidon2 commitment
target_value: pub Field, // public: claimed card value
) {
// 1. Verify commitment: hash(hand ++ salt) == hand_hash
let mut preimage: [Field; 13] = [0; 13];
for i in 0..12 { preimage[i] = hand[i]; }
preimage[12] = salt;
assert(Poseidon2::hash(preimage, 13) == hand_hash);
// 2. Prove possession: ∃ i : hand[i] == target_value
let mut found = false;
for i in 0..12 {
if hand[i] == target_value { found = true; }
}
assert(found, "Target value not found in hand");
}Why Poseidon2? It's a ZK-friendly hash function - ~100x cheaper to prove inside a circuit compared to SHA-256 or Keccak.
| Environment | Verifier | CPU Instructions | Status |
|---|---|---|---|
| Testnet | Mock Verifier (always true) |
~300K | ✅ Deployed |
Localnet (--limits unlimited) |
Real UltraHonk Verifier | ~367M | ✅ Works |
| Mainnet | Real Verifier | Needs 400M+ cap | ⏳ Waiting for limit increase |
The Stellar testnet currently has a 400M CPU instruction cap per transaction. UltraHonk proof verification requires ~367M instructions for even a basic proof - dangerously close to the limit. Per hackathon organizer guidance, local deployment with --limits unlimited is accepted for evaluation.
The mock verifier allows us to demonstrate the full transaction flow on testnet while the real verifier runs on localnet.
Traditional blockchain games require users to approve every transaction via their browser wallet (Freighter, MetaMask, etc.). In a card game with 20+ moves per match, this is unplayable.
ZK Seep uses an embedded session wallet:
- Player connects Freighter (one-time)
- An ephemeral
Keypairis generated and stored insessionStorage - Player funds the session wallet with XLM (one Freighter approval)
- All in-game transactions (
commit_hand,make_bid,make_move,end_game) are signed silently by the session wallet - When the browser tab closes, the session wallet is destroyed
This gives a Web2-like gameplay experience with full on-chain verifiability.
ZK Seep uses a dual-mode networking layer that auto-selects the best transport:
On localhost, the app uses the browser-native BroadcastChannel API for instant, zero-latency communication between tabs. No network configuration required — messages are delivered in-process.
Tab 1 ←→ BroadcastChannel("zk-seep-{roomCode}") ←→ Tab 2
Note: Open two normal browser tabs (not InPrivate/Incognito —
BroadcastChanneldoesn't bridge across browser contexts). Each tab automatically gets its own session wallet viasessionStorage.
On deployed URLs (e.g., Vercel), the app switches to PeerJS WebRTC for direct browser-to-browser communication across devices and networks. To handle NAT traversal (essential when players are on mobile data, corporate WiFi, or behind firewalls), the app uses Metered TURN relay servers.
Browser A ←→ TURN Relay (global.relay.metered.ca) ←→ Browser B
The ICE configuration covers every network scenario:
| Protocol | Port | Use Case |
|---|---|---|
| STUN | 80 | NAT discovery |
| TURN UDP | 80 | Standard relay |
| TURN TCP | 80 | Corporate firewall fallback |
| TURN | 443 | HTTPS port (rarely blocked) |
| TURNS (TLS) | 443 | Maximum compatibility |
TURN credentials are stored as environment variables (not hardcoded):
VITE_TURN_USERNAME=your_metered_username
VITE_TURN_CREDENTIAL=your_metered_credentialIf TURN credentials are not set, the app falls back to STUN-only (works on most home networks but may fail on restrictive ones).
- Player 1 creates a room → gets a room code
- Player 2 enters the room code
- Game seed, bids, and moves are exchanged directly between browsers
- No central game server required — fully decentralized
Both players run the same deterministic game engine with the same seed. Move indices are exchanged, so both engines stay perfectly synchronized.
| Factor | Detail |
|---|---|
| 100M+ players | Seep is the #1 card game in Punjab and widely played across South Asia |
| Stellar's India push | SDF is actively expanding in India - Seep brings a massive ready-made audience |
| Cheating epidemic | Every major Seep app on Play Store is plagued by cheating complaints |
| ZK is the solution | Zero-knowledge proofs make cheating mathematically impossible |
| Low fees | Stellar's ~0.00001 XLM tx fees make per-move on-chain verification viable |
| Fast finality | 5-second block times keep gameplay responsive |
| Platform | Cheating Protection | On-Chain | Cross-Device | ZK Proofs |
|---|---|---|---|---|
| Play Store Seep apps | ❌ None | ❌ | ❌ | ❌ |
| Existing blockchain games | ✅ | ❌ | ||
| ZK Seep | ✅ Cryptographic | ✅ Full | ✅ | ✅ |
| Layer | Technology |
|---|---|
| Smart Contract | Rust + Soroban SDK |
| ZK Circuit | Noir (Nargo 1.0.0-beta.9) |
| Proof System | UltraHonk (bb 0.87.0) |
| Frontend | React 19 + TypeScript + Vite |
| Styling | TailwindCSS 4 |
| Multiplayer | BroadcastChannel (local) + PeerJS WebRTC (cross-device) |
| Wallet | Freighter (connect) + Session Keypair (gameplay) |
| Deployment | Vercel (frontend) + Stellar Testnet (contracts) |
| Contract | Address |
|---|---|
| ZK Seep | CCTI7YU4VJKERNO6Y2UHKVV4WNHIPDNAHG5OXNAMAJUKL5ZQBSEJ3QDV |
| Game Hub (official) | CB4VZAT2U3UC6XFK3N23SKRF2NDCMP3QHJYMCHHFMZO7MRQO6DQ2EMYG |
| Mock Verifier | CC64CBJ5KCVCJX4PO4MQXHNFL6AGIQ2MDG6UIFGZ5NWSKN5D2ZIHVEIX |
- Rust +
wasm32-unknown-unknowntarget - Stellar CLI
- Bun (or Node.js 18+)
- Nargo 1.0.0-beta.9 (for ZK circuits)
# Clone and install
git clone https://github.com/user/ZK-Seep.git
cd ZK-Seep
# Build contracts
stellar contract build
# Deploy to testnet
bun run scripts/deploy.ts
# Start frontend
cd zk-seep-frontend
bun install
bun run devThe real UltraHonk ZK verifier requires ~367M CPU instructions — beyond the testnet 400M cap. Use localnet with unlimited limits to run the full ZK pipeline.
Note: On localnet, the deploy script uses a mock game hub (since the official Game Hub only exists on testnet). The mock hub has the same interface but is deployed locally. The
VITE_MOCK_GAME_HUB_CONTRACT_IDenv var in.env.localreflects this.
- Docker — for running the Stellar Quickstart node
- Stellar CLI —
stellarbinary in your PATH - Rust with
wasm32-unknown-unknowntarget (rustup target add wasm32-unknown-unknown) - Bun (or Node.js 18+)
docker run --rm -it -p 8000:8000 --name stellar \
stellar/quickstart:soroban-dev --local --limits unlimitedWait until you see Horizon : listening on port 8000 before proceeding.
stellar network add localnet \
--rpc-url "http://localhost:8000/soroban/rpc" \
--network-passphrase "Standalone Network ; February 2017"
stellar keys generate alice --network localnet --fundThe deploy script builds, deploys, and wires up all three contracts (mock-game-hub, mock-verifier, zk-seep) and writes the contract IDs to .env.local:
chmod +x scripts/deploy-localnet.sh
./scripts/deploy-localnet.shExpected output:
mock-game-hub: C...
mock-verifier: C...
zk-seep: C...
✅ .env.local updated
cd zk-seep-frontend
bun install
bun run dev- Open two normal browser tabs at
http://localhost:5173- Do not use InPrivate/Incognito —
BroadcastChanneldoesn't bridge across browser contexts
- Do not use InPrivate/Incognito —
- Click Create & Fund Game Wallet in both tabs
- Each tab gets its own session wallet via
sessionStorage(no Freighter needed on localnet)
- Each tab gets its own session wallet via
- Player 1 clicks Create Game → copy the room code
- Player 2 pastes the room code → clicks Join Game
- Both players authorize the game on-chain (multi-sig handshake)
- Play! House-building and bid moves generate real ZK proofs verified on-chain
ZK-Seep/
├── contracts/
│ ├── zk-seep/ # Main game contract (Rust/Soroban)
│ │ └── src/lib.rs # 689 lines - game state, turns, ZK verification
│ ├── mock-verifier/ # Always-true verifier for testnet
│ └── mock-game-hub/ # Points tracking contract
├── circuits/
│ ├── hand_contains/ # Noir circuit: prove card ∈ hand
│ └── no_high_cards/ # Noir circuit: prove no high cards
├── zk-seep-frontend/
│ ├── src/
│ │ ├── game/ # Full Seep engine (TypeScript)
│ │ │ ├── Game.ts # Game lifecycle, dealing, rounds
│ │ │ ├── Player.ts # Hand management, move generation
│ │ │ ├── Center.ts # Floor state, house building
│ │ │ ├── Card.ts # Card types, scoring
│ │ │ └── Move.ts # 7 move types
│ │ ├── games/zk-seep/
│ │ │ ├── ZkSeepGame.tsx # Main game component (1050 lines)
│ │ │ ├── zkSeepService # Contract interaction layer
│ │ │ └── components/ # UI components
│ │ ├── hooks/
│ │ │ ├── useWallet.ts # Freighter + session wallet
│ │ │ └── useOnChain.ts # Contract call hook
│ │ └── services/
│ │ └── peerService.ts # Dual-mode: BroadcastChannel / PeerJS
│ └── package.json
└── scripts/ # Build, deploy, setup scripts
This is not a weekend hackathon project. ZK Seep includes:
- ✅ Complete Seep game engine — all 7 move types, house limits, seep bonuses, multi-round dealing
- ✅ ZK circuit — Noir circuit with Poseidon2 hash commitment + card membership proof
- ✅ On-chain game contract — 689 lines of Rust, full game lifecycle with ZK proof enforcement
- ✅ Cross-device multiplayer — dual BroadcastChannel / PeerJS WebRTC, no central server
- ✅ Embedded session wallet — zero popup fatigue, Web2-like UX
- ✅ Async multi-sig handshake — two session wallets authorize
start_gameacross devices via XDR reconstruction - ✅ Mock verifier — enables full testnet demo within CPU limits
- ✅ Game Hub integration — start_game / end_game for points and leaderboard
- ✅ Beautiful UI — dark theme, card animations, responsive design
- ✅ Live deployment — playable at zk-seep.vercel.app
This project required solving hard, real-world distributed systems and cryptography problems:
| Challenge | What We Solved |
|---|---|
| WebRTC ICE Negotiation | Bypassing symmetric NATs and strict router firewalls with STUN/TURN relay fallback |
| TURN Relay Routing | Credentialed Metered TURN servers across UDP, TCP, and TLS for guaranteed P2P on any network |
| Async Multi-Sig XDRs | Cracking open, modifying sourceAccount + sequence numbers, and resealing Stellar transaction envelopes across devices |
| Smart Contract VM Traps | Navigating footprint drops, require_auth key mismatches, toEnvelope() immutability, and len() == 0 proof safety checks |
| RPC Race Conditions | Managing sequence number increments against delayed ledger indexing with breathing delays |
| Zero-Knowledge Proofs | Browser-side Noir circuit compilation + UltraHonk proof generation, verified on-chain via Soroban cross-contract calls |
| Session Wallet UX | Ephemeral per-tab keypairs that sign silently — zero wallet popups during gameplay |
The start_game contract requires both players to require_auth, but they're on separate devices. Solving this required fixing four critical issues:
- Double-Simulation Footprint Drop — Player 2 re-simulated the transaction locally, which dropped Player 1's nonce from the footprint. Fix: bypass standard submission and send the exact XDR footprint that Player 1 simulated.
toEnvelope()Immutability Trap —tx.toEnvelope()returns a disconnected copy. Injecting Player 1's signature into the envelope didn't propagate to the submitted transaction. Fix: export the modified envelope to Base64, then reconstruct viaTransactionBuilder.fromXDR().- Source Account Mismatch — The transaction's
sourceAccountwas Player 1's, but Player 2 signed the envelope. The network rejected it:"signer does not belong to account". Fix: swapsourceAccountto Player 2 before signing, while preserving Player 1's auth inside the invoke op'sauth[]array. - Sequence Number Offset — After swapping the source, the sequence number was still Player 1's. Stellar requires
current + 1. Fix: fetch Player 2's live sequence number and injectBigInt(seq) + 1ninto the XDR.
The on-chain contract does not enforce turn order — PeerJS delivers moves in ~50ms, but on-chain confirmations take ~5 seconds. Strict alternation would cause NotYourTurn race conditions.
What the contract enforces: player identity, ZK proof validation, game phase validity, score tracking. What the frontend enforces: turn order, move legality, house rules.
On localnet, the same Freighter wallet can be used for both players. Each browser tab generates its own ephemeral session wallet (sessionStorage is per-tab), so the contract sees two distinct players even when the same Freighter is connected.
Built for the Stellar Game Studio Hackathon
Making South Asia's favorite card game fair, verifiable, and unstoppable.
