Skip to content

Orders

POST /order/rfq prices a trade without placing it. POST /order places one.

There are two ways to place an order.

  • MARKET fills at the current market price. MARKET can be sized in number of contracts or wager (USD), and can be boosted when opening. Orders are always specified in number of contracts when closing.
  • FOK fills at the exact price and size of a prior quote, or not at all. Similar to MARKET, a market order can be sized in number of contracts or wager when opening, but is always sized in number of contracts when closing. Boosting is not viable on a close.

Legs

The bet itself lives in legs, which takes exactly one entry, and everything about which team and how hard to boost sits on the leg. Both POST /order/rfq and POST /order take the same leg.

Field Required Description
contractId yes Fixture identifier from GET /fixtures.
outcome yes The team abbreviation the bet is on.
side yes BUY opens or adds to a bet, SELL closes or reduces one.
market yes Which market on the fixture the leg bets on. DRAW_NO_BET is the only value accepted.
boost no Whole number from 0 to 100, how hard to boost the leg. Defaults to 0. Not accepted when closing.

contractId, outcome, side, market and orderType are case insensitive and come back uppercased.

More than one leg is not accepted, and a request carrying two or more is rejected with 501 and code -2026.

Opening vs. closing

Opening Closing
MARKET, BASE qty = contracts to buy. Fills at whatever the market gives, no price bound. boost optional. qty = contracts to sell, same unbounded fill. boost not accepted.
MARKET, QUOTE qty = dollars to spend. The dollar amount is a hard cap on chain, and the exchange shrinks the order if the cost would exceed it, so fewer contracts than quoted may be filled. boost optional. Not allowed, closes are always sized in contracts.
FOK, BASE, with rfqId Fills the exact contracts and price the quote locked in, or nothing. qty and boost come from the quote. Same, using a quote requested with side: SELL, for a guaranteed exit size.
FOK, QUOTE, with rfqId Same, and the dollar amount is a hard spend cap. Unlike MARKET, the order reverts rather than shrinking if the cost would exceed it. Not allowed, closes are always BASE.

That shrink versus revert difference is the whole point of FOK on a dollar bet, since MARKET accepts a smaller fill to stay under the cap while FOK refuses the trade instead.

Throughout that table boost means the leg's own boost, not a top level field.

A close is capped at the position held. A request to sell more contracts than are held quotes the full position rather than failing.

There is no dedicated close endpoint and no shorthand for closing everything. qty is always required.

POST /order/rfq

This endpoint prices a trade using exactly the rules POST /order would apply.

Request body

Send the bet inline, the same legs, qty and marketUnit sent to POST /order.

Field Type Required Description
legs array Yes The bet, as exactly one entry. See Legs.
qty string Yes Decimal string. marketUnit decides whether it counts contracts or a wager in USD.
marketUnit string Yes BASE sizes qty in contracts, QUOTE sizes it as a wager in USD. QUOTE is only allowed on opens, a close is always sized in contracts.
orderType string Optional MARKET or FOK. Does not change the quote.

Response

{
  "rfqId": "rfq_e38fac999994cc0a2657b461",
  "reqId": "bet-001",
  "contracts": 126,
  "entryPrice": "0.4297",
  "indexPrice": "0.4294",
  "isOpen": true,
  "knockout": "0.3738",
  "cost": "19.84",
  "maxWin": "91.57",
  "exitValue": "17.16",
  "minContracts": 10,
  "exchangeCapacity": 7399282
}
Field Meaning
rfqId Pass this back on POST /order to execute this exact quote. It stays live for 30 seconds and executes at most once. A second order against the same rfqId is refused with -2027, and one sent after it lapses with -2031. Request a new quote rather than retrying a spent one.
reqId Echoes clientOrderId if one was sent, an empty string otherwise.
contracts The size quoted, rounded to the market's lot size. A dollar denominated open and any close round down, a contract denominated open rounds to the nearest lot. A FOK order fills exactly this or nothing. A MARKET order reprices when it lands and may fill differently.
isOpen true when the quote opens or adds to a bet, false when it reduces or closes one. The four fields below that are marked opens only are present exactly when this is true.
entryPrice The price this trade fills at. Locked in for FOK, an estimate for MARKET.
indexPrice The market price before the trade. The gap to entryPrice covers the exchange's current skew plus the order's own impact.
cost The total debited: margin plus fees. On a dollar bet qty is a hard cap, so the debit never exceeds it. Opens only.
maxWin The amount returned if the bet settles in favour. In Opens rfqs only.
exitValue The amount returned by closing immediately, net of the round trip. Always below cost. In opens only.
knockout For DRAW_NO_BET markets, the index price at which the position is knocked out and the bet loses. Absent when the resulting position has no knockout price, that is when the trade leaves an unboosted position, including a full closure.
minContracts The smallest bet the market accepts, in contracts.
exchangeCapacity How many more contracts the exchange can take on this outcome right now, in contracts. A request larger than this is rejected. Opens only.

Example

const { fixtures } = await signedGet("/fixtures", { bettingOpen: "true", limit: 1 });
const fixture = fixtures[0];

const quote = await signedPost("/order/rfq", {
  orderType: "FOK",
  qty: "20",
  marketUnit: "QUOTE",
  legs: [{
    contractId: fixture.contractId,
    outcome: fixture.awayTeam,
    side: "BUY",
    market: "DRAW_NO_BET",
    boost: "50",
  }],
});
console.log(quote.contracts, "contracts costing", quote.cost, "to win", quote.maxWin);
fixture = signedGet("/fixtures", {"bettingOpen": "true", "limit": 1})["fixtures"][0]

quote = signedPost("/order/rfq", {
    "orderType": "FOK",
    "qty": "20",
    "marketUnit": "QUOTE",
    "legs": [{
        "contractId": fixture["contractId"],
        "outcome": fixture["awayTeam"],
        "side": "BUY",
        "market": "DRAW_NO_BET",
        "boost": "50",
    }],
})
print(quote["contracts"], "contracts costing", quote["cost"], "to win", quote["maxWin"])
var fixtures struct {
    Fixtures []struct {
        ContractID string `json:"contractId"`
        AwayTeam   string `json:"awayTeam"`
    } `json:"fixtures"`
}
signedGet("/fixtures", url.Values{"bettingOpen": {"true"}, "limit": {"1"}}, &fixtures)
fixture := fixtures.Fixtures[0]

var quote struct {
    RFQID     string `json:"rfqId"`
    Contracts int    `json:"contracts"`
    Cost      string `json:"cost"`
    MaxWin    string `json:"maxWin"`
}
signedPost("/order/rfq", map[string]any{
    "orderType": "FOK", "qty": "20", "marketUnit": "QUOTE",
    "legs": []map[string]string{{
        "contractId": fixture.ContractID, "outcome": fixture.AwayTeam,
        "side": "BUY", "market": "DRAW_NO_BET", "boost": "50",
    }},
}, &quote)
fmt.Println(quote.Contracts, "contracts costing", quote.Cost, "to win", quote.MaxWin)

POST /order

This endpoint submits the order and returns as soon as the transaction is broadcast.

Request body

Send the bet inline (legs, qty, marketUnit) or an rfqId from a prior quote, never both.

Field Type Required Description
orderType string Always MARKET fills at the current price, FOK fills the exact number of contracts quoted or nothing.
rfqId string Only for FOK Executes a quote from a prior POST /order/rfq. Required for FOK, optional for MARKET.
legs array Yes, unless an rfqId is sent The bet, as exactly one entry. See Legs.
qty string Yes, unless an rfqId is sent Decimal string. marketUnit decides whether it counts contracts or a wager in USD.
marketUnit string Yes, unless an rfqId is sent BASE sizes qty in contracts, QUOTE sizes it as a wager in USD. QUOTE is only allowed on opens, a close is always sized in contracts.
clientOrderId string Never, optional A caller supplied id for the bet. Resending the same one returns the order already placed rather than placing a second. Letters, digits and _:.-, up to 64 characters.

qty and legs must not accompany an rfqId, since the quote already fixed them, and the request is rejected if they appear. When an rfqId is sent without a clientOrderId, the rfqId becomes the clientOrderId. A quote belongs to the wallet that requested it, and another wallet cannot execute it.

Response

{
  "clientOrderId": "bet-001",
  "orderId": "0xc82023c3441ef25b038600dbbec85c31d28ea2bb1c7df634a9863955832cee5a",
  "contractId": "MLB_ATL_MIL_260821",
  "outcome": "ATL",
  "side": "BUY",
  "status": "SUBMITTED",
  "contracts": 126,
  "entryPrice": "0.4297",
  "cost": "19.84",
  "maxWin": "91.57",
  "knockout": "0.3738",
  "deadline": 1787299148000,
  "tx": "0x1b42ab85ffe7b43d6102abc488e6f40161b9990757145628b841d8b93e46603d"
}

status is SUBMITTED once broadcast, which means the transaction is on its way rather than that the bet filled. A failure before that point comes back as an HTTP error instead, and the clientOrderId is spent either way, so a retry needs a new one.

Track what happens next with GET /order or GET /orders/updates. A FOK order that cannot fill on its terms is removed rather than executed, and it surfaces there as CANCELED.

Executing a FOK needs only the rfqId. The fixture, size and boost all come from the quote.

const order = await signedPost("/order", {
  orderType: "FOK",
  rfqId: quote.rfqId,
  clientOrderId: `bet-${Date.now()}`,
});
console.log(order.status, order.orderId, order.tx);
order = signedPost("/order", {
    "orderType": "FOK",
    "rfqId": quote["rfqId"],
    "clientOrderId": f"bet-{int(time.time())}",
})
print(order["status"], order["orderId"], order["tx"])
type Order struct {
    OrderID       string `json:"orderId"`
    ClientOrderID string `json:"clientOrderId"`
    Status        string `json:"status"`
    Contracts     int    `json:"contracts"`
    TX            string `json:"tx"`
    Error         string `json:"error"`
}

var order Order
signedPost("/order", map[string]string{
    "orderType": "FOK", "rfqId": quote.RFQID,
    "clientOrderId": fmt.Sprintf("bet-%d", time.Now().Unix()),
}, &order)
fmt.Println(order.Status, order.OrderID, order.TX)

A market order in one call

MARKET needs no quote, so it can be placed directly.

await signedPost("/order", {
  orderType: "MARKET",
  qty: "20",
  marketUnit: "QUOTE",
  clientOrderId: "bet-002",
  legs: [{
    contractId: fixture.contractId,
    outcome: fixture.awayTeam,
    side: "BUY",
    market: "DRAW_NO_BET",
    boost: "50",
  }],
});
signedPost("/order", {
    "orderType": "MARKET",
    "qty": "20",
    "marketUnit": "QUOTE",
    "clientOrderId": "bet-002",
    "legs": [{
        "contractId": fixture["contractId"],
        "outcome": fixture["awayTeam"],
        "side": "BUY",
        "market": "DRAW_NO_BET",
        "boost": "50",
    }],
})
signedPost("/order", map[string]any{
    "orderType": "MARKET", "qty": "20", "marketUnit": "QUOTE",
    "clientOrderId": "bet-002",
    "legs": []map[string]string{{
        "contractId": fixture.ContractID, "outcome": fixture.AwayTeam,
        "side": "BUY", "market": "DRAW_NO_BET", "boost": "50",
    }},
}, &order)

Closing a bet

A close is a side: SELL on the outcome held. Closes are always sized in contracts and never take boost. A close quote returns contracts and entryPrice, but not cost, maxWin or exitValue, since those describe opening a position.

await signedPost("/order", {
  orderType: "MARKET",
  qty: "20",
  marketUnit: "BASE",
  clientOrderId: "reduce-001",
  legs: [{
    contractId: fixture.contractId,
    outcome: fixture.awayTeam,
    side: "SELL",
    market: "DRAW_NO_BET",
  }],
});
signedPost("/order", {
    "orderType": "MARKET",
    "qty": "20",
    "marketUnit": "BASE",
    "clientOrderId": "reduce-001",
    "legs": [{
        "contractId": fixture["contractId"],
        "outcome": fixture["awayTeam"],
        "side": "SELL",
        "market": "DRAW_NO_BET",
    }],
})
signedPost("/order", map[string]any{
    "orderType": "MARKET", "qty": "20", "marketUnit": "BASE",
    "clientOrderId": "reduce-001",
    "legs": []map[string]string{{
        "contractId": fixture.ContractID, "outcome": fixture.AwayTeam,
        "side": "SELL", "market": "DRAW_NO_BET",
    }},
}, &order)

Reading orders

Every read below needs a key with the read permission.

tx is the transaction hash for the order, ready to look up on a block explorer. Gas is sponsored by PredictEX, so it resolves to an ERC-4337 entry point transaction rather than a direct call to the order book.

tx can be empty for a short while on a freshly submitted order, so read status to decide whether a bet is on and poll GET /order for the hash rather than treating its absence as a failure.

GET /order

This endpoint returns one order, looked up by either orderId or clientOrderId, and exactly one of the two must be passed.

GET /order?clientOrderId=reduce-001

GET /orders

This endpoint returns the wallet's orders, newest first. Optional filters are contractId, team and status, and paging uses limit and offset. status is one of CLAIMED, SUBMITTED, OPEN, FILLED, CANCELED, EXPIRED, REJECTED or FAILED.

{"orders": [], "total": 12, "limit": 50, "offset": 0}

GET /orders/updates

This endpoint returns only the orders that changed, which suits polling without refetching everything. Pass the cursor from the previous response as the next since, and omit since on the first call to get all of them.

The first call omits since and returns every order along with a cursor.

GET /orders/updates
{"orders": [], "cursor": 1788495452000}

Each later call passes the previous cursor and returns only what moved since then.

GET /orders/updates?since=1788495452000

since is milliseconds since epoch, same clock as GET /time. The cursor advances even when orders is empty. An older cursor replays more history and is never an error.

This is cheaper than re-reading GET /orders, which costs one call per page of results against the same reads bucket. See Rate limits.