Parlay Additional Information
Understand Parlay matching, offer validity, WebSocket events, status values, and common errors.
1. The exchange model
The Parlay service is a request-quote-confirm exchange. A participant submits a multi-leg Parlay; Market Makers (SPs) respond with offers; the participant's confirmation triggers a matcher that selects among the offers; matched orders settle after the underlying events resolve.
Participant 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, quantity } │ │
│◀─────────────────────────│ │
│ │── 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 price 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 Market Makers
If multiple Market Makers sit at the best-scored tier, the matcher selects them all and dispatches price.confirm.new to every selected Market Maker simultaneously, followed by order.matched broadcasts in batch. There is no per-Market Maker 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 participant's quantity is covered. Anyone past that cutoff is not selected, even at the same displayed price.
Ranking uses a composite score:
- Price — primary factor.
max_risk— secondary tiebreaker; a larger offer ranks higher.- Submission time — tertiary tiebreaker; an earlier submission wins.
The matcher locks to the top-ranked offer's price value and only takes other offers whose price is exactly equal at full numeric precision, accumulating max_risk until the requested quantity is covered, then stops.
Practical consequences for SPs whose offers appear to "tie" the winner but don't fill:
| Factor | Effect on selection |
|---|---|
| Fractional price gap | Most common cause. Prices are compared at full floating-point precision, not at the displayed integer. Two offers that both display as, for example, +561 can be 5.6105 and 5.6098 underneath; the higher value wins outright on the primary key and no tiebreaker is consulted. |
max_risk | If prices are exactly equal at full precision, the larger max_risk ranks first. |
| Submission time | Only matters when both price and max_risk are exactly equal; an earlier submission wins. |
| Random selection / Market Maker rotation / fairness layer | None exist. Ranking is purely deterministic. |
| Market Maker reputation or fill history | Not a factor. The only inputs are price, max_risk, and timestamp. |
| Split fills | If the top Market Maker's max_risk is smaller than the participant's quantity, the next-ranked Market Makers fill the remainder in score order until the quantity 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 price values, not the displayed integer.
- If truly equal, compare
max_risk. - 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 Market Maker confirmation, the platform tracks pending orders for the round. When all Market Makers in the current round have responded, if the round was a full accept, the matcher runs again on the remaining quantity and selects the now-top tier.
| Round-N outcome | Trigger re-match? |
|---|---|
| Full accept by all Market Makers at the tier | Yes — next-best tier gets price.confirm.new |
Partial accept (confirmed_quantity < requested_quantity) | 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 the offers the participant saw earlier. A better-priced offer that lands between offers-fetch and /parlays/confirm will be used. Implications:
- Participants can receive price improvement beyond the UI display.
- Market Makers that revise a quote with a better price before confirmation are selected 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. Participants and UIs must run client-side timers from valid_until; Market Makers must submit a new quote if they need extended validity.
4. WebSocket events
| Event | Audience | Meaning |
|---|---|---|
price.ask.new | Market Maker | New Parlay request — quote it. |
price.offers.new | Participant / ISV | Aggregated Market Maker offers, grouped by price. |
price.confirm.new | Market Maker | Confirmation requested for a selected offer. |
order.matched | public + Market Maker | Preliminary. The matcher selected a counterparty; the Market Maker has not confirmed yet. |
order.finalized | participant + Market Maker | Terminal per order. The Market Maker callback resolved as accept or reject. |
order.settled | participant + Market Maker | Cash balance settlement posted. |
parlay.processing | participant | Matcher running. |
parlay.finalized | participant | Terminal per Parlay. All orders resolved. |
parlay.settled | participant | All settlement complete. |
refund.processed | participant | Refund posted, such as for open quantity. |
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 a Market Maker. |
open | Participant-side order, cash balance deducted, awaiting a match. |
matched | Market Maker callback accepted. |
finalized | Ready for settlement. |
pending_payout | Settlement calculated, payout queued. |
payout_submitted | Payout sent to the cash balance. |
settled | Cash balance transaction confirmed. |
rejected | Market Maker rejected, or liquidity was insufficient. |
void / failed | Terminal failure. |
Offer statuses: pending → confirming → partial_confirmed / confirmed / rejected.
Match statuses (parlay-level): pending → finalized / failed.
Settlement statuses (Parlay level): tbd → profit / loss / 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 | Market Maker is 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 you quote Parlays, provide a strike_id and probability for each strike in every non-SGP (single-game Parlay). SGPs do not require probabilities, but including them improves clarity for the participant.
| Error | Meaning |
|---|---|
empty price probability array | The strikes were not given strike_id values or probabilities in the quote. |
invalid probability | The given probabilities do not equal the requested price. |
8. Outcome IDs (commonly seen)
| ID | Meaning |
|---|---|
| 4 | Home |
| 5 | Away |
| 12 | Over |
| 13 | Under |
Full list owned by the ProphetX Markets team.
Updated about 1 month ago
