API Reference

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.

  • makerAmount is what the maker gives and takerAmount is what the maker receives, both in base units. The collateral token has 18 decimals, and outcome tokens use the same number of decimals.
  • nonce is the value of nonces(maker) on the Exchange contract. It is not the POLY_NONCE used to create an API key.
  • BUY: the maker gives collateral and receives outcome tokens. Size is takerAmount and price is makerAmount / takerAmount.
  • SELL: the maker gives outcome tokens and receives collateral. Size is makerAmount and price is takerAmount / 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. salt also accepts a JSON number, and signatureType is a JSON number.
  • side is the string BUY or SELL, in upper case.
  • postOnly and deferExec may be omitted or set to false. Setting either to true returns 400.
orderTypeBehaviorexpiration
GTC (default)Fills what it can, then rests on the book until filled or canceled"0"
GTDLike GTC, and closes as EXPIRED up to 60 seconds before the signed expirationUnix seconds, more than 60 seconds in the future
FOKFills completely at once or is canceled with no fill"0"
FAKFills 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. status is "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 Duplicated and does not place a second order. After a timeout or network error, check GET /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:

  • signatureType is 0 and signer equals maker.
  • taker is the zero address.
  • feeRateBps equals the market's current taker rate from GET /fee-rate. A mismatch returns 400 with the expected rate in error.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.
  • nonce equals the maker's current nonce on the Exchange contract, read with nonces(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 statusMeaning
LIVEOpen, including partially filled orders and filled orders whose settlement is not yet confirmed on-chain
MATCHEDFully filled, confirmed on-chain
CANCELEDCanceled by request, by a heartbeat timeout, as the unfilled part of a FOK or FAK order, or by a nonce increase
CANCELED_MARKET_RESOLVEDClosed because the market resolved
EXPIREDA 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.

Status alone does not tell you whether an order filled. A FOK or FAK remainder, or an order canceled while a match was settling, can read CANCELED while sizeMatched keeps growing as fills confirm, and it does not move to MATCHED. Use trades and sizeMatched to determine fills.
Trade statusMeaning
TRADE_STATUS_MATCHEDMatched off-chain, awaiting settlement
TRADE_STATUS_MINEDSettlement transaction included in a block
TRADE_STATUS_CONFIRMEDSettlement confirmed. Final
TRADE_STATUS_RETRYINGSettlement is being retried
TRADE_STATUS_FAILEDSettlement failed

REST responses use the TRADE_STATUS_ prefix. WebSocket messages use the bare value, such as MATCHED.

Read Orders and Trades

EndpointReturns
GET /data/order/{id}One order of your wallet, in any status. Another wallet's order returns 404
GET /data/ordersLIVE orders of your wallet, placed through any of its keys. Filters: id, market, assetId. Paginated
GET /data/tradesTrades 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 /activityYour 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.
  • traderSide tells you whether your wallet was the taker or the maker. owner is filled in only on your own side.
  • transactionHash is empty until the settlement transaction is mined.

Cancel Orders

EndpointBodyScope
DELETE /order{"orderId":"0x..."}Any order of your wallet
DELETE /orders["0x...", ...], 1 to 100 IDsAny order of your wallet
DELETE /cancel-allNoneLive orders placed with the calling API key only
DELETE /cancel-market-orders{"market":"0x...","assetId":"..."}, at least oneLive 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. canceled means the request was accepted, and the order can read LIVE for a moment afterwards.
  • An order already matching when the cancel arrives may still fill. Check sizeMatched and 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, allowance is the maximum uint256 value when the Exchange is approved for outcome tokens, and "0" otherwise.
  • signatureType may 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 /heartbeats with no body, or with an empty heartbeatId, 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 ID with the expected ID in error.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.
Send heartbeats well inside the 10-second window, for example every few seconds, so one slow request does not cancel your orders.