Skip to main content
Trading workflows require an authenticated session.
Perps trading changes account exposure on an instrument. An order expresses what the account wants to do, but exposure only changes when the order fills. An accepted order can rest on the book before it fills. Use order state to track accepted, resting, modified, canceled, or rejected orders. Use fills and portfolio state to confirm any exposure change.

Start an Authenticated Session

Open an authenticated session before reading private account state or submitting trading commands. See Authenticated Sessions for the full setup flow.
Create a SecureClient.
import { createSecureClient } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";

const client = await createSecureClient({
  wallet: process.env.POLYMARKET_WALLET_ADDRESS!,
  signer: privateKey(process.env.PRIVATE_KEY!),
});
Open the Perps session.
const session = await client.openPerpsSession();

Prepare a Trade

Start by identifying the Perps instrument you want to trade. Instruments define the market and include the constraints used later when building an order.
Fetch instruments and select the market you want to trade.
const instruments = await client.fetchPerpsInstruments();
const instrument = instruments.find((item) => item.symbol === "BTC-PERP");

if (!instrument) {
  throw new Error("Instrument not found");
}
Keep the selected instrument available for the next steps.

Choose Direction and Size

Before placing an order, decide whether it should open new exposure, increase existing exposure, reduce exposure, or close the position. The effect of a buy or sell depends on the current position.
Current positionBuy orderSell order
No positionOpens longOpens short
LongIncreases longReduces or closes long
ShortReduces or closes shortIncreases short
To close a position, submit an order in the opposite direction for the current open position size.
Read the portfolio when the order decision depends on current exposure.
const portfolio = await session.fetchPortfolio();
const position = portfolio.positions.find(
  (item) => item.instrumentId === instrument.id,
);
Positive position.size means the account is long. Negative position.size means the account is short. No matching position means the account has no open exposure for that instrument.

Place Orders

Place an order when the account is ready to express buy or sell intent. Use an explicit limit price when you need price protection. Use immediate-or-cancel execution when the order should fill immediately or cancel any unfilled quantity.
1

Choose a Price

Fetch a ticker when you need a lightweight current-price reference. Fetch the book when the order price depends on spread, depth, or top-of-book liquidity.
const ticker = await client.fetchPerpsTicker({ instrumentId: instrument.id });
const book = await client.fetchPerpsBook({ instrumentId: instrument.id });
Set price as pUSD per quantity unit, rounded to instrument.priceDecimals.
2

Build the Order

Create the order request from the direction, price, quantity, and execution behavior you chose. This example submits an immediate-or-cancel (IOC) buy order for 0.01 BTC-PERP at 65000 pUSD per quantity unit.
import {
  OrderSide,
  PerpsTimeInForce,
  type PlacePerpsOrderRequest,
} from "@polymarket/client";

const request: PlacePerpsOrderRequest = {
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.IOC,
};
  • side sets direction based on how you intend to open, increase, reduce, or close the position.
  • price is the price you chose in the first step, in pUSD per quantity unit.
  • quantity is the number of quantity units. Use no more than instrument.quantityDecimals decimal places.
Use clientOrderId when your integration needs its own identifier for tracking or canceling the order.
const request: PlacePerpsOrderRequest = {
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.IOC,
  clientOrderId: "7f9e4a2b6c8d0e1f1234567890abcdef",
};
3

Submit the Order

Submit the request with placeOrder. It resolves with the matching private order update after the order is accepted.
const order = await session.placeOrder(request);
Use the returned order to track whether the order filled, expired, canceled, or remains on the book.
type PerpsOrder = {
  id: PerpsOrderId;
  instrumentId: PerpsInstrumentId;
  side: OrderSide;
  price: string;
  quantity: string;
  timeInForce: PerpsTimeInForce;
  postOnly: boolean;
  status: PerpsOrderStatus;
  restingQuantity: string;
  filledQuantity: string;
  createdTimestamp: number;
  updatedTimestamp: number;
  clientOrderId?: string;
  tpSl?: PerpsTpSlOrderFields;
};
For an IOC order, status can be one of these values.
StatusDescription
PerpsOrderStatus.FilledThe order fully filled.
PerpsOrderStatus.IocNoFillThe order did not fill.
PerpsOrderStatus.IocExpiredThe order filled partially and the remainder canceled.
PerpsOrderStatus.StpCancelledThe order would have matched your own resting order, so it canceled.

Take Profit & Stop Loss

Use take-profit and stop-loss (TP/SL) orders to place conditional exits for a position. A take-profit order exits when price moves in your favor. A stop-loss order exits when price moves against you.

How Triggers Work

TP/SL orders watch the mark price, not the last traded price. A trigger fires when the mark price touches the trigger price, then submits a reduce-only order to close exposure.
PositionTake-profit fires whenStop-loss fires when
LongMark rises to triggerMark falls to trigger
ShortMark falls to triggerMark rises to trigger
For a long position, take-profit triggers usually sit above the current mark and stop-loss triggers usually sit below it. For a short position, the directions are reversed.

Market and Limit Closes

Choose how the exit should execute after the trigger fires.
Close TypeWhat HappensAvailable For
MarketThe exit executes immediately against available liquidity.Bracket orders and positions
LimitThe exit places a limit order and can remain open until it fills.Bracket orders only

Place a Bracket Order

A bracket order submits one entry order with up to one take-profit and one stop-loss trigger. The triggers stay dormant until the entry fills in full. If the entry is canceled, rejected, or only partially filled, the triggers are canceled too. A bracket protects the completed entry, not a partial fill.
You cannot attach TP/SL triggers to an order that is already resting on the book. To protect a position from an existing order, wait for the fill and then protect the position.
Place a GTC entry order with take-profit and stop-loss triggers.
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";

const result = await session.placeOrder({
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.GTC,
  takeProfit: {
    triggerPrice: "70000",
  },
  stopLoss: {
    triggerPrice: "62000",
  },
});

const entryOrderId = result.order.id;
const takeProfitOrderId = result.tpSl.takeProfit?.orderId;
const stopLossOrderId = result.tpSl.stopLoss?.orderId;
result.order is the entry order. result.tpSl contains the order IDs for the conditional exits that were accepted with the entry order.
{
  "order": {
    "id": 1234567890,
    "instrumentId": 1,
    "side": "BUY",
    "price": "65000",
    "quantity": "0.01",
    "timeInForce": "gtc",
    "postOnly": false,
    "status": "open",
    "restingQuantity": "0.01",
    "filledQuantity": "0",
    "createdTimestamp": 1767000010000,
    "updatedTimestamp": 1767000010500
  },
  "tpSl": {
    "takeProfit": { "orderId": 1234567891 },
    "stopLoss": { "orderId": 1234567892 }
  }
}
Add limitPrice to a trigger when the exit should place a limit order at trigger time.
const result = await session.placeOrder({
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.GTC,
  stopLoss: {
    triggerPrice: "62000",
    limitPrice: "61900",
  },
});

Protect an Existing Position

Add TP/SL orders to a position that is already open. Position TP/SL closes the full position at trigger time, so it does not use a fixed quantity. Position TP/SL has a few placement rules:
  • The account must already hold a position in the instrument.
  • The account can have at most one position take-profit and one position stop-loss per instrument.
  • When the trigger fires, the exit submits immediately. Position TP/SL does not support limit prices.
  • A position trigger is rejected if the current mark has already crossed it, because it would fire immediately.
Place TP/SL orders for the full current position.
const result = await session.placePositionTpSl({
  instrumentId: instrument.id,
  takeProfit: {
    triggerPrice: "70000",
  },
  stopLoss: {
    triggerPrice: "62000",
  },
});

const takeProfitOrderId = result.tpSl.takeProfit?.orderId;
const stopLossOrderId = result.tpSl.stopLoss?.orderId;
placePositionTpSl sizes the exit at trigger time, so you do not pass a position side or quantity.

Update or Cancel TP/SL

TP/SL orders cannot be modified in place. To change a trigger price or execution type, cancel the old trigger and create a replacement.
  • For position TP/SL, cancel the old trigger before creating another trigger of the same kind for the instrument.
  • For a bracket whose entry is still resting, cancel the entry and place a new bracket with the updated triggers.
  • For a bracket whose entry already filled, cancel the armed trigger and create position TP/SL as the replacement.
Cancel-then-create is not atomic. Between the cancel and the replacement, the position may be unprotected.

Configure Order Behavior

So far, we have used immediate-or-cancel orders. Next, we’ll look at how to control whether an order rests, fills immediately, or only adds liquidity.

Good-Til-Canceled Orders

Use good-til-canceled orders when the order can rest on the book until it fills or you cancel it.
Set timeInForce to GTC.
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";

const order = await session.placeOrder({
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.GTC,
});
For a GTC order, order.status can be one of these values.
StatusMeaning
PerpsOrderStatus.OpenThe order is resting on the book.
PerpsOrderStatus.FilledThe order fully filled before resting.
PerpsOrderStatus.StpCancelledThe order would have matched your own resting order, so it canceled.
If a GTC order partially fills and rests, inspect filledQuantity and restingQuantity on the returned order.

Post-Only Orders

A post-only order is a special type of good-til-canceled order that only adds liquidity. If it would cross the book and take liquidity, it is rejected instead.
Set postOnly: true on a GTC order.
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";

const order = await session.placeOrder({
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.GTC,
  postOnly: true,
});
For a post-only order, order.status can be one of these values.
StatusMeaning
PerpsOrderStatus.OpenThe order was accepted and added to the book.
PerpsOrderStatus.PostOnlyRejectedThe order would have crossed the book and taken liquidity.

Fill-Or-Kill Orders

Use fill-or-kill orders when the full quantity must fill immediately or cancel. These orders do not rest on the book.
Set timeInForce to FOK.
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";

const order = await session.placeOrder({
  instrumentId: instrument.id,
  side: OrderSide.BUY,
  price: "65000",
  quantity: "0.01",
  timeInForce: PerpsTimeInForce.FOK,
});
For a FOK order, order.status can be one of these values.
StatusMeaning
PerpsOrderStatus.FilledThe full order quantity filled immediately.
PerpsOrderStatus.FokUnfilledThe full order quantity could not fill immediately, so it canceled.
PerpsOrderStatus.StpCancelledThe order would have matched your own resting order, so it canceled.

Cancel Resting Orders

Cancel resting orders when they are stale, conflict with updated strategy, or should no longer remain on the book. Canceling a resting order does not close any filled position.
Cancel stale orders by order ID or by client order ID.
const result = await session.cancelOrder({
  orderId: order.id,
});
Use result.status to confirm whether the cancel request was accepted.
StatusMeaning
okThe cancel command was accepted.
errThe cancel command was not accepted.
Canceling a working order removes it from the book.

Close a Position

Closing a position is a new order in the opposite direction of the open position. Use the full open position size to close. Use a smaller quantity to reduce instead. Mark close orders reduce-only so they cannot flip the account into new exposure if position state changes before execution. Reduce-only is a safeguard, not a sizing shortcut. You still choose the quantity: use the full position size to close or a smaller quantity to reduce. If the order would increase exposure, flip the position, or exceed the remaining closeable size, it is rejected.
Current PositionReduce-Only BuyReduce-Only Sell
LongRejectedReduces or closes
ShortReduces or closesRejected
No positionRejectedRejected
The remaining closeable size accounts for other resting reduce-only orders on the same instrument.
Find the current position by market symbol and parse its decimal size.
import Big from "big.js";

const portfolio = await session.fetchPortfolio();
const position = portfolio.positions.find((item) => item.symbol === "BTC-PERP");

if (!position) {
  throw new Error("No open BTC-PERP position");
}

const size = new Big(position.size);
Close a long position by selling the full size, or close a short position by buying the absolute position size.
import {
  OrderSide,
  PerpsOrderStatus,
  PerpsTimeInForce,
} from "@polymarket/client";

if (size.gt(0)) {
  const order = await session.placeOrder({
    instrumentId: instrument.id,
    side: OrderSide.SELL,
    quantity: position.size,
    timeInForce: PerpsTimeInForce.IOC,
    reduceOnly: true,
  });

  if (order.status === PerpsOrderStatus.Filled) {
    // The position is fully closed.
  }
}
These examples use an IOC order with no price and only treat the close as complete when order.status is PerpsOrderStatus.Filled. Include a price when you want to limit execution, or use FOK or GTC when the close should follow a different order behavior.For reduce-only orders, also handle these statuses.
StatusMeaning
PerpsOrderStatus.ReduceOnlyInvalidThe order would not reduce the remaining closeable position.
PerpsOrderStatus.ReduceOnlyExpiredA resting reduce-only order was canceled after the position fully closed.

Update Leverage

Leverage controls how much notional exposure a position can carry relative to its margin. Cross margin uses available account collateral for the position; non-cross margin keeps the position isolated. Validate leverage against instrument.maxLeverage and the current instrument.riskTiers before submitting the change.
1

Read Current Configuration

Read the current account configuration before changing leverage.
const [config] = await session.fetchAccountConfig({
  instrumentId: instrument.id,
});
config includes the current leverage and margin mode for the instrument.
2

Update Leverage

Update leverage and whether the position uses cross margin. The result confirms the effective leverage configuration after the update.
const result = await session.updateLeverage({
  instrumentId: instrument.id,
  leverage: 5,
  crossMargin: false,
});
The result includes the effective leverage configuration after the update.
Example Result
{
  "status": "ok",
  "instrumentId": 1,
  "leverage": 5,
  "crossMargin": false
}

Reconcile Trade State

Keep local trading state in sync by starting from a local snapshot, applying live updates, and reconciling again whenever the session tells you state may have been missed. TP/SL orders appear alongside regular orders in open-order and order-history reads. Track their lifecycle in addition to the position and fills they protect.
Lifecycle StateMeaning
DormantWaiting for a bracket entry order to fill in full.
ArmedWatching the mark price for the trigger.
TriggeredFired and submitted the closing order.
CanceledRemoved by you, by parent cancellation, or by auto-cancel.
ClearedRemoved because the protected position closed or changed sides.
1

Fetch a Startup Snapshot

Fetch a snapshot when the session starts.
async function fetchTradingSnapshot() {
  const [balances, openOrders, orders, fills, portfolio] = await Promise.all([
    session.fetchBalances(),
    session.fetchOpenOrders({ instrumentId: instrument.id }),
    session.fetchOrders({ instrumentId: instrument.id }),
    session.listFills().firstPage(),
    session.fetchPortfolio(),
  ]);

  return { balances, openOrders, orders, fills, portfolio };
}

let snapshot = await fetchTradingSnapshot();
2

Apply Live Events

Then apply trading live events while the session is open.
for await (const event of session) {
  switch (event.type) {
    case "balance": {
      // event.payload: PerpsBalance
      break;
    }

    case "order": {
      // event.payload: PerpsOrder
      break;
    }

    case "fill": {
      // event.payload: PerpsAccountFill
      break;
    }

    case "portfolio": {
      // event.payload.positions contains current exposure.
      break;
    }

    case "tpsl": {
      // event.payload: PerpsTpSlUpdateEvent["payload"]
      break;
    }
  }
}
3

Reconcile on Resync

When event.type is "resync", use account reads as the first step to reconcile local state before applying more local assumptions.
if (event.type === "resync") {
  snapshot = await fetchTradingSnapshot();
}

Trading Fees

Use the fee schedule when you need to display or account for maker and taker trading fees. Fee rates are decimal strings, so "0.0002" means 0.02%.
Fetch the current Perps fee schedule.
const fees = await client.fetchPerpsFees();
// fees: PerpsFeeScheduleEntry[]
Each fee entry contains the market category and its maker and taker fee rates.
type PerpsFeeScheduleEntry = {
  category: PerpsInstrumentCategory;
  makerFeeRate: DecimalString;
  takerFeeRate: DecimalString;
};
makerFeeRate applies when an order adds liquidity, such as a resting or post-only order. takerFeeRate applies when an order removes liquidity, such as an immediately filled IOC order.