Trading
Quotes
A quote is a priced, time-bounded offer that binds contract, side, size, price, fee and quantity together — and tells your ledger exactly what to reserve. It is the record an order is checked against.
Create a quote
/api/v1/quotesAPI keyquotes:write, plus X-Predicta-User.{
"marketId": "ctr_9f31c2…", // required. The contract id from /api/v1/events
"side": "YES", // required. YES | NO
"action": "buy", // optional, defaults to "buy"
"stake": 50 // cash. > 0 and ≤ 1,000,000
}{
"marketId": "ctr_9f31c2…",
"side": "YES",
"action": "sell",
"contracts": 68.6111 // quantity. The only way to exit in full.
}| Field | Type | Notes |
|---|---|---|
marketId | string | The opaque ctr_ contract id published by /api/v1/events. An id that names no contract is 404 market_not_found. |
side | string | YES or NO. Uppercase. |
action | string | buy opens or adds; sell exits. |
stake | number | Gross cash in dollars. Bounded on both ends: below a cent buys nothing, and unbounded is a denial-of-service vector dressed as an order. |
contracts | number | Quantity. Sizes a sell only. |
Sizing: exactly one of stake or contracts
Send one. Sending both is 400 invalid_request, because a client bug that sends a stale stake alongside a computed contracts would otherwise trade a size the caller did not intend, and the response would look perfectly normal.
- A buy is sized in cash. Sizing a buy in
contractsis refused: it would commit the player to whatever that quantity costs at fill time, which is the open exposure the stake-first model exists to prevent. - An exit is sized in contracts. A partner holds a quantity — 68.6111 contracts — and to close it out by cash they would have to invert our fee arithmetic, including which way each rounding goes. Slightly high is an oversell; slightly low leaves an unsellable fraction behind. Naming the quantity moves that inversion to the side that owns the arithmetic.
The fee comes off the top
A $50 stake at 120bps wagers $49.40. The fee is taken from the stake before contracts are bought, not added on top of it — so the gross cash committed is the authorizationAmount, and a fully filled order releases nothing. A player who agreed to stake $50 is never charged $50.60.
stake 50.00
platformFee 0.60 = 50.00 × 120bps
tradeAmount 49.40 ← what buys contracts
contracts 68.6111 = 49.40 ÷ 0.72The quote object
{
"quoteId": "fc3be731-98dc-4c03-80ac-23576a708d9c",
"contractId": "ctr_9f31c2…",
"side": "YES",
"action": "buy",
"executionPrice": 0.72, // WHAT YOU ARE CHARGED, 0–1
"priceBasis": "book", // book | reference
"stake": 50,
"predictaFee": 0.6,
"predictaFeeBps": 120,
"commercialTerms": { "termsId": "…", "feeBps": 120, "revShareBps": 0 },
"venueFee": null,
"tradeAmount": 49.4,
"contracts": 68.6111,
"potentialPayout": 68.61,
"potentialProfit": 18.61,
"expiresAt": "2026-08-20T04:44:02.120Z",
"sourceTimestamp": "2026-08-20T04:43:27.496Z",
"simulated": true,
"freshness": { "level": "fresh", "ageSeconds": 5, "feed": "websocket" },
"routing": { "consideredVenues": 1, "routable": false },
"operatorMoney": {
"currency": "USD",
"authorizationAmount": "50.00", // RESERVE THIS
"actualDebit": "50.00",
"releaseAmount": "0.00",
"tradeAmount": "49.40",
"platformFee": "0.60",
"venueFee": "0.00",
"venueFeeKnown": true
},
"operatorExitMoney": null
}{
…
"action": "sell",
"operatorMoney": null, // deliberately null on an exit
"operatorExitMoney": {
"currency": "USD",
"sellProceeds": "48.72", // CREDIT THIS. Already net of both fees.
"platformFee": "0.59",
"venueFee": "0.00",
"venueFeeKnown": true
}
}Two objects, because the money runs in two directions
A buy is a debit and a sell is a credit. A single object for both would hand a wallet an authorizationAmount on a trade that pays cash out, and a partner applying the contract as written would debit the player for selling their own position. So the entry object goes null on an exit rather than being filled with a plausible lie.
Both carry exact decimal strings. On the entry object, authorizationAmount = actualDebit + releaseAmount and actualDebit = tradeAmount + platformFee + venueFee, exactly — assert them rather than tolerancing them.
| Field | Meaning |
|---|---|
quoteId | Present this to the order endpoint. Spendable once. |
executionPrice | The price per contract this quote will fill at, 0–1. A buy takes the ask, a sell takes the bid; buying NO costs 1 − bid. Never a midpoint. |
priceBasis | book means a real side of a real book. reference means no usable two-way price existed and this is an estimate — display it as one, or not at all. |
commercialTerms | The terms this quote was struck under, echoed back so your reconciliation can assert the rate you expected rather than inferring it from the fee and the stake. |
venueFee | null when the execution fee is not yet known. The money objects report it as 0.00 with venueFeeKnown: false, so you can see the difference between “free” and “unknown”. |
contracts | Quantity, rounded to four decimals for transport. The stored quote keeps full precision and is what the fill is checked against. |
potentialPayout | $1.00 per contract if the side is right, rounded down to the cent. |
potentialProfit | potentialPayout − the FULL stake, fee included. |
expiresAt | Absolute. 30 seconds by default. |
sourceTimestamp | When the underlying price was published upstream. Null when unknown. |
freshness | The server’s verdict on the price’s age — level, ageSeconds, feed. Render it; do not recompute it. The two disagreeing is the bug this field exists to make impossible. |
routing | How many sources could have competed, and whether any actually may. The alternatives are never listed: naming them is naming the venues. |
simulated | true in this build: the fill is produced locally. |
There is no venue, no venueListingId and no upstream market id anywhere on this surface. A contract is addressed only by its ctr_… id — the same one the catalogue publishes and the same one you read back on the order.
The reference probability is not on the quote
To show a player both, read yesCents from the catalogue and executionPrice from the quote. Label the first as the probability and the second as the price.
Expiry
- A quote stands for 30 seconds by default.
expiresAtis absolute: use it rather than starting your own timer at receipt. - Expiry is evaluated against the stored timestamp, never one the client sends. A client that could assert its own quote's age could hold a favourable price and submit it after the market moved.
- An expired quote is
409 quote_expiredand is never silently re-priced. Re-pricing executes at a number the player did not see, which is the precise failure quotes exist to prevent. - A quote is spendable once. A second order against the same
quoteIdis409 quote_already_used. - A quote issued to one operator cannot be spent by another:
403 quote_not_yours.
Stale prices: a cap before a refusal
Two different answers, because they are two different situations. A small bet on a slightly-behind price is ordinary business; a large one at the same moment is somebody acting on information we do not have yet.
| Response | Meaning | What to do |
|---|---|---|
422 stake_exceeds_stale_limit | Merely stale. The price is tradeable at a smaller size; this stake is not. | Re-request at or under the maxStake the response carries. |
409 price_stale | Too far behind to stand behind at any size. | Wait for the next tick and re-quote. Not a payload problem. |
The cap is checked against the ticket's stake, not the raw request: the ticket normalises and rounds what is actually being wagered, and gating on a number the ticket then changes would police the wrong figure.
When a quote is refused
| Status | error | What happened | What to do |
|---|---|---|---|
404 | market_not_found | No such contract. | Re-read the catalogue. Check the ctr_ id you sent. |
403 | market_not_offered | The contract exists; your operator is not offered it. Deliberately not a 404 — it is a real market, and saying otherwise sends you to debug your ids. | A commercial question. Escalate rather than retrying. |
409 | price_stale | The upstream price is too far behind to stand behind at any size. | Wait for the next tick and re-quote. |
422 | stake_exceeds_stale_limit | Merely stale: a small stake is fine, this one is not. | Re-request at or under the maxStake in the response. |
422 | market_not_open | The contract has closed, resolved or been voided. | Stop offering it. detail carries the status. |
422 | no_price | No usable price could be derived for that side. | Show “no price”. Do not fall back to the reference. |
422 | invalid_stake | The stake buys zero contracts. | Raise the stake. |
400 | invalid_request | The body failed validation — including sending both stake and contracts, or neither. | Read detail for the failing field. |
400 | missing_user | No X-Predicta-User header. | Send your own id for the player. |
400 | invalid_json | The body did not parse. | Fix the request. |
409 means re-quote, 422 means fix the request
The distinction is load-bearing. A 409 says the payload was correct and the state moved: the right response is to ask again in a moment. A 422 says the request itself needs to change. A client that retries a 422 unchanged will retry forever.
With the SDK
The two sizing rules are enforced by the type signatures rather than by a runtime error, so a buy cannot be sized in contracts and an exit cannot be sized in cash.
const player = predicta.forUser('user-4471');
const buy = await player.quoteBuy({ contractId, side: 'YES', stake: '50.00' });
// buy.operatorMoney.authorizationAmount → "50.00", a branded Decimal string
const exit = await player.quoteSell({ contractId, side: 'YES', contracts: 68.6111 });
// exit.operatorExitMoney.sellProceeds → "48.72"
const order = await player.submitOrder({ quoteId: buy.quoteId, clientOrderId: 'ticket-9031' });Money comes back as a branded Decimal string that will not silently become a number, with toMinorUnits / addDecimal helpers for arithmetic. See the SDK.

