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:
- Odds — primary factor.
- MaxRisk — secondary tiebreaker (larger offer ranks higher).
- 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:
| Factor | Effect on selection |
|---|---|
| Fractional odds gap | Most 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. |
| MaxRisk | If odds are exactly equal at full precision, the larger MaxRisk ranks first. |
| Submission time | Only matters when both Odds and MaxRisk are exactly equal — earlier submission wins. |
| Random selection / SP rotation / fairness layer | None exist. Ranking is purely deterministic. |
| SP reputation or fill history | Not a factor. The only inputs are odds, MaxRisk, and timestamp. |
| Split-fills | Real. 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:
- Compare the full-precision odds values, not the displayed integer.
- If truly equal, compare
MaxRisk. - 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 outcome | Trigger re-match? |
|---|---|
| Full accept by all SPs at the tier | Yes — next-best tier gets price.confirm.new |
Partial accept (confirmed_stake < requested_stake) | No — finalize and refund the remainder |
| Reject | No — 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
valid_until constraintsEvery 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
| Event | Audience | Meaning |
|---|---|---|
price.ask.new | SP | New parlay request — quote it. |
price.offers.new | User/ISV | Aggregated SP offers (per-odds buckets). |
price.confirm.new | SP | Confirmation requested for a selected offer. |
order.matched | public + SP | Preliminary. Matcher selected a counterparty; SP hasn't confirmed yet. |
order.finalized | user + SP | Terminal per-order. SP callback resolved (accept or reject). |
order.settled | user + SP | Wallet payout posted. |
parlay.processing | user | Matcher running. |
parlay.finalized | user | Terminal per-parlay. All orders resolved. |
parlay.settled | user | All settlement complete. |
refund.processed | user | Refund posted (e.g. unmatched stake). |
order.matched is preliminary
order.matched is preliminaryorder.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:
| Status | Meaning |
|---|---|
inactive | Order created, confirmation not yet sent. |
sent_confirmation | price.confirm.new dispatched to SP. |
open | User-side order, money deducted, awaiting match. |
matched | SP callback accepted. |
finalized | Ready for settlement. |
pending_payout | Settlement calculated, payout queued. |
payout_submitted | Payout sent to wallet. |
settled | Wallet transaction confirmed. |
rejected | SP rejected, or insufficient liquidity. |
void / failed | Terminal failure. |
Offer statuses: pending → confirming → partial_confirmed / confirmed / rejected.
Match statuses (parlay-level): pending → finalized / failed.
Settlement statuses (parlay-level): tbd → won / lost / push / void.
6. Common errors
error code | HTTP | When |
|---|---|---|
invalid_params | 400 | Validation failed. |
invalid_legs_combination | 400 | Legs not accepted (correlated, banned, etc.). |
no_valid_market_lines | 400 | One or more legs reference unsupported lines. |
parlay_already_exists | 400 | Re-submitting a confirmed parlay. |
parlay_not_valid | 400 | Parlay expired or not found. |
confirmation_arrive_too_late | 400 | Confirm callback after the offer's valid_until. |
order_not_valid | 400 | Confirming an order in the wrong state. |
wallet_insufficient_funds / insufficient_balance | 400 / 402 | User / ISV wallet rejected. |
unauthorized | 401 | Missing/invalid auth header. |
sp_banned | 403 | SP on a ban list. |
not_found | 404 | Resource lookup failed. |
request_timeout | 408 | Upstream timeout. |
no_offers_found | 412 | (Public sync) no SPs quoted in time. |
parlay_disabled | 503 | Parlays 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.
| Error | Meaning |
|---|---|
empty price probability array | The legs were not given strike_id or probabilities in quote |
invalid probability | The given probabilities don't equal the requested odds |
8. Outcome IDs (commonly seen)
| ID | Meaning |
|---|---|
| 4 | Home |
| 5 | Away |
| 12 | Over |
| 13 | Under |
Full list owned by the ProphetX Markets team.
