Skip to main content

Overview

Posting an order places it on the SX Bet orderbook for takers to fill. This guide covers every field in the order payload, how to sign it, and how to handle the response.

Order fields

Each order submitted to POST /orders/new requires these fields:
FieldTypeDescription
marketHashstringThe market you’re posting to. Get this from GET /markets/active or GET /markets/find
makerstringYour wallet address
baseTokenstringThe token this order is denominated in (e.g., USDC). See References for token addresses
totalBetSizestringYour maximum risk in base token units (Ethereum format). See Unit Conversion
percentageOddsstringThe odds you the maker receive, in SX protocol format (implied * 10^20). Must be on the odds ladder
expirynumberDeprecated. Must always be 2209006800
apiExpirynumberUnix timestamp (seconds) after which this order expires
executorstringThe SX Bet exchange address. Get this from GET /metadata
saltstringA random number to differentiate otherwise identical orders
isMakerBettingOutcomeOnebooleantrue if you’re betting on outcome one, false for outcome two
signaturestringYour wallet signature over the order hash

Understanding percentageOdds

This is the implied probability from your perspective as the maker. The taker gets the complementary odds on the opposite outcome:
taker_implied = 1 - (percentageOdds / 10^20)
For example, if you post at percentageOdds = "52500000000000000000" (52.5% maker implied), the taker sees 47.5% implied odds on the opposite outcome. See Odds Formats for conversions between implied, American, and decimal odds.

Understanding totalBetSize

This is the maximum amount of tokens you would put into the pot if the order is fully filled. It’s in Ethereum units — for USDC (6 decimals), "100000000" = 100 USDC. A taker can partially fill your order. The remaining unfilled portion stays on the book.
The minimum maker order size is 10 USDC.

Example

import "dotenv/config";
import { Wallet, solidityPackedKeccak256, getBytes, randomBytes, hexlify } from "ethers";

const BASE_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
const wallet = new Wallet(process.env.SX_PRIVATE_KEY);

// 1. Build the order
const order = {
  marketHash: "0x8eeace4a9bbf6235bc59695258a419ed3a85a2c8e3b6a58fb71a0d9e6b031c2b",
  maker: wallet.address,
  baseToken: "0x6629Ce1Cf35Cc1329ebB4F63202F3f197b3F050B", // Mainnet USDC — see References for testnet address
  totalBetSize: "100000000", // 100 USDC
  percentageOdds: "52500000000000000000", // 52.5% maker implied
  expiry: 2209006800,
  apiExpiry: Math.floor(Date.now() / 1000) + 3600, // expires in 1 hour
  executor: "0x52adf738AAD93c31f798a30b2C74D658e1E9a562", // Mainnet — use GET /metadata → executorAddress for testnet
  salt: BigInt(hexlify(randomBytes(32))).toString(),
  isMakerBettingOutcomeOne: true,
};

// 2. Sign
const orderHash = getBytes(
  solidityPackedKeccak256(
    [
      "bytes32",  // marketHash
      "address",  // baseToken
      "uint256",  // totalBetSize
      "uint256",  // percentageOdds
      "uint256",  // expiry
      "uint256",  // salt
      "address",  // maker
      "address",  // executor
      "bool",     // isMakerBettingOutcomeOne
    ],
    [
      order.marketHash,
      order.baseToken,
      order.totalBetSize,
      order.percentageOdds,
      order.expiry,
      order.salt,
      order.maker,
      order.executor,
      order.isMakerBettingOutcomeOne,
    ]
  )
);
const signature = await wallet.signMessage(orderHash);

// For browser wallets (e.g. MetaMask), use an injected provider instead:
// const provider = new BrowserProvider(window.ethereum);
// const signer = await provider.getSigner();
// const signature = await signer.signMessage(orderHash);

// 3. Submit
const response = await fetch(`${BASE_URL}/orders/new`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ orders: [{ ...order, signature }] }),
}).then((r) => r.json());

console.log(response);

Response

A successful response looks like this:
{
  "status": "success",
  "data": {
    "orders": [
      "0x7a9d420551c4a635849013dd908f7894766e97aee25fe656d0c5ac857e166fac"
    ],
    "statuses": {
      "0x7a9d420551c4a635849013dd908f7894766e97aee25fe656d0c5ac857e166fac": "OK"
    },
    "inserted": 1
  }
}
Each order gets a status:
StatusDescription
OKOrder created successfully
INSUFFICIENT_BALANCEInsufficient maker token balance
INVALID_MARKETNon-unique marketHashes specified
ORDERS_ALREADY_EXISTThe order already exists

Common issues

Order rejected — odds not on ladder

Your percentageOdds must land on the odds ladder (currently 0.125% intervals). See Odds Rounding for how to validate and round your odds.

Order rejected — exposure too high

Your total exposure per market hash (sum of totalBetSize - fillAmount across all open orders on that market hash) must stay under your wallet balance for that token. Cancel some orders or add funds.

Order rejected — betting not enabled

You need to approve the TokenTransferProxy contract for each token you trade. See Enabling Betting.

apiExpiry tips

  • Set a reasonable expiry — orders left open indefinitely consume exposure
  • For volatile markets, use shorter expiries (minutes) and repost
  • For stable pre-game markets, longer expiries (hours) are fine
  • The expiry field is deprecated and must always be 2209006800

Odds Rounding →

Validate and round odds to the ladder.

POST /orders/new →

Full API reference for the endpoint.

Unit Conversion →

Converting between raw values and human-readable amounts.

Latency & Server Locations →

Expected response times and co-location recommendations.