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" },
],
};
| Field | Meaning |
|---|
salt | Random uint256 (as a decimal string) for uniqueness |
maker | The wallet placing the order. Must equal signer. |
signer | The signing wallet. Must equal the authenticated wallet. |
taker | Counterparty restriction. Use the zero address for an open order. |
tokenId | ERC-1155 token_id of the outcome you’re trading (from /tokens) |
makerAmount | What you give, in raw token wei. See amount math. |
takerAmount | What you receive, in raw token wei |
expiration | Unix seconds; 0 = no expiry. Required non-zero for GTD orders. |
nonce | Order nonce |
feeRateBps | Fee in basis points (100 = 1%) |
side | 0 = BUY, 1 = SELL |
signatureType | 0 = 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.