GuidesTrading

Place orders

Build, sign and send limit and market orders, and learn the rules the API checks before it accepts one.

An order is an EIP-712 message that your wallet signs. You build and sign it with the SDK, then send it to POST /v1/orders. The order book matches it off-chain, and the exchange contract settles each match on-chain.

Before you start

You need three things:

FieldWhat it's for
outcomes[].onChainIdThe order's tokenId, which is the outcome you buy or sell. On a binary market, indexSet 1 is Yes and 2 is No.
feeRateBpsThe fee rate you sign into the order. See Fees.
isNegRisk, isYieldBearingWhich exchange settles the order, so which contract the order is signed for.
decimalPrecisionHow many decimals a price can have: 2 for steps of 0.01, 3 for steps of 0.001.
tradingStatusWhether the market accepts orders right now. See Markets.

Limit and market orders

LIMITMARKET
PriceThe price you set.The best prices in the book, within your slippage.
The unfilled partRests on the book until it fills, you remove it, or it expires.Is cancelled at once. A market order never rests on the book.
OptionsisPostOnly, selfTradePreventionisFillOrKill, slippageBps, isMinAmountOut, reservedBalancePolicy, selfTradePrevention

Place a limit order

getLimitOrderAmounts turns a price and a quantity into the order's amounts. The example buys 10 shares of the market's first outcome at 0.40 USDT each.

import { ChainId, OrderBuilder, Side } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const apiKey = "YOUR_API_KEY";
const jwt = "YOUR_JWT"; // from the Authentication guide
const marketId = "MARKET_ID";
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// the market's fee, its outcomes, and which exchange it trades on.
const marketResponse = await fetch(
  `https://api.predict.fun/v1/markets/${marketId}`,
  {
    headers: { "x-api-key": apiKey },
  },
);
const market = (await marketResponse.json()).data;

// buy 10 shares of the first outcome at 0.40 USDT each.
const { pricePerShare, makerAmount, takerAmount } =
  orderBuilder.getLimitOrderAmounts({
    side: Side.BUY,
    pricePerShareWei: 400000000000000000n, // 0.40 USDT
    quantityWei: 10000000000000000000n, // 10 shares
  });

const order = orderBuilder.buildOrder("LIMIT", {
  side: Side.BUY,
  tokenId: market.outcomes[0].onChainId,
  makerAmount,
  takerAmount,
  feeRateBps: market.feeRateBps,
});
const typedData = orderBuilder.buildTypedData(order, {
  isNegRisk: market.isNegRisk,
  isYieldBearing: market.isYieldBearing,
});
const signedOrder = await orderBuilder.signTypedDataOrder(typedData);
const hash = orderBuilder.buildTypedDataHash(typedData);

const response = await fetch("https://api.predict.fun/v1/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    Authorization: `Bearer ${jwt}`,
  },
  body: JSON.stringify({
    data: {
      order: { ...signedOrder, hash },
      pricePerShare: pricePerShare.toString(),
      strategy: "LIMIT",
    },
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.error}: ${result.message}`);
console.log(result.data); // { code, orderId, orderHash, removalLockedUntil }

A limit order has no expiration unless you set one. To set one, pass expiresAt (a Date) to buildOrder in TypeScript, or expires_at (a datetime) to BuildOrderInput in Python.

Place a market order

A market order's amounts come from the current book, so fetch it from GET /v1/markets/{id}/orderbook right before you order. getMarketOrderAmounts walks the book and returns the amounts, plus the pricePerShare to send.

You can size a market order in two ways:

  • By quantity, with quantityWei. This works for buys and sells.
  • By value, with valueWei, the most USDT to spend. This works for buys only.

The example spends up to 5 USDT on the market's first outcome and accepts up to 1% slippage.

import { ChainId, OrderBuilder, Side } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const apiKey = "YOUR_API_KEY";
const jwt = "YOUR_JWT"; // from the Authentication guide
const marketId = "MARKET_ID";
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

const headers = { "x-api-key": apiKey };
const market = (
  await (
    await fetch(`https://api.predict.fun/v1/markets/${marketId}`, { headers })
  ).json()
).data;
// the amounts come from the current book, so fetch it right before you order.
const book = (
  await (
    await fetch(`https://api.predict.fun/v1/markets/${marketId}/orderbook`, {
      headers,
    })
  ).json()
).data;

// spend up to 5 USDT on the first outcome, accepting up to 1% slippage.
const amounts = orderBuilder.getMarketOrderAmounts(
  {
    side: Side.BUY,
    valueWei: 5000000000000000000n, // 5 USDT
    slippageBps: 100n, // 1%
    isMinAmountOut: true,
  },
  book,
);

const order = orderBuilder.buildOrder("MARKET", {
  side: Side.BUY,
  tokenId: market.outcomes[0].onChainId,
  makerAmount: amounts.makerAmount,
  takerAmount: amounts.takerAmount,
  feeRateBps: market.feeRateBps,
});
const typedData = orderBuilder.buildTypedData(order, {
  isNegRisk: market.isNegRisk,
  isYieldBearing: market.isYieldBearing,
});
const signedOrder = await orderBuilder.signTypedDataOrder(typedData);
const hash = orderBuilder.buildTypedDataHash(typedData);

const response = await fetch("https://api.predict.fun/v1/orders", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
    Authorization: `Bearer ${jwt}`,
  },
  body: JSON.stringify({
    data: {
      order: { ...signedOrder, hash },
      pricePerShare: amounts.pricePerShare.toString(),
      strategy: "MARKET",
      // send the slippage settings the amounts were computed with.
      slippageBps: amounts.slippageBps.toString(),
      isMinAmountOut: amounts.isMinAmountOut,
    },
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.error}: ${result.message}`);
console.log(result.data);

The SDK sets a market order to expire 5 minutes after you build it, so build it right before you send it.

Slippage

Slippage is a tolerance in basis points, where 100 is 1%. It changes the amounts the SDK signs:

  • Buy with isMinAmountOut: makerAmount is the expected cost, and takerAmount drops by the slippage. So you spend at most what you planned and accept a few shares less. This lets you spend your whole balance.
  • Buy without isMinAmountOut: makerAmount grows by the slippage, so you may pay more for the same shares.
  • Sell: takerAmount drops by the slippage, so you accept a little less USDT.

The API applies slippage only when you send the slippageBps and isMinAmountOut the SDK returned. If the book moves beyond your slippage before the order matches, the order is cancelled without a fill. You get an orderNotAccepted event with the reason noMarketMatch. See Order lifecycle.

The request

The body wraps the signed order and its settings in data:

{
  "data": {
    "order": {
      "hash": "0x…",
      "salt": "…",
      "maker": "0x…",
      "signer": "0x…",
      "taker": "0x0000000000000000000000000000000000000000",
      "tokenId": "…",
      "makerAmount": "4000000000000000000",
      "takerAmount": "10000000000000000000",
      "expiration": "4102444800",
      "nonce": "0",
      "feeRateBps": "200",
      "side": 0,
      "signatureType": 0,
      "signature": "0x…"
    },
    "pricePerShare": "400000000000000000",
    "strategy": "LIMIT"
  }
}

The order fields are the signed EIP-712 order. Send them exactly as the SDK returns them, plus its hash. For a wallet, maker and signer are the wallet's address. For a Predict account, the SDK sets both to the account's address. side is 0 to buy and 1 to sell.

These are the settings next to the order:

FieldStrategyWhat it does
pricePerShareBothThe price, as a wei string. Required.
strategyBothLIMIT or MARKET. Required.
isPostOnlyLIMIT onlyThe order only adds liquidity. If it would match right away, it isn't placed in the book, and you get an orderNotAccepted event with the reason rejectedPostOnly.
isFillOrKillMARKET onlyThe order must fill completely, or it's cancelled without a fill.
slippageBpsMARKET onlyThe slippage the amounts allow, from 0 to 10000, as a string.
isMinAmountOutMARKET onlyTells the API that takerAmount is the minimum to receive. Send the value the SDK returned.
reservedBalancePolicyMARKET onlySKIP_RESERVED_BALANCE_CHECKS checks the order against your balance and allowance on its own, ignoring the funds your open orders already reserve. Leave it out for the default check, which counts them.
selfTradePreventionBothWhat happens when the order would match one of your own orders. See below.

Self-trade prevention

Without selfTradePrevention, your own orders can match each other. With it, they never do. The value decides what happens to the orders of yours that the new order would cross:

ValueWhat happens
CANCEL_TAKERThe new order is cancelled. Your resting orders stay.
CANCEL_MAKERYour resting orders that it would cross are cancelled. The new order still matches other users' orders.
CANCEL_BOTHThe new order and your resting orders that it would cross are cancelled.

A cancelled order gets an orderCancelled event with the reason selfTradePrevention.

Amounts and prices

Prices and amounts are integer strings in wei, where 10^18 is one unit. A price of 400000000000000000 is 0.40 USDT, and an amount of 10000000000000000000 is 10 shares. A decimal string such as "0.4" is rejected.

For a buy, makerAmount is the USDT you pay and takerAmount is the shares you get. For a sell, it's the other way around. Buying 10 shares at 0.40 USDT signs a makerAmount of 4 USDT and a takerAmount of 10 shares.

A limit order's amounts must match its price exactly, and the price can have at most decimalPrecision decimals. The SDK helpers compute amounts that match the price, so pass the pricePerShare they return. They don't know the market's decimalPrecision, so round the price to it before you call them.

Rules the API checks

The API rejects an order that breaks one of these rules. The error codes are listed on Errors.

RuleError code
A buy is worth at least 1 USDT.create_order_min_order_value_not_met
A limit sell is at least 1 share. A market sell is at least 0.01 share.create_order_min_order_size_not_met
The expiration is at least 2 minutes away, and no later than January 1, 2100.create_order_expiry_too_soon, create_order_expiry_too_far
The price is above 0 and below 1. A limit order's price has at most decimalPrecision decimals.create_order_price_out_of_range, create_order_price_precision_too_high
The amounts match the price.create_order_limit_amounts_mismatch, create_order_market_amounts_mismatch
The signed feeRateBps is at least the market's feeRateBps, less any referral discount.create_order_fee_rate_too_low
The maker is the signer.create_order_maker_signer_mismatch
Each option is used with its strategy, as in the table above.create_order_post_only_not_supported, create_order_fill_or_kill_not_supported, create_order_slippage_non_market, create_order_reserved_balance_policy_invalid
The market is trading.create_order_market_not_trading
The same order isn't sent twice.create_order_duplicate_order
Your balance and approvals cover the order. See Approvals.create_order_insufficient_collateral_balance, create_order_insufficient_shares_balance, and the approval errors

While a market's tradingStatus is MATCHING_NOT_ENABLED, the book takes orders but doesn't match them. Only post-only limit orders are accepted then. Other limit orders get create_order_trading_disabled, and market orders get create_order_market_orders_disabled.

The response

An accepted order returns 201:

{
  "success": true,
  "data": {
    "code": "OK",
    "orderId": "123456789",
    "orderHash": "0x…",
    "removalLockedUntil": null
  }
}

Keep the orderId, because you need it to remove the order. removalLockedUntil is a time on some up/down markets, which have a speed bump. On those markets, an order that isn't post-only waits a short delay before it reaches the book, and you can't remove it before removalLockedUntil. On other markets, and for post-only orders, it's null.

A 201 means the order passed its first checks. It isn't filled yet, and it isn't final. The API finishes some balance and allowance checks after it responds, and cancels the order if they fail. Follow the order's wallet events to see what happens to it.

On this page