CurtainMarketsDocs
curtain/docs/how-it-worksLaunch app

How it works

The parts

PartJobPublic or sealed
PurseYour balance inside Curtain, one per walletSealed: Enc<Shared, u64>, readable by you and the cluster
PositionYour stake on one market, both sidesSealed: Enc<Shared, Stake>
TicketA copy of one bet for the next tallySealed for the cluster alone: Enc<Mxe, Ticket>
BatchUp to 8 tickets added up togetherOnly the count of tickets and their sums are revealed
MarketThe question, the pools, the outcomePublic

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:

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 deadlineWhat happens
Pyth published no price within 15 s of the deadlineNothing can settle the market, so every stake comes back through the 7-day refund
First print arrives more than 60 s after the deadlineEvery stake is refunded
Its confidence band is wider than 1% of the priceEvery stake is refunded
Price not positive, or the feed changed its exponentEvery stake is refunded
One side's pool is emptyEvery stake is refunded
Nobody submits a print within 7 daysAnyone 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