Parlay Additional Information

1. The exchange model

The Parlay service is a request-quote-confirm exchange. A bettor submits a multi-leg parlay; market makers (SPs) respond with offers; the bettor's confirmation triggers a matcher that selects among the offers; matched orders settle after the underlying events resolve.

Bettor                 Parlay Service                 Market Maker (SP)
  │                          │                                  │
  │ POST /parlays/request    │                                  │
  │─────────────────────────▶│                                  │
  │     { parlayId }         │── price.ask.new ────────────────▶│
  │◀─────────────────────────│                                  │
  │                          │◀── POST /sp/orders/offers ───────│
  │                          │       (one or more offers)       │
  │◀── price.offers.new ─────│                                  │
  │      (aggregated)        │                                  │
  │                          │                                  │
  │ POST /parlays/confirm    │                                  │
  │─────────────────────────▶│                                  │
  │     { parlayId, stake }  │                                  │
  │◀─────────────────────────│                                  │
  │                          │── price.confirm.new ────────────▶│
  │◀── order.matched ────────│                                  │
  │      (preliminary)       │                                  │
  │                          │◀── POST /sp/orders/confirmations │
  │◀── order.finalized ──────│                                  │
  │      (terminal signal)   │                                  │


2. Matching engine semantics

The matcher is the most non-obvious piece of the platform. Both audiences need to understand it.

Single-tier per matching round

Within one matching round the matcher selects only offers at the single best-scored odds tier. So if the live pool contains [+6789 $2, +4905 $203], round 1 selects only +6789 $2. The +4905 offer is ignored for that round.

Within a tier: parallel across SPs

If multiple SPs sit at the best-scored tier, the matcher selects them all and dispatches price.confirm.new to every selected SP simultaneously, followed by order.matched broadcasts in batch. There is no per-SP sequencing within a tier.

Why "tied" offers don't always all win

When multiple offers appear to share the same price, the matcher still ranks them deterministically and takes them in order until the bettor's stake is covered. Anyone past that cutoff gets nothing, even at the same displayed odds.

Ranking uses a composite score:

  1. Odds — primary factor.
  2. MaxRisk — secondary tiebreaker (larger offer ranks higher).
  3. Submission time — tertiary tiebreaker (earlier submission wins).

The matcher locks to the top-ranked offer's Odds value and only takes other offers whose Odds is exactly equal at full numeric precision, accumulating MaxRisk until the stake is covered, then stops.

Practical consequences for SPs whose offers appear to "tie" the winner but don't fill:

FactorEffect on selection
Fractional odds gapMost common cause. Odds are compared at full floating-point precision, not at the displayed integer. Two offers that both display as e.g. "+561" can be 5.6105 vs. 5.6098 underneath, and the higher value wins outright on the primary key — no tiebreaker is consulted.
MaxRiskIf odds are exactly equal at full precision, the larger MaxRisk ranks first.
Submission timeOnly matters when both Odds and MaxRisk are exactly equal — earlier submission wins.
Random selection / SP rotation / fairness layerNone exist. Ranking is purely deterministic.
SP reputation or fill historyNot a factor. The only inputs are odds, MaxRisk, and timestamp.
Split-fillsReal. If the top SP's MaxRisk is smaller than the bettor's stake, the next-ranked SPs fill the remainder in score order until the stake is covered. Offers past that cutoff are not filled.

When investigating a "tied but didn't fill" case, check in this order:

  1. Compare the full-precision odds values, not the displayed integer.
  2. If truly equal, compare MaxRisk.
  3. If those also match, compare submission time.

If you have a specific parlay ID where this happened, ProphetX can look up the raw offer data and confirm which axis decided the order.

Across tiers: sequential, gated on full accept

After every SP confirmation, the platform tracks pending orders for the round. When all SPs in the current round have responded, if the round was a full accept, the matcher runs again on the remaining stake and selects the now-top tier.

Round-N outcomeTrigger re-match?
Full accept by all SPs at the tierYes — next-best tier gets price.confirm.new
Partial accept (confirmed_stake < requested_stake)No — finalize and refund the remainder
RejectNo — finalize and refund the remainder
Timeout (no callback)No — finalize and refund the remainder

So tiers do traverse sequentially across rounds, but only when each round fully accepts. Any short fill, reject, or timeout terminates the chain.

Live pool, not snapshot

The matcher selects against the live offer pool at confirmation time, not against whatever offers the bettor saw earlier. A better-priced offer that lands between offers-fetch and /parlays/confirm will be used. Implications:

  • Bettors can experience price improvement beyond the UI display.
  • Market makers that rev a quote with better odds before confirmation get picked over older quotes from themselves or competitors.

Latest quote supersedes earlier

When a single SP submits multiple quotes for the same parlay, the matcher only considers the most recent one. Older quotes from the same SP stay in the pool but are skipped at match time.


3. valid_until constraints

Every offer carries a valid_until (Unix nanoseconds). Server-enforced bounds at offer submission:

  • Min: 5 seconds in the future ("valid_until must be greater than 5s").
  • Max: 10 minutes in the future ("valid_until must be less than 10m0s").

There is no expiry event broadcast when an offer becomes stale. Expired offers are silently filtered out at match time. Bettors / UIs must run client-side timers off valid_until; SPs must rev quotes if they want extended validity.


4. WebSocket events

EventAudienceMeaning
price.ask.newSPNew parlay request — quote it.
price.offers.newUser/ISVAggregated SP offers (per-odds buckets).
price.confirm.newSPConfirmation requested for a selected offer.
order.matchedpublic + SPPreliminary. Matcher selected a counterparty; SP hasn't confirmed yet.
order.finalizeduser + SPTerminal per-order. SP callback resolved (accept or reject).
order.settleduser + SPWallet payout posted.
parlay.processinguserMatcher running.
parlay.finalizeduserTerminal per-parlay. All orders resolved.
parlay.settleduserAll settlement complete.
refund.processeduserRefund posted (e.g. unmatched stake).

order.matched is preliminary

order.matched fires the moment the matcher selects a counterparty — at the same time price.confirm.new is dispatched to the SP, before the SP has confirmed via callback. The order's persisted status at broadcast time is sent_confirmation, not matched. For terminal "trade is done" signals, listen for order.finalized / parlay.finalized.


5. Status enums

Order statuses:

StatusMeaning
inactiveOrder created, confirmation not yet sent.
sent_confirmationprice.confirm.new dispatched to SP.
openUser-side order, money deducted, awaiting match.
matchedSP callback accepted.
finalizedReady for settlement.
pending_payoutSettlement calculated, payout queued.
payout_submittedPayout sent to wallet.
settledWallet transaction confirmed.
rejectedSP rejected, or insufficient liquidity.
void / failedTerminal failure.

Offer statuses: pendingconfirmingpartial_confirmed / confirmed / rejected.

Match statuses (parlay-level): pendingfinalized / failed.

Settlement statuses (parlay-level): tbdwon / lost / push / void.


6. Common errors

error codeHTTPWhen
invalid_params400Validation failed.
invalid_legs_combination400Legs not accepted (correlated, banned, etc.).
no_valid_market_lines400One or more legs reference unsupported lines.
parlay_already_exists400Re-submitting a confirmed parlay.
parlay_not_valid400Parlay expired or not found.
confirmation_arrive_too_late400Confirm callback after the offer's valid_until.
order_not_valid400Confirming an order in the wrong state.
wallet_insufficient_funds / insufficient_balance400 / 402User / ISV wallet rejected.
unauthorized401Missing/invalid auth header.
sp_banned403SP on a ban list.
not_found404Resource lookup failed.
request_timeout408Upstream timeout.
no_offers_found412(Public sync) no SPs quoted in time.
parlay_disabled503Parlays disabled by config / ops.

7. Failed to Validate Price Probability

When quoting Parlays you will need to give the legs strike_id and probability per leg for any non-SGP (single game parlay) parlay. For SGPs you will not be required to give the probabilities but is recommended for better clarity for requestor.

ErrorMeaning
empty price probability arrayThe legs were not given strike_id or probabilities in quote
invalid probabilityThe given probabilities don't equal the requested odds

8. Outcome IDs (commonly seen)

IDMeaning
4Home
5Away
12Over
13Under

Full list owned by the ProphetX Markets team.