From gmx-io
Manages GMX V2 liquidity: deposit/withdraw from GM pools and GLV vaults, shift pools, query data across Arbitrum, Avalanche, Botanix.
npx claudepluginhub gmx-io/gmx-ai --plugin gmx-ioThis skill uses the workspace's default tool permissions.
GMX V2 has two liquidity layers that back perpetual and spot trading:
Generates deep links for Uniswap v2/v3/v4 liquidity positions by gathering parameters, checking pool data, and suggesting price ranges.
Discovers pending LP fees in PancakeSwap V3, Infinity (v4), and Solana positions, shows USD estimates, and generates deep links for collection.
Interact with Jupiter Lend Protocol on Solana using @jup-ag/lend-read for querying liquidity pools, jlTokens, vaults and @jup-ag/lend for deposits, withdrawals, borrows, repays, position management.
Share bugs, ideas, or general feedback.
GMX V2 has two liquidity layers that back perpetual and spot trading:
Supported chains:
| Chain | Chain ID | Native Token | GLV Vaults |
|---|---|---|---|
| Arbitrum | 42161 | ETH | Yes |
| Avalanche | 43114 | AVAX | Yes |
| Botanix | 3637 | BTC | No (contracts deployed, no vaults configured) |
Execution model: All write operations use a two-step async pattern:
ExchangeRouter or GlvRouter multicallThe user pays an execution fee (in native token) upfront to cover keeper gas costs. Excess fee is refunded.
Integration paths:
@gmx-io/sdk) — Read pool data, balances, and market info. Write operations (deposit/withdraw) are not yet in the SDK.ExchangeRouter.multicall() and GlvRouter.multicall().This skill complements gmx-trading which covers perpetual trading and swaps.
Each GM pool is a pair of tokens backing a specific market:
Deposits can be:
Withdrawals burn GM tokens and return the underlying long and short tokens. A withdrawal can specify minLongTokenAmount and minShortTokenAmount for slippage protection.
Pricing: GM token price = pool value / GM token supply. Pool value is the sum of long and short token values at oracle prices plus accrued PnL.
GM pool addresses are dynamic — discover them via sdk.markets.getMarkets().
GLV vaults hold multiple GM tokens and auto-rebalance across markets:
Known GLV vaults (source: sdk/src/configs/markets.ts):
| Chain | Vault | Long Token | Short Token | Address |
|---|---|---|---|---|
| Arbitrum | GLV [WETH-USDC] | WETH | USDC | 0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9 |
| Arbitrum | GLV [WBTC-USDC] | WBTC | USDC | 0xdF03EEd325b82bC1d4Db8b49c30ecc9E05104b96 |
| Avalanche | GLV [WAVAX-USDC] | WAVAX | USDC | 0x901eE57f7118A7be56ac079cbCDa7F22663A3874 |
Deposit modes:
isMarketTokenDeposit: true). Skips the intermediate deposit step.Withdrawals burn GLV tokens. The vault withdraws from the specified constituent market and returns the underlying long/short tokens.
Use sdk.markets.getMarketsInfo() to query all pool data:
const { GmxSdk } = require("@gmx-io/sdk");
const sdk = new GmxSdk({
chainId: 42161,
rpcUrl: "https://arb1.arbitrum.io/rpc",
oracleUrl: "https://arbitrum-api.gmxinfra.io",
subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql",
});
const { marketsInfoData, tokensData } = await sdk.markets.getMarketsInfo();
// Find a specific pool
const ethPool = Object.values(marketsInfoData).find(
(m) => tokensData[m.indexTokenAddress]?.symbol === "WETH" && !m.isSpotOnly
);
// Pool liquidity data
console.log("Long pool:", ethPool.longPoolAmount); // Long token amount in pool
console.log("Short pool:", ethPool.shortPoolAmount); // Short token amount in pool
console.log("Pool value (max):", ethPool.poolValueMax); // Total pool value USD
console.log("Pool value (min):", ethPool.poolValueMin);
// Pool capacity
console.log("Max long:", ethPool.maxLongPoolAmount); // Max long token capacity
console.log("Max short:", ethPool.maxShortPoolAmount); // Max short token capacity
// GM token address
console.log("Market token:", ethPool.marketTokenAddress);
Check GM token balances:
const { tokensData } = await sdk.tokens.getTokensBalances();
// GM tokens appear as regular tokens — filter by market token addresses
List all pools:
const { marketsInfoData, tokensData } = await sdk.markets.getMarketsInfo();
for (const market of Object.values(marketsInfoData)) {
const indexSymbol = tokensData[market.indexTokenAddress]?.symbol ?? "SPOT";
const longSymbol = tokensData[market.longTokenAddress]?.symbol;
const shortSymbol = tokensData[market.shortTokenAddress]?.symbol;
console.log(`${indexSymbol}: ${longSymbol}/${shortSymbol} — Pool: ${market.poolValueMax}`);
}
REST API — Get market info including pool sizes:
GET https://arbitrum-api.gmxinfra.io/markets/info
Returns extended market data including pool sizes, utilization, open interest, and fee factors.
GraphQL (Subsquid) — Query historical deposit/withdrawal events:
| Chain | Endpoint |
|---|---|
| Arbitrum | https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql |
| Avalanche | https://gmx.squids.live/gmx-synthetics-avalanche:prod/api/graphql |
| Botanix | https://gmx.squids.live/gmx-synthetics-botanix:prod/api/graphql |
Before calling ExchangeRouter.multicall() or GlvRouter.multicall():
1. Token approvals:
SyntheticsRouterExchangeRouter and GLV deposits/withdrawals via GlvRouter// Approve USDC to SyntheticsRouter (works for both GM and GLV operations)
await usdcContract.write.approve([syntheticsRouterAddress, amount]);
2. Default parameter values:
callbackContract: zeroAddress, // No callback
callbackGasLimit: 0n, // No callback gas
dataList: [], // Reserved for future use
uiFeeReceiver: zeroAddress, // No UI fee (set to your address if building a frontend)
3. Native token handling:
When depositing the wrapped native token (WETH on Arbitrum, WAVAX on Avalanche, PBTC on Botanix) and shouldUnwrapNativeToken is true, add the deposit amount to the sendWnt call value instead of using sendTokens. The sendWnt call wraps native token automatically.
// If depositing ETH (native) on Arbitrum:
const wntAmount = executionFee + longTokenAmount; // execution fee + deposit amount
// Use sendWnt for the full amount, skip sendTokens for the long token
4. Slippage: Apply slippage client-side before passing to the contract:
// Apply 0.3% slippage to minimum output
const minMarketTokens = expectedMarketTokens * 997n / 1000n;
All operations require an execution fee in native token. The formula is the same for all operations:
estimatedGasLimit = (per-operation formula)
oraclePriceCount = (per-operation formula)
// adjustGasLimitForEstimate (mirrors contract logic)
gasLimit = estimatedGasFeeBaseAmount
+ (estimatedGasFeePerOraclePrice × oraclePriceCount)
+ applyFactor(estimatedGasLimit, estimatedFeeMultiplierFactor)
executionFee = gasLimit × gasPrice
Per-operation formulas:
| Operation | estimatedGasLimit | oraclePriceCount |
|---|---|---|
| GM Deposit | depositToken + swaps × singleSwap | 3 + swapsCount |
| GM Withdrawal | withdrawalMultiToken + swaps × singleSwap | 3 + swapsCount |
| Shift | shift | 4 |
| GLV Deposit (raw tokens) | glvDepositGasLimit + markets × glvPerMarketGasLimit + depositToken + swaps × singleSwap | 2 + marketsCount + swapsCount |
| GLV Deposit (GM tokens) | glvDepositGasLimit + markets × glvPerMarketGasLimit | 2 + marketsCount |
| GLV Withdrawal | glvWithdrawalGasLimit + markets × glvPerMarketGasLimit + withdrawalMultiToken + swaps × singleSwap | 2 + marketsCount + swapsCount |
Important: marketsCount for GLV operations.
GLV vaults contain many constituent GM markets — GLV [WETH-USDC] on Arbitrum has 40+ markets. The contract validates the execution fee against the actual constituent count, and reverts with InsufficientExecutionFee if too low. The SDK does not expose a method to query the GLV constituent market count. Use these values:
| GLV Vault | Chain | Recommended marketsCount |
|---|---|---|
| GLV [WETH-USDC] | Arbitrum | 53 |
| GLV [WBTC-USDC] | Arbitrum | 53 |
| GLV [WAVAX-USDC] | Avalanche | 20 |
Excess execution fee is always refunded, so overestimating marketsCount is safe. GLV execution fees are typically ~0.001 ETH — roughly 10x higher than GM operations (~0.0001 ETH) due to the large number of constituent markets.
Using the SDK to get gas parameters:
const gasLimits = await sdk.utils.getGasLimits();
const gasPrice = await sdk.utils.getGasPrice();
// --- Per-operation gas limit field names from sdk.utils.getGasLimits() ---
// GM Deposit: gasLimits.depositToken
// GM Withdrawal: gasLimits.withdrawalMultiToken
// Shift: gasLimits.shift
// GLV per-market: gasLimits.glvPerMarketGasLimit
// GLV Deposit: gasLimits.glvDepositGasLimit
// GLV Withdrawal: gasLimits.glvWithdrawalGasLimit
// Swap (per swap): gasLimits.singleSwap
// Base fee fields: gasLimits.estimatedGasFeeBaseAmount,
// gasLimits.estimatedGasFeePerOraclePrice,
// gasLimits.estimatedFeeMultiplierFactor
function calculateExecutionFee(estimatedGasLimit: bigint, oraclePriceCount: bigint): bigint {
let gasLimit = gasLimits.estimatedGasFeeBaseAmount;
gasLimit += gasLimits.estimatedGasFeePerOraclePrice * oraclePriceCount;
gasLimit += estimatedGasLimit * gasLimits.estimatedFeeMultiplierFactor / 10n ** 30n;
return gasLimit * gasPrice;
}
// Example: GM deposit (no swaps)
const gmDepositFee = calculateExecutionFee(gasLimits.depositToken, 3n);
// Example: GLV deposit (raw tokens, 53 markets, no swaps)
const marketsCount = 53n;
const glvDepositGas = gasLimits.glvDepositGasLimit
+ marketsCount * gasLimits.glvPerMarketGasLimit
+ gasLimits.depositToken;
const glvDepositFee = calculateExecutionFee(glvDepositGas, 2n + marketsCount);
Use ExchangeRouter.multicall() to batch send tokens + create deposit in one transaction:
import { encodeFunctionData, zeroAddress } from "viem";
const exchangeRouterAddress = "0x1C3fa76e6E1088bCE750f23a5BFcffa1efEF6A41"; // Arbitrum
const depositVaultAddress = "0xF89e77e8Dc11691C9e8757e84aaFbCD8A67d7A55"; // Arbitrum
// Step 1: Approve tokens to SyntheticsRouter (one-time)
// await longToken.write.approve([syntheticsRouterAddress, longTokenAmount]);
// await shortToken.write.approve([syntheticsRouterAddress, shortTokenAmount]);
// Step 2: Build multicall
const wntAmount = executionFee; // Add longTokenAmount if depositing native token
const multicall = [
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendWnt",
args: [depositVaultAddress, wntAmount],
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendTokens",
args: [longTokenAddress, depositVaultAddress, longTokenAmount],
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendTokens",
args: [shortTokenAddress, depositVaultAddress, shortTokenAmount],
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "createDeposit",
args: [{
addresses: {
receiver: account.address,
callbackContract: zeroAddress,
uiFeeReceiver: zeroAddress,
market: marketTokenAddress, // GM pool address
initialLongToken: longTokenAddress,
initialShortToken: shortTokenAddress,
longTokenSwapPath: [],
shortTokenSwapPath: [],
},
minMarketTokens: minGmTokensOut, // Apply slippage
shouldUnwrapNativeToken: false,
executionFee: executionFee,
callbackGasLimit: 0n,
dataList: [],
}],
}),
];
// Step 3: Send transaction
const hash = await walletClient.writeContract({
address: exchangeRouterAddress,
abi: exchangeRouterAbi,
functionName: "multicall",
args: [multicall],
value: wntAmount,
});
Native token deposits: If depositing WETH/WAVAX/PBTC with shouldUnwrapNativeToken: true, add the deposit amount to wntAmount (executionFee + longTokenAmount) and skip the sendTokens call for that token. The sendWnt call wraps native ETH/AVAX/BTC automatically.
const withdrawalVaultAddress = "0x0628D46b5D145f183AdB6Ef1f2c97eD1C4701C55"; // Arbitrum
// Approve GM tokens to SyntheticsRouter first
// await gmToken.write.approve([syntheticsRouterAddress, marketTokenAmount]);
const multicall = [
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendWnt",
args: [withdrawalVaultAddress, executionFee], // Only execution fee, no deposit
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendTokens",
args: [marketTokenAddress, withdrawalVaultAddress, marketTokenAmount], // GM tokens
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "createWithdrawal",
args: [{
addresses: {
receiver: account.address,
callbackContract: zeroAddress,
uiFeeReceiver: zeroAddress,
market: marketTokenAddress,
longTokenSwapPath: [],
shortTokenSwapPath: [],
},
minLongTokenAmount: minLongOut, // Apply slippage
minShortTokenAmount: minShortOut, // Apply slippage
shouldUnwrapNativeToken: true, // Unwrap WETH to ETH on receive
executionFee: executionFee,
callbackGasLimit: 0n,
dataList: [],
}],
}),
];
const hash = await walletClient.writeContract({
address: exchangeRouterAddress,
abi: exchangeRouterAbi,
functionName: "multicall",
args: [multicall],
value: executionFee,
});
Use GlvRouter.multicall() (not ExchangeRouter):
const glvRouterAddress = "0x7EAdEE2ca1b4D06a0d82fDF03D715550c26AA12F"; // Arbitrum
const glvVaultAddress = "0x393053B58f9678C9c28c2cE941fF6cac49C3F8f9"; // Arbitrum
// Approve tokens to SyntheticsRouter (same as GM operations)
const multicall = [
encodeFunctionData({
abi: glvRouterAbi,
functionName: "sendWnt",
args: [glvVaultAddress, wntAmount],
}),
encodeFunctionData({
abi: glvRouterAbi,
functionName: "sendTokens",
args: [longTokenAddress, glvVaultAddress, longTokenAmount],
}),
encodeFunctionData({
abi: glvRouterAbi,
functionName: "createGlvDeposit",
args: [{
addresses: {
glv: glvTokenAddress, // GLV vault address
market: constituentMarketAddress, // Which GM market to deposit through
receiver: account.address,
callbackContract: zeroAddress,
uiFeeReceiver: zeroAddress,
initialLongToken: longTokenAddress,
initialShortToken: shortTokenAddress,
longTokenSwapPath: [],
shortTokenSwapPath: [],
},
minGlvTokens: minGlvTokensOut, // Note: minGlvTokens, not minMarketTokens
executionFee: executionFee,
callbackGasLimit: 0n,
shouldUnwrapNativeToken: false,
isMarketTokenDeposit: false, // true if depositing GM tokens directly
dataList: [],
}],
}),
];
const hash = await walletClient.writeContract({
address: glvRouterAddress,
abi: glvRouterAbi,
functionName: "multicall",
args: [multicall],
value: wntAmount,
});
Depositing GM tokens directly: Set isMarketTokenDeposit: true and send GM tokens to GlvVault instead of long/short tokens. This skips the intermediate GM deposit step and uses less gas.
// Approve GLV tokens to SyntheticsRouter first
const multicall = [
encodeFunctionData({
abi: glvRouterAbi,
functionName: "sendWnt",
args: [glvVaultAddress, executionFee],
}),
encodeFunctionData({
abi: glvRouterAbi,
functionName: "sendTokens",
args: [glvTokenAddress, glvVaultAddress, glvTokenAmount], // GLV tokens
}),
encodeFunctionData({
abi: glvRouterAbi,
functionName: "createGlvWithdrawal",
args: [{
addresses: {
receiver: account.address,
callbackContract: zeroAddress,
uiFeeReceiver: zeroAddress,
market: constituentMarketAddress, // Which GM market to withdraw from
glv: glvTokenAddress,
longTokenSwapPath: [],
shortTokenSwapPath: [],
},
minLongTokenAmount: minLongOut,
minShortTokenAmount: minShortOut,
shouldUnwrapNativeToken: true,
executionFee: executionFee,
callbackGasLimit: 0n,
dataList: [],
}],
}),
];
const hash = await walletClient.writeContract({
address: glvRouterAddress,
abi: glvRouterAbi,
functionName: "multicall",
args: [multicall],
value: executionFee,
});
Shifts move GM tokens from one pool to another atomically — without withdrawing first. Lower fees than manual withdraw + deposit.
const shiftVaultAddress = "0xfe99609C4AA83ff6816b64563Bdffd7fa68753Ab"; // Arbitrum
// Approve from-market GM tokens to SyntheticsRouter first
const multicall = [
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendWnt",
args: [shiftVaultAddress, executionFee],
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "sendTokens",
args: [fromMarketTokenAddress, shiftVaultAddress, fromMarketTokenAmount],
}),
encodeFunctionData({
abi: exchangeRouterAbi,
functionName: "createShift",
args: [{
addresses: {
receiver: account.address,
callbackContract: zeroAddress,
uiFeeReceiver: zeroAddress,
fromMarket: fromMarketTokenAddress,
toMarket: toMarketTokenAddress,
},
minMarketTokens: minToMarketTokens, // Apply slippage
executionFee: executionFee,
callbackGasLimit: 0n,
dataList: [],
}],
}),
];
const hash = await walletClient.writeContract({
address: exchangeRouterAddress,
abi: exchangeRouterAbi,
functionName: "multicall",
args: [multicall],
value: executionFee,
});
Note: CreateShiftParams does not have a shouldUnwrapNativeToken field (unlike deposit/withdrawal).
If a request hasn't been executed by a keeper yet, the creator can cancel it using the request key (bytes32) returned by the create function:
Via ExchangeRouter:
await walletClient.writeContract({
address: exchangeRouterAddress,
abi: exchangeRouterAbi,
functionName: "cancelDeposit", // or cancelWithdrawal, cancelShift
args: [requestKey],
});
Via GlvRouter:
await walletClient.writeContract({
address: glvRouterAddress,
abi: glvRouterAbi,
functionName: "cancelGlvDeposit", // or cancelGlvWithdrawal
args: [requestKey],
});
Cancellation refunds tokens to the receiver address. Only the account that created the request can cancel it.
Deposit/withdrawal fees: Same balancing incentive model as swap fees. Deposits that balance the pool (move it closer to 50/50) pay lower fees. Deposits that imbalance the pool pay higher fees. The fee is deducted from the minted GM/GLV tokens.
Execution fees: Paid in native token upfront. Covers keeper gas costs. Calculated using the formula in the Execution Fee Calculation section. Excess fee is refunded to the receiver.
@gmx-io/sdk does not yet expose convenience methods for deposit/withdraw/shift. Use contract-level multicall as shown above.ExchangeRouter.executeAtomicWithdrawal() exists but requires oracle price params — intended for advanced/keeper use.@gmx-io/sdk on npm — SDK package