How it works
The parts
| Part | Job | Public or sealed |
|---|---|---|
| Purse | Your balance inside Curtain, one per wallet | Sealed: Enc<Shared, u64>, readable by you and the cluster |
| Position | Your stake on one market, both sides | Sealed: Enc<Shared, Stake> |
| Ticket | A copy of one bet for the next tally | Sealed for the cluster alone: Enc<Mxe, Ticket> |
| Batch | Up to 8 tickets added up together | Only the count of tickets and their sums are revealed |
| Market | The question, the pools, the outcome | Public |
Placing a bet
Your browser encrypts two numbers, the side and the amount, with a key it shares with the Arcium cluster (x25519 key exchange, then the Rescue cipher). place_bet carries only those ciphertexts.
Inside Arcium, the place_bet circuit checks the amount against your sealed balance and the market's minimum ticket. If the bet fits, it moves the amount from your purse onto one side of your position and issues a ticket. If it does not fit, nothing moves. The only thing revealed is one bit, accepted, which says the size was out of range and nothing more.
A purse holds at most one ticket per batch. That is what makes the batch size a count of different bettors.
Batches and epochs
Each market keeps an open batch with 8 seats. A permissionless crank, tally, seals the open batch once it holds at least 5 tickets and one of these is true:
- its epoch of 1 minute has run out,
- betting on the market has closed,
- all 8 seats are taken.
A batch below the minimum stays open across epochs until it fills. Sealing asks the cluster for the batch's sums; the callback adds them to yes_pool and no_pool and emits OddsPublished. Nothing smaller than a whole batch is ever revealed.
sequenceDiagram
participant You
participant Solana
participant Arcium
You->>Solana: place_bet(ciphertext side, ciphertext amount)
Solana->>Arcium: queue place_bet
Arcium-->>Solana: callback: purse, position, ticket (all sealed), accepted
Note over Solana: more bettors fill the batch
Solana->>Arcium: tally(batch)
Arcium-->>Solana: callback: batch Yes sum, batch No sum
Solana-->>You: OddsPublished(yes_pool, no_pool)
The final batch
When betting closes, the last batch is tallied if it reached 5 bettors. If it did not, it is dropped: its tickets never enter the pools and those stakes are refunded at settlement.
The print rule
A market settles on the first Pyth print at or after the deadline. Never "the official price": exactly that one print.
settle_market takes a verified Pyth PriceUpdateV2 account. The program checks that the update is for the market's feed, that it was published at or after expiry, and that it is the first print there. A print proves that in either of two ways: the print before it (prev_publish_time) was published before expiry, which only the exact first print can show, or it was published within 15 seconds of expiry.
The second way is the one the prices Pyth pushes onto Solana can take, and it needs nothing but Solana. Pyth pushes its feed accounts at least every ten seconds, so the first push after a deadline always lands inside that window, and once the window has passed the account holds a price the program refuses. Anyone may submit the print and whoever gets there first fixes it, so the window is the whole choice a settler has. The settler gains nothing from submitting.
The outcome is Yes if that price is at or above the strike, otherwise No.
Stale and missing prints
| Case at the deadline | What happens |
|---|---|
| Pyth published no price within 15 s of the deadline | Nothing can settle the market, so every stake comes back through the 7-day refund |
| First print arrives more than 60 s after the deadline | Every stake is refunded |
| Its confidence band is wider than 1% of the price | Every stake is refunded |
| Price not positive, or the feed changed its exponent | Every stake is refunded |
| One side's pool is empty | Every stake is refunded |
| Nobody submits a print within 7 days | Anyone may call refund_market |
The settling push is rebuilt from Solana's own transaction history, so it can be posted long after the deadline and the 7-day refund only fires if nobody cares to settle, or if the feed published nothing inside the window.
Claims
claim is permissionless and pays a settled position into its owner's purse, for winners and losers alike. A crank can claim every position, so the act of claiming says nothing about the side. The payout is computed inside Arcium and stored encrypted.
Withdrawals
withdraw(amount) asks the cluster whether your purse holds the amount. If it does, the callback pays it to a token account you own. The amount and the destination are public.
Pause
The authority can pause new bets and new markets. Tallies, settlement, claims, deposits and withdrawals have no pause.
Next: Security and trust