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:
- An API key and a JWT for the wallet. See Authentication.
- The approvals for the market's exchange.
- The market itself, from
GET /v1/markets/{id}. These are the fields an order uses:
| Field | What it's for |
|---|---|
outcomes[].onChainId | The order's tokenId, which is the outcome you buy or sell. On a binary market, indexSet 1 is Yes and 2 is No. |
feeRateBps | The fee rate you sign into the order. See Fees. |
isNegRisk, isYieldBearing | Which exchange settles the order, so which contract the order is signed for. |
decimalPrecision | How many decimals a price can have: 2 for steps of 0.01, 3 for steps of 0.001. |
tradingStatus | Whether the market accepts orders right now. See Markets. |
Limit and market orders
LIMIT | MARKET | |
|---|---|---|
| Price | The price you set. | The best prices in the book, within your slippage. |
| The unfilled part | Rests on the book until it fills, you remove it, or it expires. | Is cancelled at once. A market order never rests on the book. |
| Options | isPostOnly, selfTradePrevention | isFillOrKill, 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:makerAmountis the expected cost, andtakerAmountdrops 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:makerAmountgrows by the slippage, so you may pay more for the same shares. - Sell:
takerAmountdrops 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:
| Field | Strategy | What it does |
|---|---|---|
pricePerShare | Both | The price, as a wei string. Required. |
strategy | Both | LIMIT or MARKET. Required. |
isPostOnly | LIMIT only | The 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. |
isFillOrKill | MARKET only | The order must fill completely, or it's cancelled without a fill. |
slippageBps | MARKET only | The slippage the amounts allow, from 0 to 10000, as a string. |
isMinAmountOut | MARKET only | Tells the API that takerAmount is the minimum to receive. Send the value the SDK returned. |
reservedBalancePolicy | MARKET only | SKIP_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. |
selfTradePrevention | Both | What 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:
| Value | What happens |
|---|---|
CANCEL_TAKER | The new order is cancelled. Your resting orders stay. |
CANCEL_MAKER | Your resting orders that it would cross are cancelled. The new order still matches other users' orders. |
CANCEL_BOTH | The 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.
| Rule | Error 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.