Skip to main content
Builds the payload that moves free USDC from the user’s perp trading account back to their wallet. Only free margin (not locked by open positions/orders) can be withdrawn.

Lighter withdraw payload shape

The response’s payloadStr is an envelope whose payload includes an L1 authorization challenge:
{
  "action": "withdraw",
  "dex": "lighter",
  "chainId": "lighter:301",
  "transport": "offchain-api",
  "payload": {
    "MessageToSign": "Lighter withdraw ... <server-generated challenge>",
    // other Lighter-native fields
  }
}
Client workflow (Lighter):
  1. Parse payloadStr.
  2. Sign payload.MessageToSign with the user’s wallet (standard EIP-191 personal_sign).
  3. Set the resulting hex on payload.L1Sig and delete payload.MessageToSign.
  4. Re-stringify the envelope → finalPayloadStr.
  5. Sign `api/2/perp/execute-v2-${timestamp}-${finalPayloadStr}` and call /2/perp/execute-v2 with that payloadStr (no top-level signedTx).

Gains withdraw payload shape

Single EVM tx under payload.data with transport: "evm-tx". Sign it and submit as the top-level signedTx to /2/perp/execute-v2; payloadStr is forwarded unchanged.

Request Body

dex
string
required
gains or lighter.
chainId
string
required
Chain of the perp account (e.g., lighter:301, evm:42161).
amountUsdc
string
required
USDC amount as a decimal string (e.g., "100"). Must be positive.

Endpoint-specific errors

Statusmessage
400withdraw payload generation failed — insufficient free margin or DEX refusal

Example — Lighter withdraw 100 USDC

const endpoint = 'api/2/perp/payloads/withdraw';
const timestamp = Date.now();
const signature = await wallet.signMessage(`${endpoint}-${timestamp}`);

const payloadRes = await fetch(`https://api.mobula.io/${endpoint}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    timestamp,
    signature,
    dex: 'lighter',
    chainId: 'lighter:301',
    amountUsdc: '100',
  }),
}).then(r => r.json());

const { data } = payloadRes;
const envelope = JSON.parse(data.payloadStr);

// Lighter L1 sig dance
const l1Sig = await wallet.signMessage(envelope.payload.MessageToSign);
envelope.payload.L1Sig = l1Sig;
delete envelope.payload.MessageToSign;

const finalPayloadStr = JSON.stringify(envelope);

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

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,
    transport: data.transport,
    payloadStr: finalPayloadStr,
    timestamp: execTs,
    signature: execSig,
  }),
});