Onchain case-opening game on Base, built for the Inco Summer Game Jam 2026.
One dollar buys a real Megapot lottery ticket, and the case comes on top of it. What's inside the case lives in an encrypted, finite pool on Inco Lightning: the contents exist and are committed before anyone opens anything, but nobody, including us, can see them until a slot is drawn and revealed.
Because the pool is drawn without replacement, the counter "3 legendaries left out of 200 slots" is verifiable rather than a marketing claim. And when the deck is spent, or its vault is taken, the contract deals it again by itself.
In Rome, a tessera was a token that granted entry: to the games, to the grain dole. It was also a single piece of a mosaic. The name is about both the ticket and the shards that assemble into one.
Live and playable at tessera.unitynodes.com.
One transaction buys a real Megapot ticket and draws a case; five weight redeems for another ticket, paid out of the game's own referral income. Redeeming, staking, the vault and the battle arena all work on chain. 118 fork tests run against live Base Sepolia and Base mainnet.
Still open: the mainnet deploy, and Megapot's testnet draw, runJackpot()
reverts on Base Sepolia even for its owner, so no jackpot ever settles there.
The same contract runs daily on mainnet. See the addresses below for the
technical record.
- Player pays 1 USDC
- The contract buys a real Megapot ticket, with itself as referrer
- The same transaction draws one slot from the encrypted pool and reveals it
- Most slots are Grout, worth nothing. The rare ones carry weight
- Five weight redeems for another real Megapot ticket, funded entirely by the 10% referral fee the game earns on every purchase
Weight is the only unit the contract counts in. A Shard is weight 1, so five of them make a ticket; a heavier slot can be worth a ticket, two, or five on its own. Each deck is judged by the table it was cut with, so a slot bought under last season's rules neither gains nor loses value when new rules arrive.
Half of that commission does not get handed out a ticket at a time. It piles up in the vault, and exactly one case in the deck opens it, all of it, at once. Same money, but a pot you can watch grow while the number of remaining cases falls.
Or don't take the ticket: stake what you won instead. Your next case decides it: anything at all doubles the stake, an empty slot burns it. You never risk money, only the bonus; the dollar already bought a real ticket either way.
Or play it against someone: the arena. Two players open a case each, the heavier card takes both bonuses, equal weight is a draw and each keeps their own. The loser still keeps the real ticket their dollar bought; the bonus is all that moves.
The card of whoever opens the battle stays sealed until a challenger pays. That is the whole reason the arena works: if the card were visible, nobody would ever take a bad matchup and there would be no market. A battle nobody joins can be abandoned after fifteen minutes, and anyone at all can settle a joined one, so the loser cannot freeze it by staying away.
No external funding. The prize pool pays for itself out of its own turnover, and the contract cannot pay out more weight than its decks were built with, that ceiling is enforced in code, not policy.
Measured with live transactions on Base Sepolia, not taken from documentation.
| Click → prize, ticket included | 7.1 – 9.4 s (avg 8.4 s) |
| Of which: covalidator | 5.9 – 8.6 s |
| Of which: transaction | 0.8 – 1.6 s |
| Gas per open, ticket included | 282 604 (383 450 first) |
| Gas per redeem | 409 152 |
| Referral earned per open | $0.10 of every $1 |
| Inco fee per open | 0 |
| Inco fee per deck (1000 slots) | 0.002 ETH, once per season |
The whole loop closed on chain: ten paid opens, five shards redeemed, and the player ends up holding eleven real tickets. The eleventh was bought by the game out of the referral fees the first ten produced.
The covalidator wait dominates and is outside our control. The roulette animation is therefore adaptive: it loops until the result lands rather than running a fixed duration.
contracts/src/ TesseraDeck: the game, the deck, shards, treasury.
contracts/src/adapters/ One interface, two Megapot ABIs.
contracts/test/ Fork tests against live Base Sepolia and Base mainnet.
web/ The site: Next.js 16, Tailwind v4, wagmi/viem.
web/app/api/opens/ Chain history, read once on the server for everyone.
scripts/ Latency measurement and the audit suite.
Chain history lives on the server rather than in each browser on purpose: the
public RPC caps getLogs at 2000 blocks, so every new visitor used to walk
the whole season a window at a time before the drop feed said anything.
| TesseraDeck (ERC-1967 proxy) | 0x985520De2A14BD443d06DcA07A57Ef4F349bd8B1 |
| MegapotLegacyAdapter | 0x5Ac6bB03e8Fb6435A46EEb70b2f1F692a108030e |
The proxy address is the game. The logic behind it is replaceable and the decks, slots, vaults and open battles survive the swap, which is the whole reason it is there.
How many decks there are, how big they are and what is in them is deliberately
not written here. Anyone can cut a deck, so the catalogue changes without a
commit, and a table frozen in a README would be out of date the first time
somebody used the site. The shape of each deck is fixed at the moment it is
cut and cannot be edited afterwards, so the chain is the only honest place to
read it from: the counters on the site do exactly that, and
scripts/audit-chain.cjs recomputes them independently to check.
Five weight makes one real ticket. Everything at weight zero is Grout: the case added nothing on top of the ticket the dollar already bought.
Because every opened slot is publicly revealed, anyone can count whether the heavy slot is still in the pool. That is the game: not a stated probability, but a finite pile you can watch empty.
A deck deals itself again, with the same size, drop table and vault rule, when its last card is drawn or when its vault is taken. Nobody triggers it: the contract does it inside the next player's transaction, and pays the Inco covalidators from its own ETH balance, which anyone can top up. So the pile is finite within a deal, and the deal it belongs to is on every open.
| Inco executor (both chains) | 0x4b9911b0191B0b6a6eA8F2Ed562e20Cff5AC8624 |
| Megapot, Base Sepolia | 0x6f03c7BCaDAdBf5E6F5900DA3d56AdD8FbDac5De |
| MPUSDC (test, free mint) | 0xA4253E7C13525287C56550b8708100f93E60509f |
| Megapot legacy, Base mainnet | 0xbEDd4F2beBE9E3E636161E644759f3cbe3d51B95 |
| Megapot new, Base mainnet | 0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2 |
Base has two live Megapot contracts with different ABIs. Both are supported; the mainnet deploy is one constructor argument, not a rewrite.
cd contracts && npm install && forge build && forge testcd web && npm install && npm run devcd scripts && npm install && node e2e-open.cjs <contract> <privateKey>cd scripts && node e2e-redeem.cjs <contract> <privateKey>Requires .env: copy .env.example and fill it in. Base Sepolia only;
MPUSDC mints freely, so nothing here costs real money.
cd scripts && set -a && . ../.env && set +a && ./audit.shFive levels, in this order: the contracts, then the chain against the server,
then the server against what is actually painted on the screen, then load and
cold-visit timing. OPENS=3 ./audit.sh adds a real browser run with a stubbed
wallet, which spends real slots out of a deck.
audit-chain.cjs deliberately does not import any site code. It reads the
chain with its own client and compares. A check assembled from the same code it
is checking would agree with every one of that code's mistakes.
Inco's docs point at a confidential-deck-template repository that is not
publicly available. This project does not use it. The deck is built directly on
e.shuffledRange and the elist primitives that ship in @inco/lightning.
MIT