Bet on prediction markets privately — your position, amount, and identity stay hidden on-chain.
GhostBet is a ZK-powered prediction market built on Base where individual bets are completely invisible on-chain. Users deposit WETH privately through Unlink's zero-knowledge protocol, then place bets via ephemeral burner wallets that are cryptographically unlinked from their identity. Only aggregate pool totals ever appear on-chain — no one can trace which wallet bet how much, or on which side. Markets are sourced from Polymarket and Base-native contracts, with live odds updating in real time. Cashouts and winnings flow back directly into the user's private Unlink balance, keeping the entire lifecycle off the public ledger.
GhostBet is built with Next.js 16 (App Router) on the frontend, Solidity contracts deployed on Base Sepolia via Foundry, and PostgreSQL through Prisma for off-chain state. The core privacy primitive is the Unlink SDK: when a user bets, the server spins up an ephemeral burner EOA via BurnerWallet.create(), funds it by withdrawing from the user's private Unlink WETH pool, and uses that burner to call depositBet() on our BaseExecutionPool contract — then immediately disposes and deletes the burner key. The burner is never associated with the user's wallet on-chain.
The trickiest part was a race condition: Unlink signals the burner as "funded" before the WETH transaction is actually mined. We solved this by polling WETH.balanceOf(burner) on-chain after the Unlink status check, and only proceeding once the balance is confirmed. We also added a 4-second delay plus 3 retries between approve() and depositBet() to handle RPC nodes that lag behind the latest block.
On cashout and claim, funds can't go directly to the user's wallet (that would deanonymize them). Instead, the operator receives the WETH via withdrawBetTo() / claimWinningsTo(), then re-deposits it into the user's Unlink private pool server-side using Permit2 — signed with the operator key, no user interaction required.
Live odds for Base-native markets are read directly from BaseExecutionPool.liquidityByCondition() rather than PooledMarket (whose totals never decrease on withdrawal), while Polymarket odds are pulled from their CLOB API and polled every 10 seconds.

