Order Examples
This page shows how to create, edit, cancel, and track orders using GmxApiSdk. All examples assume Arbitrum (chainId: 42161).
Setup
import { GmxApiSdk, PrivateKeySigner } from "@gmx-io/sdk/v2";
const sdk = new GmxApiSdk({ chainId: 42161 });
// For signing orders — PrivateKeySigner wraps viem's privateKeyToAccount
const signer = new PrivateKeySigner("0xYOUR_PRIVATE_KEY");
const account = signer.address;
IAbstractSigner is the minimal interface the SDK needs. You can implement it with any wallet:
import type { IAbstractSigner } from "@gmx-io/sdk/v2";
const walletSigner: IAbstractSigner = {
address: "0x...",
async signTypedData(domain, types, value) {
return wallet.signTypedData({ domain, types, message: value });
},
async signMessage(message) {
return wallet.signMessage({ message });
},
// Optional - only needed for classic mode
async sendTransaction(tx) {
return wallet.sendTransaction(tx);
},
};
Discovering markets and prices
Before creating orders, you need to know which markets exist and what prices to use. The symbol field used in order requests (e.g. "ETH/USD [WETH-USDC]") comes from the market data.
List available markets
const markets = await sdk.fetchMarkets();
for (const m of markets) {
console.log(m.symbol); // "ETH/USD [WETH-USDC]"
console.log(m.marketTokenAddress); // "0x70d95587d40A2caf56bd97485aB3Eec10Bee6336"
console.log(m.isSpotOnly); // false (perp market) or true (spot only)
console.log(m.leverageTiers); // [{ maxLeverage, minCollateralFactor, maxPositionSize }]
console.log(m.minPositionSizeUsd); // Minimum position size (30 decimals)
console.log(m.minCollateralUsd); // Minimum collateral (30 decimals)
}
Get current prices and market data
// All markets
const tickers = await sdk.fetchMarketsTickers();
// Filter by symbol
const ethTickers = await sdk.fetchMarketsTickers({
symbols: ["ETH/USD [WETH-USDC]"],
});
// Filter by market address
const filtered = await sdk.fetchMarketsTickers({
addresses: ["0x70d95587d40A2caf56bd97485aB3Eec10Bee6336"],
});
const ticker = ethTickers[0];
console.log(ticker.symbol); // "ETH/USD [WETH-USDC]"
console.log(ticker.markPrice); // Current mark price (30 decimals)
console.log(ticker.maxPrice); // Max price
console.log(ticker.minPrice); // Min price
console.log(ticker.high24h); // 24h high
console.log(ticker.low24h); // 24h low
console.log(ticker.priceChangePercent24hBps); // 24h change in bps
console.log(ticker.longInterestUsd); // Open interest long (30 decimals)
console.log(ticker.shortInterestUsd); // Open interest short (30 decimals)
console.log(ticker.availableLiquidityLong);
console.log(ticker.availableLiquidityShort);
console.log(ticker.fundingRateLong); // Current funding rate
console.log(ticker.borrowingRateLong); // Current borrowing rate
console.log(ticker.capacityLong); // Optional TradingCapacity for longs
console.log(ticker.capacityShort); // Optional TradingCapacity for shorts
// Fetch validated capacity for one market side directly
const longCapacity = await sdk.getTradingCapacity({
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
});
console.log(longCapacity.availableLiquidity); // 30-decimal USD bigint
console.log(longCapacity.limitingFactor); // "reserve", "openInterest", "both", or "notApplicable"
console.log(longCapacity.jitDataStatus); // "available", "stale", or "unavailable"
Cache market configuration and values separately
Use the split endpoints when your application can refresh slower-changing configuration independently from frequently changing market state:
const marketConfigs = await sdk.fetchMarketsConfig();
const marketValues = await sdk.fetchMarketsValues();
const ethConfig = marketConfigs.find(
(market) =>
market.marketTokenAddress.toLowerCase() ===
"0x70d95587d40A2caf56bd97485aB3Eec10Bee6336".toLowerCase()
);
const ethValues = marketValues.find(
(market) =>
market.marketTokenAddress.toLowerCase() ===
ethConfig?.marketTokenAddress.toLowerCase()
);
console.log(ethConfig?.maxCollateralSumLongTokenLong);
console.log(ethConfig?.minFundingIncreaseRatePerSecond);
console.log(ethValues?.virtualInventoryForPositionsInTokens);
console.log(ethValues?.updatedAt); // Unix time in milliseconds, or null
updatedAt represents the least-recent component included in that values row. Apply your own freshness limit and handle null explicitly.
Get token info
// Static token catalog (symbols, decimals, addresses)
const tokens = await sdk.fetchTokens();
// Token metadata with current prices
const tokensData = await sdk.fetchTokensData();
Read positions and orders
// Current positions for an account
const positions = await sdk.fetchPositionsInfo({ address: account });
for (const p of positions) {
console.log(p.indexName); // "ETH/USD"
console.log(p.isLong); // true or false
console.log(p.sizeInUsd); // Position size (30-decimal bigint)
console.log(p.positionValueInUsd); // Position value (30-decimal bigint)
console.log(p.collateralUsd); // Collateral value
console.log(p.marketAddress); // Market address
}
// Positions with their related TP/SL orders
const positionsWithOrders = await sdk.fetchPositionsInfo({
address: account,
includeRelatedOrders: true,
});
// Active orders
const orders = await sdk.fetchOrders({ address: account });
for (const o of orders) {
console.log(o.key); // Order key (used for edit/cancel)
console.log(o.isLong);
console.log(o.sizeDeltaUsd); // Size delta (30-decimal bigint)
console.log(o.triggerPrice); // Trigger price for limit/conditional orders
console.log(o.executionFee);
}
Check wallet balances and allowances
const balances = await sdk.fetchWalletBalances({ address: account });
const allowances = await sdk.fetchAllowances({
address: account,
spender: "router",
});
// Build an ERC-20 approval if needed
const approveTx = sdk.buildApproveTransaction({
tokenAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
spender: "router",
amount: 1_000_000_000n, // 1000 USDC
});
// approveTx.to, approveTx.data — send via your wallet
Express orders — three-step flow
Express orders use the API relay (GMX Relay). The flow is: prepare → sign → submit.
1. Prepare
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n, // $10 (30 decimals)
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" }, // 1 USDC (6 decimals)
mode: "express",
from: account,
});
// prepared.payloadType === "typed-data"
// prepared.estimates contains fee and price impact data
console.log(prepared.estimates);
// {
// sizeDeltaUsd: 10000000000000000000000000000000n,
// executionFeeAmount: ...,
// positionFeeUsd: ...,
// acceptablePrice: ...,
// swapPriceImpactDeltaUsd: ...,
// positionPriceImpactDeltaUsd: ...,
// borrowingFeeUsd: ...,
// fundingFeeUsd: ...,
// }
2. Sign
const signature = await sdk.signOrder(prepared, signer);
3. Submit
const submitted = await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
console.log(submitted.requestId); // Use this to track the order
console.log(submitted.status); // "relay_accepted", "relay_pending", etc.
Referral codes and UI fee receivers
Order prepare requests can include optional attribution fields:
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account,
referralCode: "YourReferralCode",
uiFeeReceiver: "0x1234567890123456789012345678901234567890",
});
referralCode may be either the human-readable registered referral code, such as "YourReferralCode", or an already encoded bytes32 value. Human-readable codes can contain letters, digits, and underscores, and can be up to 20 characters long. The GMX API encodes human-readable codes before building the order payload.
Use uiFeeReceiver for custom UI fee attribution. The receiver must be a valid address and should have its UI fee factor configured on-chain. The prepared Express order keeps the same receiver address, including on every part of a TWAP order; register and claim UI fees from that original receiver. The API stores TWAP grouping metadata separately in each part's dataList.
Express orders — one-call convenience
executeExpressOrder runs prepare + sign + submit in a single call:
const result = await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account,
},
signer
);
console.log(result.requestId);
Tracking order status
Poll fetchOrderStatus with the requestId returned from submission:
const status = await sdk.fetchOrderStatus({
requestId: submitted.requestId,
});
console.log(status.status);
// Possible values:
// "prepared" | "relay_accepted" | "relay_pending" | "relay_submitted"
// | "created" | "executed" | "cancelled" | "relay_failed" | "relay_reverted"
// Terminal statuses: "executed", "cancelled", "relay_failed", "relay_reverted"
Polling loop example:
async function waitForExecution(sdk, requestId, timeoutMs = 60_000) {
const terminal = new Set(["executed", "cancelled", "relay_failed", "relay_reverted"]);
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const status = await sdk.fetchOrderStatus({ requestId });
if (terminal.has(status.status)) return status;
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Order ${requestId} did not reach terminal status within ${timeoutMs}ms`);
}
const finalStatus = await waitForExecution(sdk, result.requestId);
// finalStatus.executionTxnHash — on-chain execution tx
// finalStatus.orderKeys — created order keys
// finalStatus.error — error details if failed
Market increase (open / add to position)
Long
await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 100n * 10n ** 30n, // $100
collateralToken: "USDC",
collateralToPay: { amount: 10_000_000n, token: "USDC" }, // 10 USDC
mode: "express",
from: account,
},
signer
);
Short
await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "short",
orderType: "market",
size: 100n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 10_000_000n, token: "USDC" },
mode: "express",
from: account,
},
signer
);
Limit increase
Set triggerPrice for the entry price. The order stays open until the price is reached.
triggerPrice is passed to the SDK as a 30-decimal USD price. If you inspect the created order on-chain, the stored value is converted to contract price format:
contractPrice = triggerPrice / 10n ** BigInt(indexTokenDecimals);
For example, DOGE has 8 decimals, so a $0.04 trigger price is passed to the SDK as 40000000000000000000000000000n but appears in raw order data as 400000000000000000000n.
const result = await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "limit",
size: 10n * 10n ** 30n,
triggerPrice: 2000n * 10n ** 30n, // $2,000 entry
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account,
},
signer
);
// The order is created on-chain; verify it appears in your orders list
const orders = await sdk.fetchOrders({ address: account });
Market decrease (close / reduce position)
The API calculates the effective decrease size during prepare. It can convert a requested partial decrease into a full close when leaving the residual open would violate the close checks described below.
Partial close
// Get current position size
const positions = await sdk.fetchPositionsInfo({ address: account });
const ethLong = positions.find((p) => p.isLong && p.indexName?.includes("ETH/USD"));
const currentSize = BigInt(ethLong.sizeInUsd);
await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: currentSize / 2n, // Close half
collateralToken: "USDC",
receiveToken: "USDC",
mode: "express",
from: account,
},
signer
);
Full close
await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: currentSize, // Close entire position
collateralToken: "USDC",
receiveToken: "USDC",
mode: "express",
from: account,
},
signer
);
Inspect prepared.estimates.sizeDeltaUsd when you use the three-step flow. It contains the effective size after close validation and can be the full position size even when you requested less. The API snaps to a full close when the remaining position would be less than 1 USD. For a losing position whose remaining collateral would not satisfy leverage requirements, it also snaps when the residual would be below the minimum collateral or minimum position size.
keepLeverage
When decreasing, keepLeverage controls whether collateral is withdrawn proportionally:
// keepLeverage: true — removes collateral proportionally, preserving leverage ratio
await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: currentSize / 5n,
keepLeverage: true,
collateralToken: "USDC",
receiveToken: "USDC",
mode: "express",
from: account,
},
signer
);
// keepLeverage: false — keeps collateral, reducing leverage
receiveToken
Choose which token to receive when closing:
await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: currentSize,
collateralToken: "USDC",
receiveToken: "ETH", // Receive ETH instead of USDC
mode: "express",
from: account,
},
signer
);
Conditional decrease (stop-loss / take-profit)
Stop-loss
const result = await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "stop-loss",
size: 10n * 10n ** 30n,
triggerPrice: 1800n * 10n ** 30n, // Trigger at $1,800
collateralToken: "USDC",
receiveToken: "USDC",
mode: "express",
from: account,
},
signer
);
Take-profit
const result = await sdk.executeExpressOrder(
{
kind: "decrease",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "take-profit",
size: 10n * 10n ** 30n,
triggerPrice: 5000n * 10n ** 30n, // Trigger at $5,000
collateralToken: "USDC",
receiveToken: "USDC",
mode: "express",
from: account,
},
signer
);
TP/SL with position entry
Attach take-profit and stop-loss orders when opening a position using tpsl:
// Get current mark price for TP/SL calculation
const tickers = await sdk.fetchMarketsTickers({ symbols: ["ETH/USD [WETH-USDC]"] });
const markPrice = BigInt(tickers[0].maxPrice);
await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account,
tpsl: [
{ type: "take-profit", triggerPrice: markPrice * 105n / 100n, size: 10n * 10n ** 30n },
{ type: "stop-loss", triggerPrice: markPrice * 95n / 100n, size: 10n * 10n ** 30n },
],
},
signer
);
tpsl supports partial-size TP/SL — omit size to use the full position size. tpsl is only valid on increase orders with market or limit order types. It cannot be used with twap, decrease, or swap orders.
Swap orders
await sdk.executeExpressOrder(
{
kind: "swap",
symbol: "ETH/USD [WETH-USDC]",
orderType: "market",
size: 1_000_000n, // 1 USDC (token decimals)
collateralToPay: { amount: 1_000_000n, token: "USDC" },
receiveToken: "ETH",
mode: "express",
from: account,
},
signer
);
TWAP orders
Split a large order into multiple parts executed over time:
await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "twap",
size: 20n * 10n ** 30n, // $20 total
collateralToken: "USDC",
collateralToPay: { amount: 3_000_000n, token: "USDC" },
twapConfig: { duration: 600, parts: 2 }, // 2 parts over 10 minutes
mode: "express",
from: account,
},
signer
);
twapConfig.parts must be a safe integer between 2 and 30 inclusive. twapConfig.duration must be greater than 0, and twapConfig is only valid when orderType is "twap". TWAP cannot be combined with tpsl.
The API applies full-close adjustment before it splits a decrease into parts, so each part is derived from the effective OrderEstimates.sizeDeltaUsd. It also copies the requested uiFeeReceiver to every part and prepends request and TWAP grouping metadata to each part's dataList.
Edit orders
Change the trigger price (or size) of an existing limit/conditional order:
const orders = await sdk.fetchOrders({ address: account });
const orderId = orders[0].key;
const prepared = await sdk.prepareEditOrder({
orderIds: [orderId],
newTriggerPrice: 2500n * 10n ** 30n, // New trigger: $2,500
mode: "express",
from: account,
});
const signature = await sdk.signOrder(prepared, signer);
const submitted = await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
prepareEditOrder accepts newSize, newTriggerPrice, newAcceptablePrice, newAutoCancel, and executionFeeTopUp.
Cancel orders
Cancel specific orders
const orders = await sdk.fetchOrders({ address: account });
const orderId = orders[0].key;
const prepared = await sdk.prepareCancelOrder({
orderIds: [orderId],
mode: "express",
from: account,
});
const signature = await sdk.signOrder(prepared, signer);
await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
Cancel all orders
const prepared = await sdk.prepareCancelOrder({
all: true,
mode: "express",
from: account,
});
const signature = await sdk.signOrder(prepared, signer);
await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
Classic mode (on-chain transactions)
Classic mode returns raw transaction calldata instead of typed data. You sign and broadcast the transaction yourself — no relay is involved.
The SDK v2 Classic Trading path doesn't use the delegated subaccount routers. The prepared ExchangeRouter transaction calls sendTokens(), which makes the underlying Router pull ERC-20 collateral from the transaction sender. A transaction broadcast by a subaccount therefore uses that subaccount's balance and allowance; an allowance granted by the main account doesn't apply. Use the Express Trading subaccount flow when a delegated signer must create an order that spends the main account's collateral.
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "classic",
from: account,
});
// prepared.payloadType === "transaction"
// prepared.payload.to — contract address
// prepared.payload.data — calldata
// prepared.payload.value — ETH value (execution fee)
// Send with your wallet / viem / ethers:
const txHash = await signer.sendTransaction({
to: prepared.payload.to,
data: prepared.payload.data,
value: BigInt(prepared.payload.value ?? 0),
});
PrivateKeySigner.sendTransaction requires rpcUrl in the constructor:
import { getViemChain } from "@gmx-io/sdk/configs/chains";
const signer = new PrivateKeySigner("0xYOUR_PRIVATE_KEY", {
rpcUrl: "https://arb1.arbitrum.io/rpc",
chain: getViemChain(42161),
});
Slippage and execution fee buffer
Slippage
Controls the acceptable price deviation in basis points (default varies by order type). Higher slippage widens the acceptable price range without affecting size or fee estimates.
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToPay: { amount: 1_000_000n, token: "USDC" },
slippage: 30, // 0.3% (in basis points)
mode: "express",
from: account,
});
Execution fee buffer
Adds a buffer to the execution fee in basis points. Higher buffer reduces the chance of order failure due to gas spikes.
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToPay: { amount: 1_000_000n, token: "USDC" },
executionFeeBufferBps: 5000, // 50% buffer
mode: "express",
from: account,
});
One-click trading (subaccounts)
Subaccounts let you sign orders with a delegated key, avoiding wallet popups for each trade. The SDK can generate and manage this key, or you can supply an independently generated signer. The main account authorizes delegated access, while the subaccount signs each order.
Quick setup
// Generate a local subaccount signer, refresh its on-chain status, and sign
// a SubaccountApproval only if the current on-chain authorization is not enough.
const subaccountAddress = await sdk.activateSubaccount(signer, {
expiresInSeconds: 86400, // 24 hours
maxAllowedCount: 100, // Max 100 actions
});
console.log(sdk.subaccountAddress); // "0x..."
console.log(sdk.subaccountStatus?.remainingActions);
console.log(sdk.hasActiveSubaccount); // true
console.log(sdk.subaccountApprovalMessage?.signature);
// Non-empty signature: the next subaccount order will submit this approval.
// "0x": the subaccount is already active on-chain and no new owner approval is needed.
Submit orders with subaccount
Once activated, executeExpressOrder automatically attaches the current subaccount approval and uses the subaccount signer for the order:
const result = await sdk.executeExpressOrder(
{
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account, // Main account address
},
signer // Main signer identifies the account; SDK signs with subaccount internally
);
If the order submitted a fresh approval, wait until the order is created or executed, then refresh the subaccount state before preparing another subaccount order:
let status = await sdk.fetchOrderStatus({
requestId: result.requestId,
});
while (!["created", "executed", "cancelled", "relay_failed", "relay_reverted"].includes(status.status)) {
await new Promise((r) => setTimeout(r, 2000));
status = await sdk.fetchOrderStatus({ requestId: result.requestId });
}
if (status.status === "created" || status.status === "executed") {
await sdk.refreshSubaccountState(account);
}
console.log(sdk.subaccountApprovalMessage?.signature); // "0x" after the approval is usable on-chain
Manual subaccount flow
For more control, use the explicit prepare, sign, and submit flow. If the SDK has active subaccount state, prepareOrder() attaches the current subaccount address and approval, signOrder() uses the subaccount signer, and submitOrder() forwards the approval that belongs to the prepared request:
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account,
});
const signature = await sdk.signOrder(prepared, signer);
const submitted = await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
Use an externally managed subaccount
An independently generated subaccount doesn't need to come from generateSubaccount() or the GMX Interface. If it is already authorized on-chain for SUBACCOUNT_ORDER_ACTION, create an empty approval with getEmptySubaccountApproval(). Its "0x" signature tells the relay router to use the existing on-chain authorization rather than apply a new owner-signed approval.
Pass both subaccountAddress and subaccountApproval to prepareOrder(). Passing only subaccountAddress doesn't bind the main account to a subaccount approval hash, so the prepared typed data contains the zero address in message.account.
The high-level sdk.signOrder() helper selects a subaccount signer only when the same GmxApiSdk instance generated and stored that signer. For an external signer, use signPreparedOrder(), pass the main account as accountAddress, and include the same subaccountApproval in the submit request:
import { signPreparedOrder } from "@gmx-io/sdk/utils/orderTransactions";
import { getEmptySubaccountApproval } from "@gmx-io/sdk/utils/subaccount";
const chainId = 42161;
const subaccountSigner = new PrivateKeySigner("0xYOUR_SUBACCOUNT_PRIVATE_KEY");
const subaccountAddress = subaccountSigner.address;
const subaccountApproval = getEmptySubaccountApproval(chainId, subaccountAddress);
const prepared = await sdk.prepareOrder({
kind: "increase",
symbol: "ETH/USD [WETH-USDC]",
direction: "long",
orderType: "market",
size: 10n * 10n ** 30n,
collateralToken: "USDC",
collateralToPay: { amount: 1_000_000n, token: "USDC" },
mode: "express",
from: account, // Main account address
subaccountAddress,
subaccountApproval,
});
const signature = await signPreparedOrder(
prepared,
subaccountSigner,
chainId,
account, // Main account address
);
const submitted = await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
subaccountApproval,
},
});
Before using an empty approval, confirm that the subaccount is active, its expiry is in the future, and its remaining action count is greater than zero. Generate and store externally managed keys within your own custody boundary instead of extracting the browser-local key from the GMX Interface. See Delegated trading integration for authorization, key custody, monitoring, and revocation guidance.
Step-by-step setup
// 1. Generate subaccount (deterministic from main signer)
const subAddr = await sdk.generateSubaccount(signer);
// 2. Refresh SDK-managed on-chain status
const status = await sdk.refreshSubaccountState(signer.address);
// 3. Activate with custom limits
await sdk.activateSubaccount(signer, {
expiresInSeconds: 86400,
maxAllowedCount: (status?.currentActionsCount ?? 0n) + 50n,
});
// 4. Refresh after an approval-bearing order is created or executed
await sdk.refreshSubaccountState(signer.address);
// 5. Clear when done or when the connected wallet account changes
sdk.clearSubaccount();
Collateral management
Deposit or withdraw collateral on an existing position using prepareCollateral:
const prepared = await sdk.prepareCollateral({
operation: "deposit", // or "withdraw"
positionKey: "0x...", // Position key from fetchPositionsInfo
amount: 5_000_000n, // 5 USDC (token decimals)
mode: "express",
from: account,
});
const signature = await sdk.signOrder(prepared, signer);
await sdk.submitOrder({
mode: prepared.mode,
requestId: prepared.requestId,
signature,
from: account,
idempotencyKey: prepared.idempotencyKey,
eip712Data: {
batchParams: prepared.payload.batchParams,
relayParams: prepared.payload.relayParams,
},
});
prepareCollateral also accepts optional slippage, executionFeeBufferBps, and gasPaymentToken parameters.
GMX Account operations
GMX Account helpers are available in @gmx-io/sdk/v2. Same-chain operations return or submit wallet transactions on the settlement chain. Cross-chain deposits prepare source-chain transactions. Cross-chain withdrawals prepare typed data and submit through the relayer API.
Setup
import { GmxApiSdk, PrivateKeySigner } from "@gmx-io/sdk/v2";
import { ARBITRUM, SOURCE_BASE_MAINNET, getViemChain } from "@gmx-io/sdk/configs/chains";
import { getMultichainTokenGroups, resolveStargatePool } from "@gmx-io/sdk/configs/multichainTokens";
const sdk = new GmxApiSdk({ chainId: ARBITRUM });
const signer = new PrivateKeySigner(process.env.PRIVATE_KEY as `0x${string}`, {
rpcUrl: process.env.ARBITRUM_RPC_URL,
chain: getViemChain(ARBITRUM),
});
const sourceSigner = new PrivateKeySigner(process.env.PRIVATE_KEY as `0x${string}`, {
rpcUrl: process.env.BASE_RPC_URL,
chain: getViemChain(SOURCE_BASE_MAINNET),
});
const account = signer.address;
const tokenGroups = getMultichainTokenGroups();
const arbitrumUsdc = resolveStargatePool(tokenGroups, { chainId: ARBITRUM, tokenSymbol: "USDC" });
const baseUsdc = resolveStargatePool(tokenGroups, { chainId: SOURCE_BASE_MAINNET, tokenSymbol: "USDC" });
if (!arbitrumUsdc || !baseUsdc) {
throw new Error("USDC is not configured for the selected chains");
}
Same-chain deposit and withdrawal
const depositTxHash = await sdk.executeSameChainDeposit(signer, {
tokenAddress: arbitrumUsdc.address,
amount: 1_000_000n,
account,
});
const bridgeOutParams = sdk.buildSameChainWithdrawBridgeOutParams({
tokenAddress: arbitrumUsdc.address,
amount: 500_000n,
});
const withdrawTxHash = await sdk.executeSameChainWithdraw(signer, {
bridgeOutParams,
});
Cross-chain deposit
await sdk.executeErc20Approve(sourceSigner, {
tokenAddress: baseUsdc.address,
spender: baseUsdc.stargate,
amount: 1_000_000n,
});
const deposit = await sdk.executeCrossChainDeposit(sourceSigner, {
srcChainId: SOURCE_BASE_MAINNET,
account,
tokenSymbol: baseUsdc.symbol,
amount: 1_000_000n,
});
Cross-chain withdrawal
const bridgeOutParams = sdk.buildCrossChainWithdrawBridgeOutParams({
tokenAddress: arbitrumUsdc.address,
amount: 500_000n,
dstChainId: SOURCE_BASE_MAINNET,
stargateAddress: arbitrumUsdc.stargate,
slippageBps: 5,
});
const submitted = await sdk.executeCrossChainWithdraw(signer, {
srcChainId: SOURCE_BASE_MAINNET,
account,
bridgeOutParams,
});
const status = await sdk.getCrossChainWithdrawStatus(submitted.requestId);
Manual cross-chain withdrawal
const prepared = await sdk.prepareCrossChainWithdraw({
srcChainId: SOURCE_BASE_MAINNET,
account,
bridgeOutParams,
});
const signature = await sdk.signCrossChainWithdraw(prepared, signer);
const submitted = await sdk.submitCrossChainWithdraw({
srcChainId: SOURCE_BASE_MAINNET,
account,
signature,
bridgeOutParams,
relayParamsPayload: prepared.payload.relayParams,
relayerFeeTokenAddress: prepared.payload.gasPaymentParams.relayerFeeTokenAddress,
relayerFeeAmount: prepared.payload.gasPaymentParams.relayerFeeAmount,
requestId: prepared.requestId,
});
Type reference
Use these tables as a compact reference for the public transaction types used by the examples above.
PrepareOrderRequest
| Field | Type | Required | Notes |
|---|---|---|---|
kind | "increase" | "decrease" | "swap" | Yes | |
symbol | string | Yes | Market symbol, for example, "ETH/USD [WETH-USDC]" |
direction | "long" | "short" | For increase/decrease | |
orderType | "market" | "limit" | "stop-market" | "take-profit" | "stop-loss" | "twap" | Yes | |
size | bigint | Yes | USD value with 30 decimals for positions; token decimals for swaps |
triggerPrice | bigint | For limit/conditional | SDK/API input uses 30-decimal USD precision. Raw contract/order data stores the converted contract price: price30 / 10 ** indexTokenDecimals. |
slippage | number | No | Basis points |
collateralToken | string | No | Token symbol for position collateral |
collateralToPay | { amount: bigint, token: string } | For increase/swap | Token amount in native decimals |
receiveToken | string | For swap/decrease | Token symbol to receive |
keepLeverage | boolean | No | For decrease — withdraw collateral proportionally |
manualSwapPath | string[] | No | Override auto swap path with market addresses |
acceptablePriceImpactBps | bigint | No | Acceptable price impact in basis points |
executionFeeBufferBps | number | No | Execution fee buffer in basis points |
uiFeeReceiver | string | No | UI fee receiver address; for TWAP orders, the API copies it to every part |
twapConfig | { duration: number, parts: number, frequency?: number } | For TWAP | parts must be a safe integer from 2 through 30, and duration is in seconds. The published type exposes frequency, but the production API derives the schedule from duration and parts. |
tpsl | Array<{ type, triggerPrice, size? }> | No | TP/SL sidecar orders (increase only) |
gasPaymentToken | string | No | Token for gas payment |
referralCode | string | No | Human-readable referral code or encoded bytes32; human-readable codes support letters, digits, and underscores, up to 20 characters |
mode | "express" | "classic" | Yes | |
from | string | Yes | Main account or order owner address; also the transaction sender in Classic Trading |
subaccountAddress | string | No | Express Trading subaccount address; pass it with subaccountApproval |
subaccountApproval | object | No | Express Trading approval payload; required when subaccountAddress is set |
PrepareOrderResponse
| Field | Type | Description |
|---|---|---|
requestId | string | Request identifier used for signing, submission, and status polling |
idempotencyKey | string? | Idempotency key returned by the API, when available |
payloadType | "transaction" | "typed-data" | Whether the prepared payload is a classic transaction or EIP-712 typed data |
mode | "express" | "classic" | Transaction mode requested during preparation |
payload | Record<string, any> | Prepared transaction or typed-data payload |
estimates | OrderEstimates? | Fee, impact, and acceptable-price estimates |
expiresAt | number? | Unix timestamp when the prepared payload expires |
warnings | string[]? | Non-blocking API warnings for the prepared order |
validationWarnings | OrderValidationWarning[]? | Structured capacity warnings such as INSUFFICIENT_LIQUIDITY or TRADING_CAPACITY_UNAVAILABLE |
traceId | string? | API trace identifier for support and debugging |
SubmitOrderResponse
| Field | Type | Description |
|---|---|---|
requestId | string | Request identifier |
status | string | See status values above |
txHash | string? | Relay transaction hash |
taskId | string? | GMX Relay task identifier, when relay submission creates one |
error | { code, message }? | Error details |
traceId | string? | API trace identifier for support and debugging |
OrderEstimates
Returned in prepared.estimates:
| Field | Type | Description |
|---|---|---|
sizeDeltaUsd | bigint | Effective position size delta after full-close adjustment (30 decimals); it can differ from the requested size |
executionFeeAmount | bigint | Execution fee in gas token |
positionFeeUsd | bigint | Position fee (30 decimals) |
acceptablePrice | bigint | Worst acceptable execution price (30 decimals) |
positionPriceImpactDeltaUsd | bigint | Position price impact (30 decimals) |
swapPriceImpactDeltaUsd | bigint | Swap price impact (30 decimals) |
borrowingFeeUsd | bigint | Borrowing fee (30 decimals) |
fundingFeeUsd | bigint | Funding fee (30 decimals) |
tradingCapacity | TradingCapacity? | Capacity used to validate the requested market side, when available |
Prepare-order errors
parsePrepareOrderError(error) parses a caught HttpError with status 400 into a PrepareOrderError. isPrepareOrderError(value) validates an already deserialized unknown value. Both helpers are exported from @gmx-io/sdk/v2.
import { parsePrepareOrderError, type PrepareOrderRequest } from "@gmx-io/sdk/v2";
async function prepare(request: PrepareOrderRequest) {
try {
return await sdk.prepareOrder(request);
} catch (error) {
const prepareError = parsePrepareOrderError(error);
if (prepareError?.code === "INSUFFICIENT_LIQUIDITY") {
console.log(prepareError.details.availableLiquidity);
console.log(prepareError.details.requestedSizeUsd);
}
throw error;
}
}
The prepare runtime can also return INSUFFICIENT_COLLATERAL when the remaining collateral would fall below the on-chain minimum, including for an individual TWAP part. This code isn't yet declared in the exported PrepareOrderError union, so handle it as a raw 400 application error until the public type is aligned.
OrderStatusResponse
| Field | Type | Description |
|---|---|---|
requestId | string | Request identifier |
status | string | See status values above |
txHash | string? | Relay transaction hash |
createdTxnHash | string? | On-chain order creation tx |
executionTxnHash | string? | On-chain execution tx |
orderKeys | string[]? | Created order keys |
cancellationReason | string? | Why the order was cancelled |
taskId | string? | GMX Relay task identifier, when available |
error | { code, message }? | Error details |
createdAt | string? | API creation timestamp |
updatedAt | string? | API update timestamp |
traceId | string? | API trace identifier for support and debugging |