Skip to content

Pricing & Payout Math

All probabilities and payouts are fixed-point integers. 1 USDC = 1,000,000 micro-USDC, and a probability of 50% is stored as 500_000.

PROB_SCALE = 1e6 per-leg probabilities, and USDC
HIGH_SCALE = 1e12 the combined-probability accumulator

For a leg betting selected_outcome on market M:

price_yes = lmsr::price_yes(M.q_yes, M.q_no, M.b) // 0 … 1_000_000
prob = match selected_outcome {
Yes => price_yes,
No => 1_000_000 − price_yes,
}
require config.prob_floor_bps ≤ prob ≤ config.prob_ceiling_bps

That value is written into parlay.legs[i].probability_at_entry and never recomputed. You are paid at the multiplier you locked, whatever the market does afterward.

The band is admin config in basis points of PROB_SCALE (2000 bps = 20% = 200_000 units), bounded by the hard constants HARD_PROB_FLOOR_BPS = 100 (1%) and HARD_PROB_CEILING_BPS = 9900 (99%). Out-of-band legs fail with ProbabilityOutOfRange (6014). The examples below use a 5% floor for illustration. → Leg Eligibility

The spot price_yes above feeds a one-hour TWAP, which is what actually gets locked — not the instantaneous LMSR price. That is what makes a leg expensive to manipulate. → CLOB Markets as Legs

const HIGH_SCALE: u128 = 1_000_000_000_000;
const PROB_SCALE: u128 = 1_000_000;
let mut combined = HIGH_SCALE; // 1.0 in HIGH_SCALE units
for prob in legs.probs {
combined = (combined * prob) / PROB_SCALE; // stays in HIGH_SCALE
}
require!(combined > 0); // collapse guard

Accumulating in the legs’ native 1e6 scale truncates at every step, and the error compounds in the trader’s favour. A 4-leg slip at 5% each:

StepprobUnder PROB_SCALEUnder HIGH_SCALE
0—1_000_0001_000_000_000_000
150_00050_00050_000_000_000
250_0002_5002_500_000_000
350_000125125_000_000
450_0006 (true: 6.25)6_250_000 (exact)

Multiplier is HIGH_SCALE / combined:

ScaleMultiplierError
PROB_SCALE166,666×+4.17% overpay
HIGH_SCALE160,000×exact

A 4% systematic overpay on every longshot slip is the difference between a solvent vault and a drained one.

combined > 0 catches the case where truncation flattens the accumulator to zero. Enough legs at a low floor would do it. Failing loudly beats emitting a junk multiplier from a division by a truncated value — so the probability floor plus this guard is what practically caps leg count at deep-longshot probabilities.

The hard floor of 1% is set exactly where the comment in config.rs puts it: below that, per-leg multipliers blow up — a 1% leg is 100×, so two of them is already 10,000×.

combined ≤ 1e12
combined × prob ≤ 1e12 × 1e6 = 1e18 u128::MAX ≈ 3.4e38 ✓
stake × HIGH_SCALE ≤ 1.8e19 × 1e12 = 1.8e31 ✓
let potential_payout_u128 = stake * HIGH_SCALE / combined;
let potential_payout = u64::try_from(potential_payout_u128)?;
require!(potential_payout <= config.max_payout); // PayoutExceedsMax
require!(potential_payout >= min_payout); // SlippageExceeded

That u64::try_from is the hard ceiling. Together with max_payout, it is the program’s only protection against a multiplier explosion from a degenerate combined.

stake $10 = 10_000_000 micro-USDC
legs 50%, 40%, 25%
combined = 1e12
× 500_000 / 1e6 = 500_000_000_000
× 400_000 / 1e6 = 200_000_000_000
× 250_000 / 1e6 = 50_000_000_000
payout = 10_000_000 × 1e12 / 50_000_000_000
= 200_000_000 → $200 (20×)

When a leg’s market cancels, the leg goes Voided and is divided out of the multiplier:

new_payout = old_payout × prob_i / PROB_SCALE

The algebra: the original payout was stake × HIGH_SCALE / ∏ pⱼ. Removing leg i — treating it as a 1× contribution — gives

stake × HIGH_SCALE / ∏_{j≠i} pⱼ
= (stake × HIGH_SCALE / ∏ pⱼ) × pᵢ / PROB_SCALE
= old_payout × pᵢ / PROB_SCALE

Continuing the example, if the 25% leg cancels:

new_payout = 200_000_000 × 250_000 / 1_000_000
= 50_000_000 → $50 (5×)

You keep a slip on the two surviving legs at their honest 5× multiplier.

Down-rounding from integer division is biased toward the house — the recomputed payout is at most one micro-USDC below the true value — which only strengthens vault solvency. total_exposure decreases by old_payout − new_payout.

fee = floor(potential_payout × fee_bps_at_create / 10_000)
fee = min(fee, potential_payout)
net = potential_payout − fee

fee_bps_at_create is snapshotted into the Parlay account at create time. An admin update_config between create and claim cannot change what a winning user pays. The min is belt-and-braces against a config that somehow exceeded the 10% cap.

There is no fee on the stake — only on a winning gross payout.

create total_exposure += potential_payout
leg voided total_exposure −= (old_payout − new_payout)
slip lost total_exposure −= potential_payout (immediately)
all voided potential_payout reset to stake; exposure follows
claim total_exposure −= potential_payout
admin void total_exposure −= potential_payout

Releasing the full exposure the instant a slip goes Lost is conservative — it can no longer pay out, so the reservation is dead weight.

→ Vault & Solvency

OperationDirectionWho benefits
Combined-probability accumulationtruncateshouse (payout rounds down)
potential_payout u64 conversiontruncateshouse
Cancelled-leg recomputetruncateshouse
Feefloortrader

Every material rounding decision favours the vault by at most one micro-USDC, and the 20% solvency buffer is sized to absorb the accumulated drift.