Docs
Contents

Example apps

The testnet's games are one app program, zol-coinflip, so players use one balance for all of them. Each game shows a different way to decide an outcome inside the rollup. Build an app walks through the coin flip's table life cycle, which the other two reuse.

GameDecides withPageShows
Coin flipMagicBlock VRF/coinflip.htmlSessions, verifiable randomness, keeper-free closing
ZEC Up/DownMagicBlock's price oracle (Pyth)/updown.htmlReading a live feed in the rollup, time-bounded settlement
Sealed-bid auctionCommit, then reveal/auction.htmlMany players in one pot, hidden bids without a TEE, a certificate on Solana

Coin flip

Trust: the VRF oracle and the rollup validator decide; neither player nor house can pick the outcome. A flip the VRF never answers can be abandoned after 60 s (stakes stay put).

ZEC Up/Down

Adapted from MagicBlock's binary-prediction template, with the ledger instead of SPL token custody.

Why the window: the oracle account holds only its latest price, so whoever settles picks the moment. The template settles at whatever price is current whenever someone calls it, which lets a player wait for a good price; the 2 s window limits that to a few price updates (review finding F4 in ../docs/review/close-pot-actions.md).

// settle_open_bet (abridged)
let (price, published) = read_price(oracle, now)?;
if published < bet.expiry { return err!(PriceNotYet); }                         // too early: retry
if published > bet.expiry + SETTLE_PUBLISH_WINDOW_SECS { /* void */ return Ok(()); }
let (from, to, amount) = if price == bet.open_price { /* tie */ }
    else if (price > bet.open_price) == bet.up { (house_seat, bettor_seat, updown_win(stake)) }
    else { (bettor_seat, house_seat, stake) };
zol_ledger::cpi::move_seat(signed(..), from, to, amount)?;

Sealed-bid auction

Commit-reveal on a ledger pot. MagicBlock's sealed-auction template hides bids in a Private Ephemeral Rollup (a TEE); this version needs no new trust, at the cost of publishing all bids at the reveal.

PhaseWhereWhat happens
OpenSolanacreate_auction: the seller (seat 0, a 1-zat stake) opens a pot and an Auction with a deposit D (the bid cap) and absolute deadlines
Register (45 s)Solanaregister_bidder: each bidder pays D into the pot. Equal deposits reveal nothing. At most 8 bidders
StartSolanastart_auction at the register deadline: pot and auction into the rollup
Bid (45 s)rollupcommit_bid(sha256("ZOL/bid/v1" ‖ auction ‖ bidder ‖ amount ‖ salt)); replaceable until the deadline
Reveal (45 s)rollupreveal_bid(amount, salt), checked against the commitment, amount ≤ D
Endrollupend_auction (anyone, or the scheduled task): the highest revealed bid wins (ties: earlier registration) and pays its bid to the seller; each sealed bid never revealed pays D/10 to those who revealed; the pot returns with cash_out_auction attached
CertificateSolanaThe Auction account stays, with the winner and the price

The salt is sha256("ZOL/bid-salt/v1" ‖ session seed ‖ auction), so a page can always recompute it from the player's key; only the amount must be remembered (the page stores it). bid_commitment has a test vector shared by the Rust and TypeScript implementations.

Why the penalty: without one, a bidder could watch others reveal and then decide whether to reveal. It goes to the bidders who revealed, not the seller, so a seller bidding through a second key cannot use a non-reveal as a free cancel.

The house

The games' counterparty is an ordinary player: a session key with a ledger balance (the bankroll), run by a service (../web/house/). It builds the transactions players sign (and pays their fees), seats itself, opens auctions, settles bets, and closes tables as a fallback. To the ledger and the VRF it is a player like any other; the ledger's guarantees do not depend on it.