Skip to main content
Builds the payload that moves USDC from the user’s wallet into their perp trading account. For Lighter this bridges USDC from originChainId to the Lighter L2 account via a multi-step route; for Gains this bridges from originChainId to the Gains chain in a single EVM tx. Deposits are eventually consistent and tracked asynchronously — the execute-v2 response carries a processId you poll via /2/perp/check-process.

Lighter deposit payload shape

The response’s payloadStr is a canonical envelope whose payload carries a Relay-style route:
{
  "action": "deposit",
  "dex": "lighter",
  "chainId": "lighter:301",
  "transport": "evm-tx",
  "payload": {
    "route": "...",
    "steps": [
      {
        "id": "...",
        "kind": "transaction",
        "items": [
          { "status": "incomplete", "data": { "to": "0x..", "data": "0x..", "value": "0", "chainId": 42161, "gas": "...", "maxFeePerGas": "...", "maxPriorityFeePerGas": "..." } }
        ]
      }
      // additional steps (approvals, bridge, …)
    ]
  }
}
Client workflow (Lighter):
  1. Parse payloadStr.
  2. For every step with kind === "transaction" and every item whose status !== "complete", sign the tx with the user’s key on item.data.chainId.
  3. Push each signed hex string (in order) into a new array payload.signedTxs.
  4. Re-stringify the envelope → finalPayloadStr.
  5. Sign `api/2/perp/execute-v2-${timestamp}-${finalPayloadStr}` and call /2/perp/execute-v2 with that payloadStr and no top-level signedTx.

Gains deposit payload shape

Single EVM tx under payload.data (same shape as Gains orders). Sign it, then call /2/perp/execute-v2 with the unchanged payloadStr and the signed tx as the top-level signedTx field.

Request Body

dex
string
required
gains or lighter.
chainId
string
required
Destination chain (the DEX’s chain). For Lighter: lighter:301 or lighter:304. For Gains: evm:42161.
originChainId
string
required
Source chain holding the user’s USDC. Same as chainId for a same-chain deposit.
amountUsdc
string
required
USDC amount as a decimal string (e.g., "250" or "100.5"). Must be positive.

Endpoint-specific errors

Statusmessage
400deposit payload generation failed — bridge unavailable, insufficient USDC, or DEX refusal

Example — Lighter bridge from Arbitrum

import { ethers } from 'ethers';

const endpoint = 'api/2/perp/payloads/deposit';
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',
    originChainId: 'evm:42161',
    amountUsdc: '250',
  }),
}).then(r => r.json());

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

// 1. sign each bridge tx in order
const provider = new ethers.JsonRpcProvider(arbitrumRpcUrl);
let nonce = await provider.getTransactionCount(wallet.address);
const feeData = await provider.getFeeData();
const signedTxs = [];

for (const step of parsed.payload.steps) {
  if (step.kind !== 'transaction') continue;
  for (const item of step.items) {
    if (item.status === 'complete') continue;
    const tx = item.data;
    const signed = await wallet.signTransaction({
      to: tx.to,
      data: tx.data,
      value: tx.value ? BigInt(tx.value) : 0n,
      chainId: tx.chainId,
      nonce: nonce++,
      gasLimit: tx.gas ? BigInt(tx.gas) : 1_500_000n,
      maxFeePerGas: tx.maxFeePerGas ? BigInt(tx.maxFeePerGas) : feeData.maxFeePerGas,
      maxPriorityFeePerGas: tx.maxPriorityFeePerGas ? BigInt(tx.maxPriorityFeePerGas) : feeData.maxPriorityFeePerGas,
      type: 2,
    });
    signedTxs.push(signed);
  }
}

// 2. inject + re-stringify
parsed.payload.signedTxs = signedTxs;
const finalPayloadStr = JSON.stringify(parsed);

// 3. sign execute-v2 over the UPDATED string
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,
    transport: data.transport,
    payloadStr: finalPayloadStr,
    timestamp: execTs,
    signature: execSig,
  }),
}).then(r => r.json());

// poll execRes.data.processId via /2/perp/check-process