Skip to main content
POST
/
2
/
perp
/
payloads
/
deposit
Build deposit payload
curl --request POST \
  --url https://demo-api.mobula.io/api/2/perp/payloads/deposit \
  --header 'Content-Type: application/json' \
  --data '
{
  "timestamp": 123,
  "signature": "<string>",
  "dex": "lighter",
  "chainId": "<string>",
  "originChainId": "<string>",
  "amountUsdc": "<string>"
}
'
{
  "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-only. Gains has no deposit endpoint — collateral on Gains is sent directly with each create-order. Skip this for Gains.
Bridges USDC from originChainId to the Lighter L2 account via a multi-step Relay route. Deposits are eventually consistent and tracked asynchronously — the execute-v2 response carries a processId you poll via /2/perp/check-process.
First-time deposits register the account on Lighter. If the EOA has never deposited on Lighter, this call must transfer ≥ 5 USDC (a Lighter-side requirement). Once the bridge settles, Lighter assigns an accountIndex to the L1 address — needed by /2/perp/payloads/create-account and every subsequent trade/withdraw call. See the create-account page for how to discover the accountIndex afterwards.

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.

Request Body

dex
string
required
Must be lighter.
chainId
string
required
Destination Lighter chain. lighter:301 or lighter:304.
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.

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
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

Body

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

Destination Lighter chain (lighter:301 or lighter:304).

originChainId
string
required

Source chain holding the user's USDC.

amountUsdc
string
required

USDC amount as a decimal string (e.g., "250"). Must be positive.

Response

200 - application/json

Canonical payload envelope

success
boolean
required
data
object
required