Skip to main content
POST
/
2
/
perp
/
payloads
/
update-margin
Build update-margin payload
curl --request POST \
  --url https://demo-api.mobula.io/api/2/perp/payloads/update-margin \
  --header 'Content-Type: application/json' \
  --data '
{
  "timestamp": 123,
  "signature": "<string>",
  "dex": "gains",
  "chainId": "<string>",
  "marketId": "<string>",
  "positionId": "<string>",
  "usdcAmount": 123,
  "increase": true,
  "newLeverage": 123
}
'
{
  "success": true,
  "data": {
    "action": "<string>",
    "dex": "<string>",
    "chainId": "<string>",
    "transport": "offchain-api",
    "payloadStr": "<string>",
    "marketId": "<string>"
  }
}

Documentation Index

Fetch the complete documentation index at: https://docs.mobula.io/llms.txt

Use this file to discover all available pages before exploring further.

Lighter requires the wallet to be a registered account before any trade/withdraw action.If the EOA has never deposited on Lighter, this endpoint will fail. First-time setup is a two-step prerequisite:
  1. Deposit ≥ 5 USDC via /2/perp/payloads/deposit → Lighter creates an accountIndex on-chain once the bridge settles. (5 USDC is a Lighter requirement, not a Mobula limit.)
  2. Provision API key + auth token via /2/perp/payloads/create-account using that accountIndex.
Read the Build Create-Account Payload page for the full setup flow including how to discover the accountIndex after a deposit.
Adjusts margin on an open position. The exact semantics depend on the DEX:
  • Lighter — supply usdcAmount and increase to add or remove USDC collateral on the market’s position.
  • Gains — supply newLeverage to change the trade’s leverage, which effectively adjusts its collateral.

Request Body

dex
string
required
gains or lighter.
chainId
string
required
Chain of the position.
marketId
string
Mobula market identifier.
positionId
string
Gains trade index. Required for Gains.
usdcAmount
number
Lighter only. USDC amount to add or remove (> 0).
increase
boolean
Lighter only. true to add margin, false to remove it.
newLeverage
number
Gains only. New per-trade leverage (> 0).

Authentication

Every /2/perp/payloads/<action> endpoint verifies the caller by requiring two extra fields in the request body alongside the action parameters:
timestamp
number
required
Unix timestamp in milliseconds. Must be within 30 seconds of server time. Older timestamps are rejected to prevent replay.
signature
string
required
Hex signature (EIP-191 personal_sign) of the message `${endpoint}-${timestamp}`, where endpoint is the path of this endpoint without the leading slash (e.g., for this page: api/2/perp/payloads/<this-action>). The recovered signer address becomes the user for the request. Single-use — replay returns 403 signature already used.
// Replace `<action>` with the action of THIS page (e.g. create-account, deposit, …)
const endpoint = 'api/2/perp/payloads/<action>';
const timestamp = Date.now();
const signature = await wallet.signMessage(`${endpoint}-${timestamp}`);

await fetch(`https://api.mobula.io/${endpoint}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    timestamp,
    signature,
    // ...action-specific fields below
  }),
});

Authentication errors

Statusmessage
403timestamp expired — timestamp older than 30s
403signature already used — replay attempt
400zod validation failedtimestamp/signature shape invalid

Response envelope

Every /2/perp/payloads/<action> endpoint returns the same envelope shape. You pass these fields verbatim into POST /2/perp/execute-v2 to execute the action.
data
object

Endpoint-specific errors

Statusmessage
400update-margin payload action failed — missing position, wrong leg supplied for the DEX, or DEX refusal

Full flow — update margin end-to-end

Single example covering both DEXes (Lighter offchain-api with usdcAmount+increase, Gains evm-tx with newLeverage). The flow branches on data.transport.
import { ethers } from 'ethers';

// 1. Auth-sign + fetch the update-margin payload
const endpoint = 'api/2/perp/payloads/update-margin';
const ts = Date.now();
const authSig = await wallet.signMessage(`${endpoint}-${ts}`);

// Lighter: add 50 USDC of margin to BTC-USD
const lighterBody = {
  timestamp: ts, signature: authSig,
  dex: 'lighter', chainId: 'lighter:301',
  marketId: 'lighter-btc-usd',
  usdcAmount: 50, increase: true,
};

// Gains alternative: lower leverage to 5x on Gains trade #12345
// const gainsBody = {
//   timestamp: ts, signature: authSig,
//   dex: 'gains', chainId: 'evm:42161',
//   marketId: 'gains-btc-usd',
//   positionId: '12345', newLeverage: 5,
// };

const { data } = await fetch(`https://api.mobula.io/${endpoint}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(lighterBody),
}).then(r => r.json());

// 2. Branch on transport
let signedTx;
const finalPayloadStr = data.payloadStr;

if (data.transport === 'evm-tx') {
  // Gains
  const txData = JSON.parse(data.payloadStr).payload.data;
  const provider = new ethers.JsonRpcProvider(rpcUrlFor(txData.chainId));
  const feeData = await provider.getFeeData();

  const baseTx = {
    to: txData.to,
    data: txData.callData,
    value: txData.value ? BigInt(txData.value) : 0n,
    from: wallet.address,
    chainId: txData.chainId,
    nonce: txData.nonce ?? await provider.getTransactionCount(wallet.address),
  };
  const gasLimit = txData.gas ? BigInt(txData.gas) : await provider.estimateGas(baseTx);

  signedTx = await wallet.signTransaction({
    ...baseTx, gasLimit,
    maxFeePerGas: txData.maxFeePerGas ? BigInt(txData.maxFeePerGas) : feeData.maxFeePerGas,
    maxPriorityFeePerGas: txData.maxPriorityFeePerGas ? BigInt(txData.maxPriorityFeePerGas) : feeData.maxPriorityFeePerGas,
    type: 2,
  });
}

// 3. Sign + submit execute-v2
const execTs = Date.now();
const execSig = await wallet.signMessage(
  `api/2/perp/execute-v2-${execTs}-${finalPayloadStr}`,
);

const execRes = await fetch('https://api.mobula.io/api/2/perp/execute-v2', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: data.action,
    dex: data.dex,
    chainId: data.chainId,
    marketId: data.marketId,
    transport: data.transport,
    payloadStr: finalPayloadStr,
    timestamp: execTs,
    signature: execSig,
    ...(signedTx && { signedTx }),
  }),
}).then(r => r.json());

Body

application/json
timestamp
number
required
signature
string
required
dex
enum<string>
required
Available options:
gains,
lighter
chainId
string
required
marketId
string
positionId
string

Gains trade index. Required for Gains.

usdcAmount
number

Lighter only. USDC amount to add or remove (> 0).

increase
boolean

Lighter only. true = add margin, false = remove.

newLeverage
number

Gains only. New per-trade leverage (> 0).

Response

200 - application/json

Canonical payload envelope

success
boolean
required
data
object
required