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

# Build Create-Order Payload

> Build a signed canonical payload to open a new perpetual position on Gains Network or Lighter. The response is submitted as-is to /2/perp/execute-v2.

<Warning>
  **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`](/rest-api-reference/endpoint/perp-payload-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`](/rest-api-reference/endpoint/perp-payload-create-account) using that `accountIndex`.

  Read the [Build Create-Account Payload](/rest-api-reference/endpoint/perp-payload-create-account) page for the full setup flow including how to discover the `accountIndex` after a deposit.
</Warning>

<Warning>
  **Gains requires a one-time USDC approval per wallet.** Gains' Diamond proxy executes `USDC.transferFrom(user, gainsVault, collateral)` mid-trade. Without an `approve(spender, …)` granting it allowance, `/2/perp/execute-v2` returns `400` with the on-chain revert:

  ```
  ERC20: transfer amount exceeds allowance
  ```

  Before the first Gains order on a wallet, send `USDC.approve(payload.data.to, MaxUint256)` (the `spender` is the `to` field from the build response). See [Approval prerequisite](/cookbooks/perp-execution-flow#gains-usdc-allowance-on-the-trading-proxy) in the cookbook for a copy-paste helper. Native USDC per chain — Arbitrum `0xaf88d065e77c8cC2239327C5EDb3A432268e5831`, Base `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`.
</Warning>

Builds the canonical order payload used by `/2/perp/execute-v2`. The signer recovered from the `signature` field becomes the order's `user`.

### Request Body

<ParamField body="baseToken" type="string" required>
  Base token address, symbol, or Mobula asset id.
</ParamField>

<ParamField body="quote" type="string" required>
  Quote token of the market. Semantics differ per DEX:

  * **Lighter** — pass the ERC-20 collateral symbol (`USDC`).
  * **Gains** — pass the synthetic quote (`USD`). Sending `USDC` returns `"No market matching base/quote"`.

  When unsure, pass `marketId` (e.g. `gains-btc-usd`, `lighter-btc-usd`) and the field is derived server-side.
</ParamField>

<ParamField body="leverage" type="number" required>
  Leverage multiplier (e.g., `10`).
</ParamField>

<ParamField body="long" type="boolean" required>
  `true` for long, `false` for short.
</ParamField>

<ParamField body="reduceOnly" type="boolean" required>
  `true` if the order must only reduce an existing position.
</ParamField>

<ParamField body="collateralAmount" type="number" required>
  Collateral in `quote` units (e.g., USDC).
</ParamField>

<ParamField body="orderType" type="string">
  One of `market`, `limit`, `stop_limit`. Default `market`.
</ParamField>

<ParamField body="openPrice" type="number">
  Trigger/limit price. Required for `limit` and `stop_limit`.
</ParamField>

<ParamField body="tp" type="number">Take-profit price.</ParamField>

<ParamField body="sl" type="number">Stop-loss price.</ParamField>

<ParamField body="amountRaw" type="number">
  Raw position size in base-token units. If omitted, derived from `collateralAmount * leverage`.
</ParamField>

<ParamField body="maxSlippageP" type="number">Max slippage in percent.</ParamField>

<ParamField body="chainIds" type="string[]">
  Routing **hint**, not a strict filter. The router prefers the requested chains but may fall back to another supported chain if the requested market is not deployed on any of them. Example: passing `chainIds: ['evm:42161']` for a market that only exists on Base will silently return `chainId: 'evm:8453'`. **Always trust the response `chainId`** when broadcasting.

  Pass an explicit `marketId` (or rely on the response) for unambiguous routing.
</ParamField>

<ParamField body="dexes" type="string[]">Restrict routing to `gains` and/or `lighter`.</ParamField>

<ParamField body="marginMode" type="number">`0` = cross, `1` = isolated (DEX-specific).</ParamField>

<ParamField body="referrer" type="string">Referrer wallet address for fee sharing.</ParamField>

<ParamField body="marketId" type="string">Force a specific Mobula market instead of routing.</ParamField>

### Authentication

Every `/2/perp/payloads/<action>` endpoint verifies the caller by requiring two extra fields in the request body alongside the action parameters:

<ParamField body="timestamp" type="number" required>
  Unix timestamp in milliseconds. Must be within 30 seconds of server time. Older timestamps are rejected to prevent replay.
</ParamField>

<ParamField body="signature" type="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`.
</ParamField>

```javascript theme={null}
// 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

| Status | `message`                                                       |
| ------ | --------------------------------------------------------------- |
| 403    | `timestamp expired` — timestamp older than 30s                  |
| 403    | `signature already used` — replay attempt                       |
| 400    | `zod validation failed` — `timestamp`/`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.

<Note>
  **Top-level shape.** Successful (2xx) responses return `{ data: { ... } }`. A `success: true` flag is only present inside the body of `execute-v2`'s response, not on the payload-build endpoints. Parse defensively: read `body.data`, then check for the action-specific fields you need (e.g. `data.payloadStr`).
</Note>

<ResponseField name="data" type="object">
  <Expandable title="data">
    <ResponseField name="action" type="string">
      Canonical action name — one of `withdraw`, `create-account`, `deposit`, `create-order`, `close-position`, `cancel-order`, `update-margin`, `edit-order`.
    </ResponseField>

    <ResponseField name="dex" type="string">`gains` or `lighter`.</ResponseField>
    <ResponseField name="chainId" type="string">Chain where the action lands (e.g., `evm:42161`, `lighter:301`).</ResponseField>
    <ResponseField name="marketId" type="string">Mobula market identifier. Present when the action targets a specific market.</ResponseField>

    <ResponseField name="transport" type="string">
      `offchain-api` — server submits to the DEX off-chain API on the user's behalf (Lighter trades, Lighter `withdraw`, Lighter `create-account`).<br />
      `evm-tx` — server broadcasts a user-signed EVM transaction (Lighter `deposit` bridge route, Gains trade actions). The Gains case requires a top-level `signedTx` on execute-v2; the Lighter `deposit` case injects signed txs **inside** `payloadStr`.
    </ResponseField>

    <ResponseField name="payloadStr" type="string">
      JSON-stringified canonical envelope. For most actions you forward this byte-for-byte into `/2/perp/execute-v2`. Mutations are required for: Lighter `deposit` (inject `payload.signedTxs`), Lighter `withdraw` (sign `payload.MessageToSign` → `payload.L1Sig`, delete `MessageToSign`), Lighter `create-account` (same as `withdraw` only if `payload.MessageToSign` is present). After mutation, re-stringify and sign execute-v2 over the **new** string. Never alter the envelope metadata (`action`, `dex`, `chainId`, `transport`, `marketId`) — execute-v2 cross-checks it.
    </ResponseField>
  </Expandable>
</ResponseField>

### Endpoint-specific errors

| Status | `message`                                                                                      |
| ------ | ---------------------------------------------------------------------------------------------- |
| 500    | `could not build create-order payload` — no DEX could build a payload for the given parameters |

### Full flow — open a position end-to-end

Single example covering both DEXes (Lighter offchain-api, Gains evm-tx). The flow branches on `data.transport`.

```javascript theme={null}
import { ethers } from 'ethers';

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

const { data } = await fetch(`https://api.mobula.io/${endpoint}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    timestamp: ts,
    signature: authSig,
    baseToken: 'BTC',
    quote: 'USDC',                   // Lighter quote. For Gains: 'USD' (synthetic) — or omit and pass `marketId`.
    long: true,
    reduceOnly: false,
    leverage: 10,
    collateralAmount: 100,           // 100 USDC of collateral
    orderType: 'market',
    maxSlippageP: 0.5,
    dexes: ['lighter'],              // or ['gains'], or omit to let routing pick
  }),
}).then(r => r.json());

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

if (data.transport === 'evm-tx') {
  // Gains: sign the single EVM tx and pass as top-level signedTx.
  // Read nonce / feeData from a real chain RPC — NOT an embedded-wallet
  // provider, which can return stale or default-to-0 nonce.
  const txData = JSON.parse(data.payloadStr).payload.data;
  const provider = new ethers.JsonRpcProvider(rpcUrlFor(txData.chainId));
  const [nonce, feeData] = await Promise.all([
    provider.getTransactionCount(wallet.address, 'pending'),
    provider.getFeeData(),
  ]);

  signedTx = await wallet.signTransaction({
    to: txData.to,
    data: txData.callData,                            // calldata field is `callData`, not `data`
    value: txData.value ? BigInt(txData.value) : 0n,
    from: wallet.address,
    chainId: txData.chainId,
    nonce: txData.nonce ?? nonce,
    // Gains Diamond proxy under-reports estimateGas (~28 k vs ~270 k real burn);
    // use a floor of 1.5 M or take max(estimate * 2, 1_500_000n).
    gasLimit: txData.gas ? BigInt(txData.gas) : 1_500_000n,
    // Bump 3x to absorb base-fee drift across the build → execute roundtrip.
    maxFeePerGas: txData.maxFeePerGas
      ? BigInt(txData.maxFeePerGas)
      : (feeData.maxFeePerGas ?? 0n) * 3n,
    maxPriorityFeePerGas: txData.maxPriorityFeePerGas
      ? BigInt(txData.maxPriorityFeePerGas)
      : (feeData.maxPriorityFeePerGas ?? 0n),
    type: 2,
  });
}
// Lighter offchain-api: nothing to sign here

// 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());
```


## OpenAPI

````yaml POST /2/perp/payloads/create-order
openapi: 3.0.0
info:
  version: 1.0.0
  title: Mobula API
  description: >-
    Documentation of the Mobula API


    **Demo API**: The default server (demo-api.mobula.io) is a demo API with
    rate limits.

    For production use, please use api.mobula.io with an API key from
    https://admin.mobula.io
servers:
  - url: https://demo-api.mobula.io/api/
    description: Demo API (rate limited, for testing only)
  - url: https://api.mobula.io/api/
    description: Production API (requires API key)
security: []
tags:
  - name: V2 - Token
    description: Token details, price, security, ATH, and holder data
  - name: V2 - Market Data
    description: Market details, OHLCV history, and lighthouse metrics
  - name: V2 - Trades
    description: Token trades, enriched trades, and trade filters
  - name: V2 - Wallet
    description: Wallet positions, activity, trades, analysis, and labels
  - name: V2 - Assets
    description: Cross-chain asset details and price history
  - name: V2 - Swap
    description: Swap quoting and execution
  - name: V2 - Perps
    description: Perpetual futures quoting, execution, and positions
  - name: V2 - Bridge
    description: Cross-chain bridge quoting and intent status (Alpha Preview)
  - name: V2 - DeFi
    description: Bonding pools and pulse data
  - name: V2 - Search
    description: Universal fast search
  - name: V2 - Usage
    description: Per-key API and WebSocket usage history
  - name: V2 - Blockchains
    description: System metadata and chain listings
  - name: V2 - Prediction Markets
    description: >-
      Polymarket markets/events, wallet positions, and the full execution stack
      (auth, order build/submit/cancel, approvals, pUSD wrap/unwrap, deploy,
      deposit/withdraw, redeem). Alpha — see /api/2/pm/*.
  - name: V1 - Market Data
    description: Market prices, history, sparklines, pairs, and multi-data
  - name: V1 - Wallet
    description: Wallet portfolio, transactions, history, and NFTs
  - name: V1 - Token
    description: First buyers
  - name: V1 - Trades
    description: Market trades by pair
  - name: V1 - Metadata
    description: Token metadata, categories, and news
  - name: V1 - Assets
    description: List all assets
  - name: V1 - Search
    description: Search for assets, tokens, and pairs
  - name: V1 - DeFi
    description: Bonding pool pulse data
  - name: V1 - Blockchains
    description: Blockchain listings, pairs, and stats
  - name: V1 - Webhooks
    description: Webhook management
  - name: V1 - Feed
    description: Custom feed creation
paths:
  /2/perp/payloads/create-order:
    post:
      tags:
        - V2 - Perps
      summary: Build create-order payload
      description: >-
        Build a signed canonical payload to open a new perpetual position on
        Gains Network or Lighter. Forward the response to /2/perp/execute-v2.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                timestamp:
                  type: number
                signature:
                  type: string
                baseToken:
                  type: string
                quote:
                  type: string
                leverage:
                  type: number
                long:
                  type: boolean
                reduceOnly:
                  type: boolean
                collateralAmount:
                  type: number
                orderType:
                  type: string
                  enum:
                    - market
                    - limit
                    - stop_limit
                openPrice:
                  type: number
                tp:
                  type: number
                sl:
                  type: number
                amountRaw:
                  type: number
                maxSlippageP:
                  type: number
                chainIds:
                  type: array
                  items:
                    type: string
                dexes:
                  type: array
                  items:
                    type: string
                    enum:
                      - gains
                      - lighter
                marginMode:
                  type: number
                referrer:
                  type: string
                marketId:
                  type: string
              required:
                - timestamp
                - signature
                - baseToken
                - quote
                - leverage
                - long
                - reduceOnly
                - collateralAmount
      responses:
        '200':
          description: Canonical payload envelope
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      action:
                        type: string
                      dex:
                        type: string
                      chainId:
                        type: string
                      marketId:
                        type: string
                      transport:
                        type: string
                        enum:
                          - offchain-api
                          - evm-tx
                      payloadStr:
                        type: string
                    required:
                      - action
                      - dex
                      - chainId
                      - transport
                      - payloadStr
                required:
                  - success
                  - data

````