Orders
POST /order/rfq prices a trade without placing it. POST /order places one.
There are two ways to place an order.
MARKETfills at the current market price.MARKETcan 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.FOKfills at the exact price and size of a prior quote, or not at all. Similar toMARKET, 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",
}},
}, "e)
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.