HOW IT WORKS
Blackjack where every card you're dealt is a real Megapot lottery ticket, and the dealer's hole card is genuinely unreadable rather than merely hidden.
A card is a ticket is a dollar
A card is not a picture of a ticket. It is the same on-chain object. Every card you are dealt is a real Megapot ticket NFT costing $1, carrying five normal numbers and a bonus ball, bought through Jackpot.buyTickets with our _source tag on every call. So are the dealer's first two. The one exception is the dealer's tail draws, and section 03 says exactly why.
Those numbers are printed on the card because they are the ticket's actual picks, read from NFT state. Whatever tickets you are holding when the drawing fires are live entries in that night's jackpot.
Because every card costs a dollar, hitting is never free. This is blackjack where every card you take raises the bet.
The hole card
The dealer's hole card is drawn as an Inco Lightning encrypted handle with no access grant issued to anyone. Not to you, and not to the house. There is no key held anywhere that could read it before it is revealed.
Its ticket numbers are still public, because NFT state is public. You can see the dealer's lucky numbers all game. You just cannot see what card it is. That gap is the whole product, and it is why the dealer's total shows as 9 + ? rather than being hidden.
At resolve the handle is revealed on chain and the covalidator returns a signed attestation binding that value to that handle, checked by e.verifyDecryption. A valid attestation for a different card cannot be substituted. From that point the dealer plays out in ordinary plaintext Solidity.
Precisely: this is a covalidator attestation, not a zero-knowledge proof. Inco is confidential compute. We say attestation because that is what it is.
What the pot counts
Every card you are dealt is a real Megapot ticket. So is the dealer's up card and its hole card. Those four are bought at deal time, and every card you take on a hit is bought when you take it.
The dealer's tail draws — the cards it turns over after you stand — are cards from the encrypted shoe with no ticket behind them. The dealer draws to seventeen using handles committed before your decision, and committing a handle costs an Inco fee, not a dollar. This is deliberate: the dealer might use none of them, and buying tickets for cards that never get dealt would burn money on nothing.
The pot counts tickets that exist. It rises when you hit, never when the dealer draws. Cards without a ticket are drawn without one on screen too, so you can always see which is which.
Why every card costs a signature
A card is a real on-chain asset, so dealing one is a real transaction. That is why a hand asks you to sign more than once: deal, each hit, and stand are separate actions that move real USDC and mint real tickets.
What we would not do is hand the tickets to a relayer or a throwaway key we control. Then the winning entries belong to an address that is not yours, and “these tickets are yours” stops being true. That is the line, and it is why there are prompts.
The fix is an embedded wallet, and it is next. Privy-style embedded wallets — the same mechanism behind one-tap play in Base App — are provisioned for you and can sign within a scoped session without a popup per action. The part that matters here: an embedded wallet is exportable. The private key is yours, you can take it out, and tickets mint to your own address. So it buys the frictionless UX without moving ownership anywhere — which is exactly the trade a relayer or a burner key fails. Session keys scoped to this table are the same idea with a narrower grant.
That is a frontend change, not a contract change, and it is the first thing after the jam. It is not in v1 for time, not for architecture.
A prepaid balance — deposit once, play against it, withdraw whenever — is the other half of the roadmap and does need a contract change: deal() pulls USDC from msg.sender on every hand, and these contracts were frozen and deployed before the deadline. Both paths keep the tickets yours; neither is a reason to pretend the current signature count is ideal.
House rules
The rules the contract enforces. Nothing here is aspirational.
| Deck | Infinite shoe, drawn with replacement from Inco encrypted randomness |
|---|---|
| Natural blackjack | Pays 6:5, two cards only. A three-card 21 is a plain win |
| Dealer on soft 17 | Stands. hitSoft17 exists on chain and is off |
| Doubling | Any two cards, no total restriction |
| Double after hit | Not allowed |
| Splitting / insurance / surrender | Not offered |
| Push | Each side keeps its own |
| Busting | Permitted after a bust. It only costs you more on a hand you already lost |
| Abandoned hands | Force-stood, then resolved on merits. The house never seizes a pot |
Doubling was originally restricted to hard 9, 10 and 11 as a house-edge lever. It was dropped: enforcing it would mean reading your total at settle, long after you acted, then either voiding the hand or silently reclassifying the double as a hit — producing a hand you never played. The edge is already carried by 6:5, S17 and the infinite shoe.
Selling tickets back
Win, and you keep the tickets. They are yours, they are transferable, and they are live entries in the drawing. If you would rather have the dollars now, the house will bid for them.
The bid is below face and always will be, because a ticket is worth less than a dollar — that is simply what the expected value is. So the offer is shown next to the live EV it is priced from, computed on chain by TicketMath from the drawing's actual tier payouts and ball bounds.
Against face, a $0.65 bid looks like a 35% haircut. Against holder value — EV net of the 10% referrer win share, which is the number the bid is actually set against — it is about 6%. Both numbers are on screen so you can check the arithmetic instead of trusting it.
Nothing about this is hardcoded. Tier payouts, ball bounds and referral rates are all owner-settable and change between drawings, so they are read live from getDrawingState every time.
At the drawing
Tickets belong to a specific drawing and are worth nothing after it. So the table closes a buffer before the drawing fires, and a hand cannot straddle the boundary.
Any hand still open at close is force-stood and resolved on its merits — the house does not seize it. If a hand is stuck awaiting an attestation past its deadline, it is pushed and each side keeps its own; past the drawing, when tickets are worthless, your share settles in USDC at the standing price instead of returning dead paper.
After the drawing, winnings are claimed against whoever holds the ticket. Megapot gates claimWinnings on the current NFT holder, not the original buyer, so winnings follow the token. If you won it, it is yours.
A ticket is only good for its own drawing. Holding one afterwards is holding a receipt: it cannot be entered into the next drawing, and if it won, the winnings sit unclaimed until someone claims them. Nothing expires your right to claim — but nothing claims it for you either.
Claiming, including tickets you were given
Winnings follow the token, but Megapot's own site keys its ticket list to the address that bought the ticket, and that list does not update when a ticket moves. So a ticket you won at this table — bought by our vault, transferred to you — is claimable by you on chain while being effectively invisible to you there.
The claim page exists to close that gap. It asks the NFT contract what you actually hold, checks each ticket against the drawing's winning numbers, and calls claimWinnings as the holder. It is not a Hole Card feature in any real sense — it works for any Megapot ticket in your wallet, however it got there.
Measured, not asserted: on drawing 140 this claimed two tickets worth $4.377857 gross and $3.940071 landed. The difference is the 10% referrer win share described in section 06 — exactly 90%, to the last decimal place.
The house, and what the house can do
The house is a contract with a visible balance, not a company. Every card the dealer is dealt is bought with the vault's own USDC, and the vault must cover its side of a hand before the hand can open — so the table stops offering hands when the bankroll cannot back one, and says so instead of letting the transaction revert. That figure is read live on the table; this is a jam-scale bankroll, and it is meant to be looked at rather than described.
It follows that a big enough win closes the table for a while. When the house loses, it loses tickets rather than dollars, and it cannot turn those back into dollars until the drawing settles and the winnings are claimed. That is a real constraint, not a maintenance window.
What we will not claim is that the house cannot be drained. The game logic cannot drain it: nothing in a hand moves tickets or USDC out except releasing a pot to a winner, and withdrawBankroll cannot touch reserved funds. But setTable is owner-callable and does not require its argument to be a contract, so the owner can point table at an address they control and then call the table-only paths directly. That is an admin key, it is normal for a jam build, and it emits a TableSet event when used. You should not have to take our word for the shape of the risk — that is the shape of it.