Skip to main content
Orders are signed off-chain with EIP-712 typed data and submitted to POST /v1/orders. This page covers exactly what to sign and how to compute the amounts so the matching engine accepts your order.
Amount math is unforgiving. maker_amount and taker_amount must be derived with BigInt integer-tick math at the collateral’s decimals (18 on BSC), with BUY ceiling and SELL flooring the collateral. Anything signed with float arithmetic or the wrong decimals is rejected at ingest with Drifted signed amounts.

EIP-712 domain

const domain = {
  name: "ForesightExchange",
  version: "1",
  chainId: 56,                              // BSC mainnet
  verifyingContract: market.ctf_exchange_address, // from GET /v1/markets/{id}
};
verifyingContract is the market’s ctf_exchange_address — read it from the market object. Do not hard-code it; it can differ per chain.

The Order struct

The typed-data primary type is Order, with these 12 fields in this order:
const types = {
  Order: [
    { name: "salt",          type: "uint256" },
    { name: "maker",         type: "address" },
    { name: "signer",        type: "address" },
    { name: "taker",         type: "address" },
    { name: "tokenId",       type: "uint256" },
    { name: "makerAmount",   type: "uint256" },
    { name: "takerAmount",   type: "uint256" },
    { name: "expiration",    type: "uint256" },
    { name: "nonce",         type: "uint256" },
    { name: "feeRateBps",    type: "uint256" },
    { name: "side",          type: "uint8"   },
    { name: "signatureType", type: "uint8"   },
  ],
};
FieldMeaning
saltRandom uint256 (as a decimal string) for uniqueness
makerThe wallet placing the order. Must equal signer.
signerThe signing wallet. Must equal the authenticated wallet.
takerCounterparty restriction. Use the zero address for an open order.
tokenIdERC-1155 token_id of the outcome you’re trading (from /tokens)
makerAmountWhat you give, in raw token wei. See amount math.
takerAmountWhat you receive, in raw token wei
expirationUnix seconds; 0 = no expiry. Required non-zero for GTD orders.
nonceOrder nonce
feeRateBpsFee in basis points (100 = 1%)
side0 = BUY, 1 = SELL
signatureType0 = EOA, 1 = EIP-1271 (smart-contract wallet)
The order kind (LIMIT / MARKET / GTD) is not part of the signed struct — it travels on the REST request body only as order_type. The signed struct carries side as a number, not the human-readable string.

Outcome → token

Pick the token_id for the outcome you want from GET /v1/markets/{condition_id}/tokens:
{
  "tokens": [
    { "outcome": 0, "outcome_label": "NO",  "token_id": "1234...", "decimals": 18 },
    { "outcome": 1, "outcome_label": "YES", "token_id": "5678...", "decimals": 18 }
  ]
}
To buy YES, sign with outcome: 1 and the YES token_id.

Prices are 2-decimal ticks

A price is a YES probability in [0.01, 1.00], quantized to 2 decimal places (100 ticks). Sign at sub-tick precision and the on-chain crossing check can reject the match, so round first:
// "0.7657" -> "0.77"  (round half up to 2dp)
function quantizePrice(price: string): string {
  const [w = "0", f = ""] = price.split(".");
  if (f.length <= 2) return `${BigInt(w)}.${(f + "00").slice(0, 2)}`;
  let ticks = BigInt(w) * 100n + BigInt(f.slice(0, 2));
  if (f[2] >= "5") ticks += 1n;
  return `${ticks / 100n}.${(ticks % 100n).toString().padStart(2, "0")}`;
}

Amount math

Read decimals from the market’s /tokens response (18 on BSC). Then, with price (already quantized) and size as decimal strings:
BUY:   makerAmount = ceil(size × price)   (collateral you pay)
       takerAmount = size                 (shares you receive)

SELL:  makerAmount = size                 (shares you sell)
       takerAmount = floor(size × price)  (collateral you receive)
All values are scaled to the collateral’s decimals. BUY ceils the collateral and SELL floors it — this asymmetric rounding is what keeps the on-chain crossing check satisfied. It costs at most 1 wei.
const DECIMALS = 18n; // BSC — confirm via /tokens

function toWei(decimal: string, decimals: bigint): bigint {
  const [w = "0", f = ""] = decimal.split(".");
  const frac = (f + "0".repeat(Number(decimals))).slice(0, Number(decimals));
  return BigInt(w) * 10n ** decimals + BigInt(frac || "0");
}

function computeAmounts(side: "BUY" | "SELL", price: string, size: string, decimals: bigint) {
  const sizeWei = toWei(size, decimals);
  const priceWei = toWei(price, decimals);
  const denom = 10n ** decimals;
  const collateral =
    side === "BUY"
      ? (sizeWei * priceWei + denom - 1n) / denom // ceil
      : (sizeWei * priceWei) / denom;             // floor
  return side === "BUY"
    ? { makerAmount: collateral, takerAmount: sizeWei }
    : { makerAmount: sizeWei, takerAmount: collateral };
}
Stay in BigInt the entire way. Number, parseFloat, and * price on a float silently corrupt 18-decimal amounts by a few wei, which the ingest validator rejects. Compute collateral as size × price at full scale, then ceil (BUY) or floor (SELL).

Complete example (viem)

import { createWalletClient, custom } from "viem";

const account = "0xYourWallet...";
const market = await (
  await fetch("https://api.foresight.now/v1/markets/" + conditionId + "?chain_id=56")
).json();
const { tokens } = await (
  await fetch(
    "https://api.foresight.now/v1/markets/" + conditionId + "/tokens?chain_id=56"
  )
).json();

const outcome = 1; // YES
const yes = tokens.find((t) => t.outcome === outcome);
const decimals = BigInt(yes.decimals); // 18 on BSC

const side = "BUY";
const price = quantizePrice("0.55");
const size = "100";
const { makerAmount, takerAmount } = computeAmounts(side, price, size, decimals);

const struct = {
  salt: randomUint256(),               // e.g. crypto.getRandomValues -> decimal string
  maker: account,
  signer: account,                     // maker must equal signer
  taker: "0x0000000000000000000000000000000000000000",
  tokenId: yes.token_id,
  makerAmount: makerAmount.toString(),
  takerAmount: takerAmount.toString(),
  expiration: "0",                     // no expiry
  nonce: "0",
  feeRateBps: "100",
  side: side === "BUY" ? 0 : 1,
  signatureType: 0,                    // EOA
};

const walletClient = createWalletClient({ account, transport: custom(window.ethereum) });

const signature = await walletClient.signTypedData({
  account,
  domain: {
    name: "ForesightExchange",
    version: "1",
    chainId: 56,
    verifyingContract: market.ctf_exchange_address,
  },
  types: {
    Order: [
      { name: "salt", type: "uint256" },
      { name: "maker", type: "address" },
      { name: "signer", type: "address" },
      { name: "taker", type: "address" },
      { name: "tokenId", type: "uint256" },
      { name: "makerAmount", type: "uint256" },
      { name: "takerAmount", type: "uint256" },
      { name: "expiration", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "feeRateBps", type: "uint256" },
      { name: "side", type: "uint8" },
      { name: "signatureType", type: "uint8" },
    ],
  },
  primaryType: "Order",
  message: struct,
});

Submit the order

The request body wraps the signed struct in snake_case, plus the human-readable order_type, outcome, price, and size:
curl -X POST https://api.foresight.now/v1/orders \
  -H "fs-api-key: $FS_API_KEY" \
  -H "fs-api-secret: $FS_API_SECRET" \
  -H "fs-idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [{
      "condition_id": "0x...",
      "chain_id": 56,
      "side": "BUY",
      "order_type": "LIMIT",
      "outcome": 1,
      "price": "0.55",
      "size": "100",
      "salt": "...",
      "signer": "0xYourWallet...",
      "taker": "0x0000000000000000000000000000000000000000",
      "token_id": "5678...",
      "maker_amount": "...",
      "taker_amount": "...",
      "expiration": "0",
      "nonce": "0",
      "fee_rate_bps": 100,
      "signature_type": 0,
      "signature": "0x..."
    }]
  }'
You can batch up to 15 orders per request. The response always returns status: OPEN for accepted orders — matching is asynchronous:
{
  "results": [
    {
      "order_hash": "0x...",
      "condition_id": "0x...",
      "chain_id": 56,
      "status": "OPEN",
      "side": "BUY",
      "outcome": 1,
      "price": "0.55",
      "size": "100",
      "remaining_size": "100",
      "fills": [],
      "book_added_at": null,
      "created_at": "2026-06-03T..."
    }
  ]
}
Each item in results[] is either a success or an inline { "error": { "code", "message" } }. A mixed array is normal — always inspect every item. Fills and status transitions arrive on the user WebSocket channel, not here.

Idempotency

Set an fs-idempotency-key header on POST /v1/orders to make retries safe. Replaying the same key + same body returns the cached result with _idempotent_replayed: true. The same key + a different body returns IDEMPOTENCY_CONFLICT (409). Idempotency is honored on order placement only.

Cancelling

  • POST /v1/orders/cancel — cancel by hash (up to 100), by filter (condition_id + chain_id + optional outcome/side), or all (empty body).
  • POST /v1/orders/{order_hash}/cancel-onchain — returns unsigned calldata for a hard on-chain cancel against the CTF Exchange. You submit and pay for that transaction yourself; the backend does not send it.