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

POST/api/v1/quotesAPI key
quotes:write, plus X-Predicta-User.
request — sizing a buy in cash
{
  "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
}
request — sizing an exit in contracts
{
  "marketId": "ctr_9f31c2…",
  "side": "YES",
  "action": "sell",
  "contracts": 68.6111         // quantity. The only way to exit in full.
}
FieldTypeNotes
marketIdstringThe opaque ctr_ contract id published by /api/v1/events. An id that names no contract is 404 market_not_found.
sidestringYES or NO. Uppercase.
actionstringbuy opens or adds; sell exits.
stakenumberGross cash in dollars. Bounded on both ends: below a cent buys nothing, and unbounded is a denial-of-service vector dressed as an order.
contractsnumberQuantity. 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 contracts is 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.

arithmetic
stake        50.00
platformFee   0.60   = 50.00 × 120bps
tradeAmount  49.40   ← what buys contracts
contracts    68.6111 = 49.40 ÷ 0.72

The quote object

200 application/json — a buy
{
  "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
}
200 application/json — a sell (money only)
{
  …
  "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.

FieldMeaning
quoteIdPresent this to the order endpoint. Spendable once.
executionPriceThe 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.
priceBasisbook 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.
commercialTermsThe 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.
venueFeenull 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”.
contractsQuantity, 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.
potentialProfitpotentialPayout − the FULL stake, fee included.
expiresAtAbsolute. 30 seconds by default.
sourceTimestampWhen the underlying price was published upstream. Null when unknown.
freshnessThe 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.
routingHow many sources could have competed, and whether any actually may. The alternatives are never listed: naming them is naming the venues.
simulatedtrue 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. expiresAt is 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_expired and 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 quoteId is 409 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.

ResponseMeaningWhat to do
422 stake_exceeds_stale_limitMerely 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_staleToo 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

StatuserrorWhat happenedWhat to do
404market_not_foundNo such contract.Re-read the catalogue. Check the ctr_ id you sent.
403market_not_offeredThe 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.
409price_staleThe upstream price is too far behind to stand behind at any size.Wait for the next tick and re-quote.
422stake_exceeds_stale_limitMerely stale: a small stake is fine, this one is not.Re-request at or under the maxStake in the response.
422market_not_openThe contract has closed, resolved or been voided.Stop offering it. detail carries the status.
422no_priceNo usable price could be derived for that side.Show “no price”. Do not fall back to the reference.
422invalid_stakeThe stake buys zero contracts.Raise the stake.
400invalid_requestThe body failed validation — including sending both stake and contracts, or neither.Read detail for the failing field.
400missing_userNo X-Predicta-User header.Send your own id for the player.
400invalid_jsonThe 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.

typescript
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.