Stop & take orders
A stop or take order waits until the mark price of a property reaches a price that you set. This price is the trigger price. When the mark price reaches it, Loaf places a limit order for you. The API calls these orders conditional orders.
Use them to:
- Limit a loss on a position (stop-loss).
- Lock in a profit on a position (take-profit).
- Buy when the price rises above a level, or falls to a level.
Loaf does not reserve your USDC or tokens when you create a stop or take order. Loaf reserves them only when the order triggers. Authenticate with your API key from the web app API section.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/orders/conditional | Create a stop or take order |
POST | /api/orders/conditional/cancel | Cancel a stop or take order |
POST | /api/orders/ | Create a buy with a take-profit and a stop-loss attached (tpPrice, slPrice) |
How stop and take orders work
Order types
| Type | When the order triggers, Loaf places |
|---|---|
STOP_MARKET | A limit order at a protected price, calculated from the trigger price |
TAKE_MARKET | A limit order at a protected price, calculated from the trigger price |
STOP_LIMIT | A limit order at the price that you set |
TAKE_LIMIT | A limit order at the price that you set |
The type and the side set the direction. The type alone does not:
| Type | A BUY triggers when the mark price | A SELL triggers when the mark price |
|---|---|---|
STOP_MARKET, STOP_LIMIT | Rises to the trigger price or above | Falls to the trigger price or below |
TAKE_MARKET, TAKE_LIMIT | Falls to the trigger price or below | Rises to the trigger price or above |
Common uses:
| Goal | Type and side | Trigger price |
|---|---|---|
| Stop-loss on a position | STOP_MARKET SELL | Below the mark price |
| Take-profit on a position | TAKE_MARKET SELL | Above the mark price |
| Buy when the price rises above a level | STOP_MARKET BUY | Above the mark price |
| Buy when the price falls to a level | TAKE_MARKET BUY | Below the mark price |
Mark price
Loaf compares the trigger price with the mark price of the property. This is the same price that the markprice:{tokenName} WebSocket channel sends. The mark price is the midpoint between the best bid and the best ask.
An order triggers when the mark price is at the trigger price or past it, in the direction of the order. The mark price does not have to cross the trigger price while you watch. For example, if the market opens with the mark price already past your trigger price, your order triggers at the open.
Price of the limit order
Loaf sets the price of the limit order when you create the stop or take order. This price does not change.
STOP_LIMITandTAKE_LIMIT: the price is thepricethat you send.STOP_MARKETandTAKE_MARKET: sendprice: 0. Loaf calculates a protected price from the trigger price andmaxSlippageBps, the slippage limit for market orders. GetmaxSlippageBpsfrom Property market detail. Loaf rounds the result to the nearest cent.
| Side | Protected price |
|---|---|
BUY | triggerPrice × (1 + maxSlippageBps / 10000) |
SELL | triggerPrice × (1 - maxSlippageBps / 10000) |
Example: maxSlippageBps is 300 (3%). A STOP_MARKET SELL with a trigger price of 90 gets a protected price of 87.30.
A STOP_MARKET or TAKE_MARKET order is not a market order. When it triggers, Loaf places a limit order at a fixed price. If the market moves past that price quickly, the limit order stays on the book. It does not fill until the market comes back to its price.
Status
| Status | Meaning |
|---|---|
PENDING | A take-profit or stop-loss that is attached to a buy. It waits for the buy to fill completely. |
ARMED | Active. Loaf compares the trigger price with the mark price. |
PLACED | Triggered. Loaf placed the limit order. placedOrderId is the id of that order. |
FAILED | Loaf could not place the limit order. rejectionReason gives the reason. |
CANCELLED | Cancelled by you, by Cancel all orders, or by the linked order (see Attach take-profit and stop-loss to a buy). |
PLACED, FAILED, and CANCELLED are final. After PLACED, follow the limit order placedOrderId for fills. In rare cases, the exchange rejects that limit order. Then the status changes from PLACED to FAILED.
A rejectionReason on an ARMED order shows a temporary problem. The order is still active, and Loaf tries again. It is not a failure.
When the order triggers
- Loaf places a limit order with the side, price, and quantity of the stop or take order.
- The limit order must pass the same checks as an order that you place yourself. Examples are the daily price band and the minimum order value.
- If your USDC or tokens are locked in your own open orders on the same side of the same property, Loaf cancels those orders first. Then it places the limit order.
If a check fails, or you do not have enough USDC or tokens, the status changes to FAILED. Loaf does not try again.
A stop or take order can cancel your other orders when it triggers. For example, if a stop-loss triggers while your limit sell is on the book, Loaf cancels the limit sell to free the tokens. You then get CANCELLED updates for orders that you did not cancel. Loaf does not cancel your orders on other properties.
Things to know
- You cannot change a stop or take order. To change it, cancel it and create a new one.
- The quantity is fixed. If your tokens fall below the quantity of a
SELLorder, the order changes toFAILED. This can occur before the mark price reaches the trigger price. Create a new order for the new size. - Halts and market hours. Stop and take orders do not trigger while trading is halted or the market is closed. When trading starts again, an order triggers immediately if the mark price is at or past its trigger price.
- Competition rounds. When a trading competition round starts or ends, Loaf removes all stop and take orders on all properties. The status does not change to
CANCELLED, and you do not get an update. Create your orders again after the change. - Open-order limits.
PENDINGandARMEDorders count toward your open-order limits, together with your other open orders. - Separate orders are not linked. A stop-loss and a take-profit that you create separately do not cancel each other. To get a linked pair, attach them to a buy.
Create a stop or take order
POST /api/orders/conditional
Creates a stop or take order for the authenticated user. The order starts with status ARMED.
Request body
Type: ConditionalOrderRequestBody (@loafmarkets/shared-types)
interface ConditionalOrderRequestBody {
tokenName: string; // lowercase slug, e.g. "opera" — not the ticker
price: number; // 0 for STOP_MARKET and TAKE_MARKET
quantity: number;
side: 'BUY' | 'SELL';
type: 'STOP_MARKET' | 'STOP_LIMIT' | 'TAKE_MARKET' | 'TAKE_LIMIT';
triggerPrice: number;
timeInForce: 'GTC';
deadline: number; // always 0
}Request
import axios from 'axios';
import type { ConditionalOrderRequestBody } from '@loafmarkets/shared-types';
// Stop-loss: sell 5 tokens if the mark price falls to 90
const body: ConditionalOrderRequestBody = {
tokenName: 'opera',
price: 0,
quantity: 5,
side: 'SELL',
type: 'STOP_MARKET',
triggerPrice: 90,
timeInForce: 'GTC',
deadline: 0,
};
const response = await axios.post('https://api.loafmarkets.com/api/orders/conditional', body, {
headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` },
});
console.log(response.data);Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenName | string | Yes | (body) Lowercase property tokenName (letters a-z only, max 20). Not the ticker. |
price | number | Yes | (body) Limit price in USDC for STOP_LIMIT and TAKE_LIMIT (max 2 decimal places). Use 0 for STOP_MARKET and TAKE_MARKET. |
quantity | number | Yes | (body) Order size in tokens (max 1 decimal place). |
side | 'BUY' | 'SELL' | Yes | (body) Order side. The side and the type set the direction. See Order types. |
type | 'STOP_MARKET' | 'STOP_LIMIT' | 'TAKE_MARKET' | 'TAKE_LIMIT' | Yes | (body) Order type. |
triggerPrice | number | Yes | (body) Mark price in USDC that triggers the order (max 2 decimal places). |
timeInForce | 'GTC' | Yes | (body) Time in force (always GTC). |
deadline | number | Yes | (body) Always 0. |
Loaf refuses a trigger price that the mark price has already reached. A trigger price equal to the mark price is also refused. Before you send the order, compare the trigger price with the mark price from markprice:{tokenName}. If the mark price is already there, place a normal order.
When you create the order, Loaf also checks that:
- For a
SELL, you hold at leastquantitytokens of the property. Tokens that are locked in your open orders count. - For a
BUY, you have enough USDC forprice × quantityplus the taker fee. USDC that is locked in your open orders counts. - The order value (
price × quantity) is at least 10 USDC. ASELLof your whole position is exempt. - You are below your open-order limits.
For STOP_MARKET and TAKE_MARKET, these checks use the protected price, not the trigger price. Loaf checks your balance again when the order triggers.
Response
Type: OrderApiResponse
interface OrderApiResponse {
success: boolean;
orderId: number;
errorMessage?: string;
}Example response:
{
"success": true,
"orderId": 9101
}orderId is the id of the stop or take order. Use it to cancel the order and to find the order in your order lists. Cancel order does not accept this id.
A 200 response means that the order is ARMED. It does not mean that the order will trigger or fill. If the order fails when it triggers, you get the reason in rejectionReason, not as an error from this endpoint. See Track your stop and take orders.
Errors
| Status | Code | Description |
|---|---|---|
400 | validation_error | Request failed schema validation. For example, price is not 0 for a STOP_MARKET order, or a price has more than 2 decimal places. |
400 | — | Order refused. The mark price already reached the trigger price, you do not have enough tokens or USDC, the order value is below the minimum, or you reached an open-order limit. The error message gives the reason. |
401 | unauthorized | Missing or invalid authentication. |
403 | — | Trading closed, halted, ineligible for competition, or world gate rejected the property. |
404 | not_found | Unknown tokenName. |
500 | internal_error | Unexpected server error. |
If the mark price already reached the trigger price, the message is Order would trigger immediately: the mark price is already through the trigger price.
Requires API key authentication.
Attach take-profit and stop-loss to a buy
POST /api/orders/
Add tpPrice, slPrice, or both to a BUY order. For the other fields, see Create order. Loaf creates a SELL for the full quantity of the buy:
| Field | Creates |
|---|---|
tpPrice | A take-profit: TAKE_MARKET SELL with trigger price tpPrice |
slPrice | A stop-loss: STOP_MARKET SELL with trigger price slPrice |
The two orders are linked. When one of them triggers, Loaf cancels the other one at the same time. This is the only way to get a linked pair.
Rules
- You can attach them only to a
BUY. tpPricemust be aboveslPrice.LIMITbuy:slPrice < price < tpPrice.MARKETbuy:slPrice < mark price < tpPrice. Loaf uses the mark price before slippage.- Prices can have at most 2 decimal places.
- Do not send a field that you do not use. Do not send
null.
Request
import axios from 'axios';
import type { OrderRequestBody } from '@loafmarkets/shared-types';
// Limit buy: 10 tokens @ 100, take-profit at 120, stop-loss at 90
const body: OrderRequestBody = {
tokenName: 'opera',
price: 100,
quantity: 10,
side: 'BUY',
type: 'LIMIT',
timeInForce: 'GTC',
deadline: 0,
tpPrice: 120,
slPrice: 90,
};
const response = await axios.post('https://api.loafmarkets.com/api/orders/', body, {
headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` },
});
console.log(response.data); // orderId is the id of the buyWhat happens next
- Loaf creates the take-profit and the stop-loss with status
PENDING. They cannot trigger yet. - When the buy fills completely, both change to
ARMED. This occurs a few seconds after the last fill. - When one of them triggers, Loaf cancels the other one.
If the buy does not fill completely:
| The buy ends | The take-profit and the stop-loss |
|---|---|
| With no fill (for example, you cancel it) | Change to CANCELLED. |
| Partially filled (for example, you cancel the rest) | Change to FAILED. Their quantity is larger than your position. Create new orders for the filled quantity. |
While the buy is partially filled, the tokens that you already bought are not protected. The take-profit and the stop-loss stay PENDING until the buy fills completely.
The response contains only the orderId of the buy. To find the take-profit and the stop-loss, look for orders with parentOrderId equal to that orderId. They are in openOrders and in order_update messages. See Track your stop and take orders.
MARKET buy with tpPrice. Loaf checks tpPrice against the mark price before slippage, but the buy can fill above the mark price. If the buy fills at or above tpPrice, the take-profit can trigger as soon as it is armed and sell at a loss. The trigger price is not a minimum sale price, and it does not include fees. Set tpPrice well above the mark price, or use a LIMIT buy.
Errors
| Status | Code | Description |
|---|---|---|
400 | validation_error | tpPrice or slPrice breaks a rule above. For example, the order is a SELL, or slPrice is not below price. |
400 | — | MARKET buy: slPrice is not below the mark price, or tpPrice is not above it. |
All other errors are the same as for Create order.
Cancel a stop or take order
POST /api/orders/conditional/cancel
Cancels one stop or take order with status PENDING or ARMED. This endpoint also works while trading is halted.
After the status changes to PLACED, the order on the book is a normal limit order. To cancel it, send its placedOrderId to Cancel order.
Request body
Type: CancelOrderRequestBody (@loafmarkets/shared-types)
interface CancelOrderRequestBody {
orderId: number; // id of the stop or take order
}Request
import axios from 'axios';
import type { CancelOrderRequestBody } from '@loafmarkets/shared-types';
const body: CancelOrderRequestBody = {
orderId: 9101,
};
const response = await axios.post('https://api.loafmarkets.com/api/orders/conditional/cancel', body, {
headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` },
});
console.log(response.data);Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
orderId | number | Yes | (body) Id of the stop or take order. |
Response
Type: CancelOrderResponse
interface CancelOrderResponse {
success: boolean;
orderId: number;
errorMessage?: string;
}Example response:
{
"success": true,
"orderId": 9101
}Errors
| Status | Code | Description |
|---|---|---|
400 | validation_error | Request failed schema validation. |
400 | — | The order is already PLACED, FAILED, or CANCELLED. |
401 | unauthorized | Missing or invalid authentication. |
404 | not_found | No stop or take order with this id on your account. The id of a normal order also gives 404. |
500 | internal_error | Unexpected server error. |
Requires API key authentication. Cancel all orders also cancels your PENDING and ARMED stop and take orders. During a platform-wide trading halt, Cancel order and Cancel all orders return 403, but this endpoint still works. Use it to stop an order from triggering when trading starts again.
Track your stop and take orders
Stop and take orders are in the same lists as your other orders:
| Where | What you get |
|---|---|
openOrders in Portfolio component | Stop and take orders with status PENDING or ARMED |
| Order history | Stop and take orders with all statuses |
order_update on your private portfolio:{userId} channel | One message for each status change |
Active orders and Cancelled orders do not include stop and take orders.
Use type to find stop and take orders in these lists. A stop or take order has one of the four types above. It does not have the filledQuantity and filledAt fields.
Type: ConditionalOrderHistoryItem (@loafmarkets/shared-types)
interface ConditionalOrderHistoryItem {
id: number; // id of the stop or take order
propertyId: number;
tokenName: string;
side: 'BUY' | 'SELL';
type: 'STOP_MARKET' | 'STOP_LIMIT' | 'TAKE_MARKET' | 'TAKE_LIMIT';
status: 'PENDING' | 'ARMED' | 'PLACED' | 'FAILED' | 'CANCELLED';
timeInForce: 'GTC';
quantity: number;
price: number; // price of the limit order that Loaf places
triggerPrice: number;
rejectionReason: string | null;
deadline: number;
parentOrderId: number | null; // the buy that a take-profit or stop-loss is attached to
placedOrderId: number | null; // the limit order that Loaf placed
triggeredAt: number | null; // unix seconds
cancelledAt: number | null; // unix seconds
createdAt: number; // unix seconds
}Example order_update message:
{
"type": "order_update",
"order": {
"id": 9101,
"propertyId": 42,
"tokenName": "opera",
"side": "SELL",
"type": "STOP_MARKET",
"status": "PLACED",
"timeInForce": "GTC",
"quantity": 5,
"price": 87.3,
"triggerPrice": 90,
"rejectionReason": null,
"deadline": 0,
"parentOrderId": null,
"placedOrderId": 9150,
"triggeredAt": 1758600000,
"cancelledAt": null,
"createdAt": 1758500000
},
"timestamp": 1758600000
}import { isConditionalOrder, type OrderHistoryItem } from '@loafmarkets/shared-types';
function onOrderUpdate(order: OrderHistoryItem) {
if (isConditionalOrder(order)) {
console.log(order.type, order.status, order.triggerPrice, order.rejectionReason);
} else {
console.log(order.id, order.status, order.filledQuantity);
}
}