A jackpot changes more than the payout animation. Calculate the risk quotes and tiered reserves that shape slot fees, wager limits, and vault exposure.
Why this matters
Section titled “Why this matters”Slot paytables are heavy-tailed: a tiny probability mass at huge multipliers dominates variance. The facet reads two quotes at openSession:
quoteCaps— per-session escrow / reserved-profit ceilings.maxReservedProfitis held to the single-win cap.quoteRiskParams— the four figures that set the session’s fees, its bet-size cap and its share of the aggregate vault reserve (VaR + jackpot tier).
A quote that fails a guardrail is refused at whitelisting and at every openSession. A quote that passes and is still wrong pays fees on the wrong edge, gets the wrong maximum wager, and risks InsufficientPortfolioReserve at openSession.
This document covers the quote and the reserve. The fee split, the bet-size cap and their errors are in Contract Constraints → What your quotes decide.
What to return for a typical single-spin slot
Section titled “What to return for a typical single-spin slot”Align with the interface in ICasinoGameV2.sol:
maxPayout: Largest total payout for the round (e.g.wager * topMultiplier), never belowexpectedPayout. Used for the bet-size cap, the high-multiplier floor and heavy-tail detection.probabilityWad: Probability that the round pays exactlymaxPayout, in WAD (1e18 = 100%): the jackpot (top) tier alone, not the chance of any win. The facet +VaultReserveLibuse this for tiered jackpot pooling whenisHeavyTailis true.- Must be ≤ 1 WAD.
expectedPayout: RTP ×wager, the mean payout of the round. In a game with player choices, quote the RTP under the player’s best policy. The edge,wager − expectedPayout, is what every fee of the session is cut from: on the fictional paytable below (RTP 92%) a 10% game dev cut pays you 0.8% of every wager, won or lost. The LP income cut from the same edge is all the reserve expects the vault to keep of the session.bodyVarianceScaled: the body variance — the variance of the round’s payout distribution with the top tier excluded, per bet, in wei² × 1e18 (see below). Return0only if the top tier is the game’s sole winning outcome (coinflip-style). A multi-tier paytable that returns0is under-measured: for a rich slot the body is ~89% of the variance.
Body variance
Section titled “Body variance”The protocol pins one variance figure per bet at openSession:
pinnedVariance = bodyVariance + p_top · (1 − p_top) · maxPayout²- The top-tier binary term is computed on chain from your
probabilityWadandmaxPayout. - The body variance is yours to supply. It is counted for every bet, heavy-tail or not.
- A quote is checked against guardrails at whitelisting and again at every
openSession. Each has its own error:QuoteFiguresLib__RtpOutsideBand— RTP (expectedPayout / wager) is outside the council-set RTP band, 60% to 99.9% by default.QuoteFiguresLib__MaxPayoutBelowExpectedPayout—maxPayoutis belowexpectedPayout.QuoteFiguresLib__BodyVarianceMissing— the body variance is0althoughexpectedPayoutis aboveprobabilityWad × maxPayout / 1e18, so tiers below the top one pay.QuoteFiguresLib__VarianceNotPositive— the plain variance of the whole payout is not positive. It goes negative when several tiers win and the body variance is quoted far too low.
- The plain variance formula and what whitelisting quotes are in Contract Constraints → Guardrails.
- A body variance quoted too low can still pass. It understates the game’s risk and raises its bet-size cap above what the vault should accept, so quote it truthfully: the
quote-figurescheck ofgame-verify, run on every build you push, compares the quoted RTP and variance with sampled play.
Units. The reserve stores variance in wei² × 1e18 so that sqrt(sum / 1e18) is in wei. For a body standard deviation of σ_body tokens per unit wager:
uint256 bodySigma = Math.mulDiv(wager, bodySigmaWad, 1e18); // weibodyVarianceScaled = bodySigma * bodySigma * 1e18;where bodySigmaWad = σ_body × 1e18. Equivalently, from the paytable (unit wager, multipliers M_k, probabilities p_k, top tier excluded):
σ_body² = Σ_{k ≠ top} p_k · M_k² − (Σ_{k ≠ top} p_k · M_k)²Heavy-tail regime (on-chain)
Section titled “Heavy-tail regime (on-chain)”VaultReserveLib.isHeavyTail is roughly:
maxPayout / wager >governance multiplier threshold (default 100), andprobabilityWad <governance probability threshold (default 0.1% = 1e15 WAD).
Then the tiered model applies: explicit jackpot reserve + body-only VaR instead of naive normal VaR on the full tail. The top-tier binary term is swapped out of the VaR because the jackpot tier covers it; the body variance stays in.
Example paytable (fictional)
Section titled “Example paytable (fictional)”Single-spin game: outcome is one row; payout = wager × multiplier (0 = loss). Probabilities sum to 1. RTP ≈ 92% (house edge 8%).
| Tier | Multiplier M | Probability p | RTP contribution p·M |
|---|---|---|---|
| Jackpot | 5 000× | 2×10⁻⁶ (1 in 500 000) | 0.01 |
| Major | 200× | 5×10⁻⁵ | 0.01 |
| Big | 50× | 0.002 | 0.10 |
| Small | 10× | 0.05 | 0.50 |
| Mini | 2× | 0.15 | 0.30 |
| Loss | 0× | 0.795948 | 0.00 |
| Σ | 1.000000 | RTP = 0.92 |
Checklist for risk:
maxPayout / wager= 5 000 → above default multiplier threshold 100.- Jackpot probability = 2×10⁻⁶ < 0.001 (10⁻³) → below default probability threshold when expressed in WAD (2×10⁻⁶ × 1e18 = 2×10¹² WAD, and 1e15 WAD = 0.1% — 2×10¹² < 1e15). So
isHeavyTailis true with defaults: tiered reserve applies on-chain.
Example: tiered VaR (how the protocol uses your quote)
Section titled “Example: tiered VaR (how the protocol uses your quote)”Numbers are illustrative (governance multipliers, N, and w are examples).
Given
- Top tier: multiplier Mmax = 5 000, probability pJ = 2×10⁻⁶ (this is what you expose as
probabilityWad=2e12in WAD terms:p_J * 1e18). - Suppose N = 100 concurrent heavy-tail sessions on the vault, all on this game, w = 10 tokens per spin (largest active wager wmax = 10 for the illustration).
- Confidence z = 3.72 (the portfolio default,
confidenceMultiplierBps = 372→ 99.99%). It drives the Tier-2 VaR. The jackpot count below holds the same 99.99%.
Tier 1 — jackpot reserve
A vault’s heavy-tail sessions share one jackpot tier, whatever their game or gameData. The tier holds the vault’s largest open jackpot liability, maxPayout - wager, times a count of simultaneous jackpots. The count is Poisson with mean λ: each session’s top-tier probability weighted by its jackpot over the largest, summed. With every jackpot equal, as here, λ = N · pJ. The reserve holds the smallest count kj whose Poisson probability of at most kj jackpots is 99.99% or more, floored at 1 and clamped to N (you can never have more concurrent jackpots than open bets):
Above an expected count of 30 the count is the normal bound with its skew correction:
So one full jackpot-sized loss to the vault is covered before applying normal-VaR on the rest of the distribution. Unlike a flat coverage factor, this self-adjusts to traffic: as N (hence λ) grows, the reserved jackpot count rises with it at the same 99.99%, and it never exceeds the live session count.
Tier 2 — body (sub-jackpot) mass
The jackpot tier contributes pJ·Mmax² = 2×10⁻⁶ × 5 000² = 50 to the second moment of the multiplier. The protocol swaps that binary term out of the VaR (the jackpot tier covers it) and runs the portfolio VaR on the body variance you quoted in bodyVarianceScaled. For the fictional paytable, per unit wager and top tier excluded:
so with w = 10 the body standard deviation is ≈ 3.43 × 10 = 34.3 tokens per spin and, over a six-decimal token such as USDC, the quote is bodyVarianceScaled = (34.3e6)² × 1e18. If you return 0 here, the Tier-2 VaR sees no variance at all for this slot.
The VaR is z · σ of the open sessions less what the vault expects to keep of them. That is the LP income each session pinned at openSession: the LP cut’s share of the edge left after the game dev fee. The game dev fee, the protocol fee and the operator fee are paid out whatever the outcome, so the reserve does not count them. With a 10% game dev cut and the LP cut at 35%, a 10-token spin at 92% RTP pins 10 × 8% × 90% × 35% = 0.252 tokens:
Contrast — naive worst case
100 spins × worst loss ≈ 100 × (5 000 × 10) = 5 000 000 tokens reserved if you reserved full worst-case per seat — the tiered model avoids that by separating the rare jackpot from the bulk of the paytable.
Implementing quoteCaps
Section titled “Implementing quoteCaps”For a single-spin slot, the player posts wager once; there is no extra stake mid-round.
maxEscrowStake: at leastwager(typicallymaxEscrowStake = wager).maxReservedProfit: worst-case vault liability above the wager for that round:max(0, maxPayout - wager)when the worst case is a single-line jackpot onwager.
If gameData selects paylines or denomination, decode it the same way in quoteCaps and quoteRiskParams so caps and risk always match the round you will run in onSessionStart / onRandomness.
function quoteCaps(uint256 wager, bytes calldata gameData) external view returns (uint256 maxEscrowStake, uint256 maxReservedProfit){ Paytable memory t = paytableFor(gameData); uint256 maxPayout = wager * t.jackpotMultiplier; // e.g. 5000 * wager for top tier maxEscrowStake = wager; maxReservedProfit = maxPayout > wager ? maxPayout - wager : 0;}(Adjust naming/scaling to your storage; the idea is: maxReservedProfit = peak profit the vault might owe beyond escrowed stake for this session.)
Implementing quoteRiskParams
Section titled “Implementing quoteRiskParams”Contract surface (from ICasinoGameV2):
maxPayout: worst-case token payout for the round (same units aswager).probabilityWad: pjackpot × 1e18 for the top tier only (the tier with largest multiplier): the probability of exactlymaxPayout.expectedPayout: RTP × wager in token units, i.e. for one spin. It sets the session’s edge and so its fees.bodyVarianceScaled: the body variance of one spin with the top tier removed, in wei² × 1e18 (see Body variance).0only for a single-tier game.
Worked mapping for the fictional paytable (one spin, wager = w):
| Return field | Value |
|---|---|
maxPayout | w * 5000 |
probabilityWad | 2e12 — i.e. (2e-6) * 1e18 |
expectedPayout | (w * 9200) / 10000 if RTP is exactly 92% = 9200 BPS |
bodyVarianceScaled | bodySigma² * 1e18 with bodySigma = w * 3.43e18 / 1e18 (σbody ≈ 3.43 per unit wager) |
uint256 internal constant WAD = 1e18;uint16 private constant RTP_BPS = 9200; // 92.00%
// pJackpot = 2e-6 => probabilityWad = 2e12uint256 private constant JACKPOT_PROB_WAD = 2e12;// sigma of the paytable with the jackpot tier removed, per unit wager (WAD): sqrt(11.77) ~= 3.43uint256 private constant BODY_SIGMA_WAD = 3.43e18;
function quoteRiskParams(uint256 wager, bytes calldata gameData) external view returns ( uint256 maxPayout, uint256 probabilityWad, uint256 expectedPayout, uint256 bodyVarianceScaled ){ Paytable memory t = paytableFor(gameData); maxPayout = Math.mulDiv(wager, t.jackpotMultiplier, 1); // e.g. 5000 * w probabilityWad = JACKPOT_PROB_WAD; // must match actual top-tier probability expectedPayout = Math.mulDiv(wager, RTP_BPS, 10_000); uint256 bodySigma = Math.mulDiv(wager, BODY_SIGMA_WAD, WAD); bodyVarianceScaled = bodySigma * bodySigma * WAD;}Invariants you must preserve
probabilityWadis exactly the marginal probability of the highest-multiplier winning outcome, the one that paysmaxPayout— not “any win”, not average win chance. Return the same value for every wager on onegameData: the jackpot tier weights it by the session’s jackpot to count the jackpots that may hit together.expectedPayoutmust match the same paytable you simulate inonRandomness(same RTP). Fees are paid on the edge it implies, whatever the outcome.- If you have multiple
gameDatavariants (different RTP skins), recomputemaxPayout,probabilityWad,expectedPayoutandbodyVarianceScaledfrom the same tables. bodyVarianceScaledexcludes exactly one tier — the one whose probability you return asprobabilityWad— so the on-chain binary term and your body term never double count.
Implementing session phases (typical RNG slot)
Section titled “Implementing session phases (typical RNG slot)”onSessionStart: Decodectx.gameData, commitnewGameState(e.g. pending spin), setnextPhase = WAITING_RANDOMNESS,requestRandomnessNow = true,reservedProfitDeltaper your game’s pattern (seeCoinflipGame/ facet for how reserved profit interacts withquoteCaps).onRandomness: Maprandomnessto an outcome index using a reproducible function (uniform draw over cumulative weights), computepayout, setSETTLED,requestRandomnessNow = false.
onPlayerAction can revert if the slot has no mid-round choices.
Doc drift warning
Section titled “Doc drift warning”ICasinoGameV2 + CasinoGameFacet + QuoteFiguresLib + VaultReserveLib are the source of truth for exact field names and units; this document describes how they use your quote.