Skip to main content
A payout table is an array of 1 to 32 outcomes { p, m }:
  • p: probability as a u64 in billionths. Every p is greater than zero, and they sum to exactly 1 000 000 000.
  • m: multiplier as a u64 in basis points of the ticket price. 10 000 is 1×, 0 pays nothing, and the ceiling is 100 000 000 (10 000×).
An outcome’s payout is price × m / 10 000, rounded down, in USDC base units.

The invariant

open_ticket computes, in 128-bit integers with no rounding:
and admits the payout table only if, for the app share f it passes:
That is the margin floor: the declared margin must cover the 100 bps vault margin floor, the 100 bps protocol share and the app’s own share. The program also derives the declared margin it stores in the ticket:
No floating-point value is involved anywhere, and your client should not use one either. Build payout tables in bigint.
Round in the vault’s favour: round probabilities down, and if you round a multiplier up, derive the probability from the rounded multiplier. Put the remainder of 1 000 000 000 on the zero outcome so the sum is exact.

Examples

Win or nothing. For a target payout T on ticket price s at margin e bps:
Three tiers at a 2 % margin. Refused: pays out more than it takes in. [{ p: 500_000_000n, m: 21_000n }, { p: 500_000_000n, m: 0n }] returns 105 % on average. open_ticket fails with PayoutTableOverpays. Refused: fair, but the margin does not cover the split. A payout table with a 1.5 % margin and no app share is fair but leaves the vault less than its 100 bps floor plus the 100 bps protocol share. open_ticket fails with MarginFloor.

How much a vault can back

The vault reserves each ticket’s liability, its worst case net of what the ticket price brought in:
Admission then checks, against the vault as it stands at open: free_balance = assets − reserved liability − open vault credits. The per-ticket cap uses the vault margin, net of shares, so inflating the declared margin does not widen it, and every basis point of app share narrows it. After a drawdown trip the cap is halved for 48 hours.

Sizing a payout

Two consequences matter for an interface:
  1. Only the top multiplier costs capital. A payout table that pays something 90 % of the time can be trivial to back; a single large payout decides what the vault must hold.
  2. The cap bounds the liability, not the multiplier. On a fixed payout table, a cap C admits a ticket price of about C / (max(m) − 1). The same vault that refuses 25 USDC at 1 000× accepts a few cents at 1 000×.
On an idle vault with a 2 % declared margin, the smallest a payout table can declare, and no app share (vault margin 100 bps), the Kelly term binds first: 0.5 × 100 bps is 50 bps of the free balance, below the 150 bps bound on assets. The smallest vault that carries a win-or-nothing ticket is roughly liability / 0.005:
The 2 % minimum returns the most reward per unit of budget and is the most demanding margin for capacity. Its vault margin is 100 bps, so the Kelly term of the per-ticket cap is 50 bps of the free balance, against 125 bps at a 3.5 % margin (vault margin 250 bps). For the same liability, every vault size here is 2.5 times what a 3.5 % payout table needs (Kelly term 50 bps against 125 bps), and a given vault backs a top payout 0.4 times as large: large top payouts at the minimum margin need a larger vault. Above a 4 % margin with no app share, the 150 bps bound on assets binds first on an idle vault.
As the vault fills up, the free balance shrinks with it, so the largest payout the vault accepts falls as utilisation rises.

Show the limit, do not hit it

Read Config (for the parameters) and Vault (for totalAssets, totalReservedLiability, openVaultCredits) and apply the checks above in integers to show users the largest payout, or the largest ticket price, the vault accepts right now. Then simulate the transaction before asking for a signature: the program is the authority, and the vault can move between your read and the user’s click.

Common refusals

The full list is on Errors.