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):
- Parse
payloadStr.
- For every step with
kind === "transaction" and every item whose status !== "complete", sign the tx with the user’s key on item.data.chainId.
- Push each signed hex string (in order) into a new array
payload.signedTxs.
- Re-stringify the envelope →
finalPayloadStr.
- 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
Destination chain (the DEX’s chain). For Lighter: lighter:301 or lighter:304. For Gains: evm:42161.
Source chain holding the user’s USDC. Same as chainId for a same-chain deposit.
USDC amount as a decimal string (e.g., "250" or "100.5"). Must be positive.
Endpoint-specific errors
| Status | message |
|---|
| 400 | deposit 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