Orders
Every endpoint on this page needs L2 authentication. The order maker must be the wallet that owns the API key.
Signing an Order
An order is EIP-712 typed data signed by the maker wallet.
Domain
name: "Starzip POR CTF Exchange"
version: "1"
chainId: 97 during the preview
verifyingContract: 0x20769ADbD470C180949226C5Db04dBf0cAF212A3 (Exchange, preview)
Type
Order(
uint256 salt,
address maker,
address signer,
address taker,
uint256 tokenId,
uint256 makerAmount,
uint256 takerAmount,
uint256 expiration,
uint256 nonce,
uint256 feeRateBps,
uint8 side, // 0 = BUY, 1 = SELL
uint8 signatureType // 0 = EOA
)The network, contract addresses, and collateral token are listed in Network and Contracts.
makerAmountis what the maker gives andtakerAmountis what the maker receives, both in base units. The collateral token has 18 decimals, and outcome tokens use the same number of decimals.nonceis the value ofnonces(maker)on the Exchange contract. It is not thePOLY_NONCEused to create an API key.- BUY: the maker gives collateral and receives outcome tokens. Size is
takerAmountand price ismakerAmount / takerAmount. - SELL: the maker gives outcome tokens and receives collateral. Size is
makerAmountand price istakerAmount / makerAmount. - The order ID is the EIP-712 hash of the order.
- The signature must be 65 bytes with a recovery byte of 27 or 28 and a low-s value. Standard wallet signers produce this form.
Place an Order
POST /order with this body:
{
"order": {
"salt": "123456789",
"maker": "0x<wallet>",
"signer": "0x<wallet>",
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": "<tokenId>",
"makerAmount": "<base units>",
"takerAmount": "<base units>",
"expiration": "0",
"nonce": "<Exchange.nonces(maker)>",
"feeRateBps": "<baseFee from GET /fee-rate>",
"side": "BUY",
"signatureType": 0,
"signature": "0x..."
},
"orderType": "GTC"
}- Numeric order fields are quoted decimal strings.
saltalso accepts a JSON number, andsignatureTypeis a JSON number. sideis the stringBUYorSELL, in upper case.postOnlyanddeferExecmay be omitted or set tofalse. Setting either totruereturns 400.
| orderType | Behavior | expiration |
|---|---|---|
| GTC (default) | Fills what it can, then rests on the book until filled or canceled | "0" |
| GTD | Like GTC, and closes as EXPIRED up to 60 seconds before the signed expiration | Unix seconds, more than 60 seconds in the future |
| FOK | Fills completely at once or is canceled with no fill | "0" |
| FAK | Fills what it can at once. The rest is canceled and never rests | "0" |
Every order type uses the limit price in the signed order. A successful response:
{ "success": true, "errorMsg": "", "orderId": "0x<order hash>", "status": "live" }- Matching is asynchronous. The response confirms the order was accepted.
statusis"live"or"matched", or"delayed"when matching did not finish in time; a delayed order is still being processed. Follow fills through trades or the user WebSocket channel. - Submitting the same signed order again is rejected with 400
Duplicatedand does not place a second order. After a timeout or network error, checkGET /data/order/{id}before you resubmit. - A rejected order returns an HTTP error with the reason in
error.message. See Responses and Errors.
Place a Batch
POST /orders takes a JSON array of 1 to 15 bodies, each shaped like the POST /order body. Orders are processed in array order, so balance locked by an earlier order counts against a later one.
The response is always 200 with one result per order, in the same order. Check success on each item. A failed item looks like:
{ "success": false, "errorMsg": "<reason>", "orderId": "", "status": "" }Only an empty, oversized, or malformed array returns 400.
Validation Rules
An order is rejected with 400 unless all of these hold:
signatureTypeis 0 andsignerequalsmaker.takeris the zero address.feeRateBpsequals the market's current taker rate fromGET /fee-rate. A mismatch returns 400 with the expected rate inerror.data.feeRateBps; sign again with that value.- The price is a multiple of the market's tick size, at least one tick and at most 1 minus one tick.
- The token amount is a whole multiple of 1 / tick size in base units, and at least the market minimum order size.
nonceequals the maker's current nonce on the Exchange contract, read withnonces(maker).- The token belongs to a market that is currently active.
- BUY: the maker has enough collateral balance and allowance for the Exchange, after the amounts locked by its live orders. The check includes the largest fee the order could pay: makerAmount × (10000 + feeRateBps) / 10000, rounded up.
- SELL: the maker has enough outcome tokens after its live orders, and has approved the Exchange for outcome tokens.
An order whose maker is not the API key's wallet returns 403. If the chain cannot be read during validation, the API returns 503 chain unavailable; retry the request.
Matching
- Orders match by price, then by arrival time.
- A resting order trades at its own price. The incoming order receives any price improvement.
- A BUY order can also match a BUY order for the other outcome of the same market when their prices sum to at least 1. A SELL order can match a SELL order for the other outcome when their prices sum to at most 1.
- Orders from the same wallet never match each other, even across different API keys. Both orders stay on the book.
- A partially filled GTC or GTD order stays on the book with the status LIVE.
- Matched trades are settled on-chain.
Order and Trade Status
| Order status | Meaning |
|---|---|
LIVE | Open, including partially filled orders and filled orders whose settlement is not yet confirmed on-chain |
MATCHED | Fully filled, confirmed on-chain |
CANCELED | Canceled by request, by a heartbeat timeout, as the unfilled part of a FOK or FAK order, or by a nonce increase |
CANCELED_MARKET_RESOLVED | Closed because the market resolved |
EXPIRED | A GTD order that reached its expiry |
Only a LIVE order changes status. sizeMatched counts fills confirmed on-chain, so it can trail the trade events on the user WebSocket channel by the time settlement takes.
CANCELED while sizeMatched keeps growing as fills confirm, and it does not move to MATCHED. Use trades and sizeMatched to determine fills.| Trade status | Meaning |
|---|---|
TRADE_STATUS_MATCHED | Matched off-chain, awaiting settlement |
TRADE_STATUS_MINED | Settlement transaction included in a block |
TRADE_STATUS_CONFIRMED | Settlement confirmed. Final |
TRADE_STATUS_RETRYING | Settlement is being retried |
TRADE_STATUS_FAILED | Settlement failed |
REST responses use the TRADE_STATUS_ prefix. WebSocket messages use the bare value, such as MATCHED.
Read Orders and Trades
| Endpoint | Returns |
|---|---|
GET /data/order/{id} | One order of your wallet, in any status. Another wallet's order returns 404 |
GET /data/orders | LIVE orders of your wallet, placed through any of its keys. Filters: id, market, assetId. Paginated |
GET /data/trades | Trades where your wallet is the taker or a maker, in every status. Filters: id, market, assetId, makerAddress (must be your wallet), after and before (Unix seconds, exclusive). Paginated |
GET /activity | Your account history: fills as BUY or SELL entries and taker fees as FEE entries, each with amount, transactionHash, and timestamp. Paginated |
An order object:
{
"id": "0x<order hash>",
"status": "LIVE",
"owner": "<API key ID that placed it>",
"makerAddress": "0x...",
"market": "0x<conditionId>",
"assetId": "<tokenId>",
"side": "BUY",
"originalSize": "100",
"sizeMatched": "40",
"price": "0.48",
"outcome": "YES",
"expiration": "0",
"orderType": "GTC",
"associateTrades": ["<trade ID>"],
"createdAt": 1760000000
}A trade object:
{
"id": "<trade ID>",
"takerOrderId": "0x...",
"market": "0x<conditionId>",
"assetId": "<tokenId>",
"side": "BUY",
"size": "40",
"feeRateBps": "0",
"price": "0.48",
"status": "TRADE_STATUS_CONFIRMED",
"matchTime": "1760000000",
"lastUpdate": "1760000012",
"outcome": "YES",
"bucketIndex": 0,
"owner": "<API key ID>",
"makerAddress": "0x...",
"transactionHash": "0x...",
"traderSide": "TAKER",
"makerOrders": [
{
"orderId": "0x...",
"owner": "",
"makerAddress": "0x...",
"matchedAmount": "40",
"price": "0.48",
"feeRateBps": "0",
"assetId": "<tokenId>",
"outcome": "YES",
"side": "SELL"
}
]
}- The top-level fields describe the taker order. Each trade has exactly one maker order.
traderSidetells you whether your wallet was the taker or the maker.owneris filled in only on your own side.transactionHashis empty until the settlement transaction is mined.
Cancel Orders
| Endpoint | Body | Scope |
|---|---|---|
DELETE /order | {"orderId":"0x..."} | Any order of your wallet |
DELETE /orders | ["0x...", ...], 1 to 100 IDs | Any order of your wallet |
DELETE /cancel-all | None | Live orders placed with the calling API key only |
DELETE /cancel-market-orders | {"market":"0x...","assetId":"..."}, at least one | Live orders placed with the calling API key in that market or token |
All four return:
{
"canceled": ["0x..."],
"notCanceled": { "0x...": "order not found" }
}- Cancellation is asynchronous.
canceledmeans the request was accepted, and the order can readLIVEfor a moment afterwards. - An order already matching when the cancel arrives may still fill. Check
sizeMatchedand trades rather than the status. - To invalidate every order of a wallet at the contract level, increase the wallet's nonce on the Exchange contract. Live orders with a lower nonce are canceled.
Balance and Allowance
GET /balance-allowance?assetType=COLLATERAL or ?assetType=CONDITIONAL&tokenId=... returns {"balance":"...","allowance":"..."}.
- Both values are decimal strings in base units, not whole tokens.
- The balance is the on-chain balance. Amounts locked by your live orders are not subtracted, but order validation does subtract them, so an order can be rejected for insufficient balance even when this endpoint shows enough.
- For
CONDITIONAL,allowanceis the maximum uint256 value when the Exchange is approved for outcome tokens, and"0"otherwise. signatureTypemay be omitted or set to 0.
Heartbeats
Heartbeats cancel your orders automatically if your client stops responding. They are optional and apply per API key.
- Send
POST /heartbeatswith no body, or with an emptyheartbeatId, to start. The response is{"heartbeatId":"<id>"}. - Send each following heartbeat with
{"heartbeatId":"<id>"}set to the ID from the previous response. - A wrong ID returns 400
Invalid Heartbeat IDwith the expected ID inerror.data.heartbeatId, and does not count as a heartbeat. - Send a heartbeat about every 5 seconds to stay well inside the window.
- If no valid heartbeat arrives for 10 seconds, every live order placed with that key is canceled, normally within about 11 seconds. Orders placed with other keys are not affected.
- After a timeout, start again without an ID.