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; Quoters 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 Quoter
│ │ │
│ 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 Quoters
If multiple Quoters sit at the best-scored tier, the matcher selects them all and dispatches price.confirm.new to every selected Quoter simultaneously, followed by order.matched broadcasts in batch. There is no per-Quoter 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 Quoters 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 / Quoter rotation / fairness layer | None exist. Ranking is purely deterministic. |
| Quoter reputation or fill history | Not a factor. The only inputs are price, max_risk, and timestamp. |
| Split fills | If the top Quoter's max_risk is smaller than the participant's quantity, the next-ranked Quoters 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 Quoter confirmation, the platform tracks pending orders for the round. When all Quoters 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 Quoters 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.
Timeouts are counted the same way as rejects. A Quoter that accumulates too many timeouts or rejects is temporarily suspended for 15 minutes.
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.
- Quoters 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 Quoter submits multiple quotes for the same parlay, the matcher only considers the most recent one. Older quotes from the same Quoter 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; Quoters must submit a new quote if they need extended validity.
4. WebSocket events
| Event | Audience | Meaning |
|---|---|---|
price.ask.new | Quoter | New Parlay request — quote it. |
price.offers.new | Participant / ISV | Aggregated Quoter offers, grouped by price. |
price.confirm.new | Quoter | Confirmation requested for a selected offer. |
order.matched | public + Quoter | Preliminary. The matcher selected a counterparty; the Quoter has not confirmed yet. |
order.finalized | participant + Quoter | Terminal per order. The Quoter callback resolved as accept or reject. |
order.settled | Quoter | 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 Quoter, before the Quoter 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 Quoter. |
open | Participant-side order, cash balance deducted, awaiting a match. |
matched | Quoter 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 | Quoter 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 | Quoter is on a ban list. |
not_found | 404 | Resource lookup failed. |
request_timeout | 408 | Upstream timeout. |
no_offers_found | 412 | (Public sync) no Quoters 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 6 days ago
