Risk Controls
This page describes the risk controls that are live on the house-banked parlay book, and the operational policy around them. It is written for whoever runs the vault.
Where the risk sits
Section titled “Where the risk sits”Every open slip is a short option written by the vault. The payout is fixed
at create time, so the vault’s loss on a winning slip is
potential_payout − stake, known in advance and reserved in full.
total_exposure = Σ potential_payout over all Active and Won-unclaimed slipsThe exposure is reserved at full notional the moment a slip is created — there is no netting, no expected-value discount. A 20× slip reserves 20× the stake even though it is 5% likely to pay.
That conservatism is the point: the vault’s solvency doesn’t depend on any probability estimate being right.
Control 1 — the 80% rule
Section titled “Control 1 — the 80% rule”total_exposure + new_potential_payout ≤ 0.8 × vault.amountChecked on every create_parlay. Failing it rejects the slip with
ExposureLimitExceeded (6011).
Consequences to plan around:
- Headroom is a hard ceiling, not a soft target. There is no queue and no partial fill.
- Large slips fail before small ones. As headroom shrinks, users experience apparently random rejections — big bets fail while small ones go through.
The vault’s current balance and reserved exposure are public:
curl -s https://prod-api.gomarket.io/api/parlays/vault/The API is a mirror maintained from webhooks; on-chain state is authoritative. → Parlays API
Control 2 — per-slip caps
Section titled “Control 2 — per-slip caps”| Cap | Field |
|---|---|
| Max gross payout per slip | config.max_payout |
| Max stake | config.max_stake |
| Min stake | config.min_stake |
| Max legs | config.max_legs (≤ 10) |
max_payout is the tail-risk control: it bounds the single worst outcome
regardless of what multiplier the math produces. Combined with the
u64::try_from on the payout computation, it is the program’s only
protection against a multiplier explosion from a degenerate combined
probability.
Control 3 — the probability floor
Section titled “Control 3 — the probability floor”config.prob_floor_bps ≤ prob ≤ config.prob_ceiling_bpsBoth ends are on-chain and configurable, bounded by hard constants at 1%
and 99%. Two jobs. It stops the 1e12 accumulator truncating to zero on deep-longshot
slips (CombinedProbabilityZero, 6041), and it caps how extreme a multiplier
can be produced at all — ten legs at the floor would fail the guard rather
than emit a junk number.
The ceiling stops legs that contribute nothing: a 98% leg adds 1.02× to the payout while adding a real 2% chance of killing the slip — bad for the user and pure tail risk for the vault. It is enforced on-chain, not merely in the client.
Control 4 — correlation policy
Section titled “Control 4 — correlation policy”The on-chain guard is blunt: two legs from the same event are rejected
(SameEventCorrelation, 6005), read from the MarketEventLink PDA at create
time.
It works because the multiplier ∏ pᵢ assumes independence. “Team A wins”
(60%) and “Team A wins by 2+” (35%) multiply to 21% — a 4.8× payout — when
the real joint probability is around 35%, worth 2.9×. Left open, a maker
farms that gap until the vault is empty.
The limit of the guard
Section titled “The limit of the guard”The check is per event, so it catches the correlation that comes from two
legs describing the same contest. Correlation that spans events — two
matches with a shared dependency, two markets on the same underlying driver
— is separate event_ids and passes.
That boundary is deliberate: an on-chain check has to be cheap and
deterministic, and event_id equality is both. Correlation beyond it is
handled by how events are curated rather than by the program, which is why
event grouping is a risk decision and not just a navigation one.
Control 5 — pausing
Section titled “Control 5 — pausing”is_paused = true → create_parlay rejected with Paused (6000)Scope is deliberately narrow: pausing stops new risk only. Settling and claiming continue to work, and existing slips resolve and pay out normally.
Pausing is not a freeze on user funds, and shouldn’t be operated as one. Use it when the pricing inputs are untrustworthy — a market data outage, a suspected manipulation, a correlation discovered after the fact — and let the existing book run off.
Control 6 — TWAP pricing
Section titled “Control 6 — TWAP pricing”Legs price off a one-hour TWAP, not spot. → CLOB Markets as Legs
The attack it blocks: push a thin market to an extreme, open a large parlay at the distorted probability, push it back. Against a time-weighted average that requires holding the distortion for a meaningful fraction of an hour, against anyone willing to trade the other side.
Supporting guards: min_market_age_secs (validated to be ≥ the TWAP
window, so an admin cannot set it below the averaging period even by
mistake), min_market_b, min_market_volume, and the explicit
TwapWindowTooShort / TwapTooStale checks.
Treasury and custody
Section titled “Treasury and custody”| Account | Role |
|---|---|
| Vault | Holds stakes, pays winners. Authority is the PDA itself. |
| Treasury | Receives fees at claim. config.treasury. |
| Admin | Signs config changes, vault seed/withdraw, voids. |
In production the admin is a Squads multisig. The backend never signs parlay transactions — admin actions go through the multisig.
What the admin cannot do
Section titled “What the admin cannot do”| Why | |
|---|---|
Rotate amm_program_id | Absent from update_config. A rotated program id would let an admin point the parlay at a malicious AMM whose Market accounts decode to chosen outcomes. Requires a new program deployment. |
| Redirect a refund | void_parlay’s destination is bound to parlay.user by an equality check and a token::authority = user constraint. |
| Change a placed slip’s fee | fee_bps_at_create is snapshotted. |
| Drain reserved funds | withdraw_vault caps at vault.amount − total_exposure. |
| Settle or claim for a user | settle_leg is permissionless but outcome-determined; claim_parlay needs the user’s signature. |
update_config applies to future parlays only. Every placed slip carries its
own snapshot of what matters.
Reading the vault yourself
Section titled “Reading the vault yourself”Everything the solvency rules depend on is public:
curl -s https://prod-api.gomarket.io/api/parlays/vault/| Field | Meaning |
|---|---|
balance | USDC in the vault |
total_exposure | Sum of potential_payout across open, unclaimed slips |
max_payout_cap, min_stake, max_stake, max_legs | The configured caps |
Headroom for new slips is 0.8 × balance − total_exposure. When it nears
zero, large slips start failing with ExposureLimitExceeded while small ones
still succeed — that’s the 80% rule working, not an error.
The API is a mirror maintained from webhooks; the on-chain accounts are authoritative. → Parlays API · Contracts & Audits