> ## 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-Account Payload

> Build a signed canonical payload to create/provision the user's account on a perpetual DEX (e.g., register a Lighter sub-account or API key).

<Note>**Lighter-only.** Gains has no provisioning step — accounts are implicit. Skip this endpoint for Gains.</Note>

Provisions the signer's Lighter sub-account API key + auth token. Run once per `(L1 address, accountIndex)` after the user's first deposit so subsequent trades, withdrawals, etc. can authenticate against Lighter.

## Account lifecycle on Lighter

A wallet (EOA) is **not** a Lighter account by default. Lighter only registers an account on-chain after the L1 address makes its first USDC deposit (≥ **5 USDC**, a Lighter-side requirement). Once the deposit settles, Lighter assigns an `accountIndex` to that L1 address. The integrator must then call this endpoint with the discovered `accountIndex` to provision an API key + auth token — without it, every other Lighter perp endpoint (`create-order`, `close-position`, `withdraw`, …) will fail.

End-to-end first-time setup:

1. **Deposit** ≥ 5 USDC via [`/2/perp/payloads/deposit`](/rest-api-reference/endpoint/perp-payload-deposit) → submit via `/2/perp/execute-v2` → poll [`/2/perp/check-process`](/rest-api-reference/endpoint/perp-check-process) until success.
2. **Discover the `accountIndex`** by polling Lighter's account-lookup endpoint (the bridge takes a few seconds to settle on L2):
   ```http theme={null}
   GET https://mainnet.zklighter.elliot.ai/api/v1/account?by=l1_address&value=<EOA address>
   Accept: application/json
   ```
   Response shape (success): `{ "code": 200, "accounts": [{ "account_index": <number>, ... }] }`. Poll every \~1s until `accounts[0].account_index` is present.
   <Info>**Coming soon.** Mobula will expose a proxy endpoint so integrators don't need to call Lighter directly. For now, hit Lighter's URL above.</Info>
3. **Provision the API key** by calling this endpoint with the discovered `accountIndex` and submitting via `/2/perp/execute-v2`. The response payload may carry `payload.MessageToSign` — sign it, set `payload.L1Sig`, delete `payload.MessageToSign`, re-stringify, and sign execute-v2 over the new string.
4. The user's wallet can now call all Lighter trade/withdraw endpoints.

### Request Body

<ParamField body="dex" type="string" required>Must be `lighter`.</ParamField>

<ParamField body="chainId" type="string" required>
  Must be `lighter:304` (the only chain that accepts Lighter `create-account` today).
</ParamField>

<ParamField body="accountIndex" type="number" required>
  Lighter sub-account index (non-negative integer). Discovered via the Lighter `/api/v1/account?by=l1_address&value=<EOA>` lookup after the first deposit settles.
</ParamField>

<ParamField body="apiKeyIndex" type="number">
  Lighter API key slot to provision (≥ 0). Pick any unused slot. Defaults server-side if omitted.
</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`                                                                                                    |
| ------ | ------------------------------------------------------------------------------------------------------------ |
| 400    | `accountIndex must be a non-negative integer for lighter create-account` — missing or invalid `accountIndex` |
| 400    | `Invalid chainId "<value>" for lighter create-account` — chainId other than `lighter:304`                    |
| 400    | `create-account payload generation failed` — Lighter rejected provisioning (e.g., slot already in use)       |
| 501    | `payload action "create-account" not implemented yet` — `dex` other than `lighter`                           |

### Full flow — provision a Lighter sub-account end-to-end

Assumes the wallet has already deposited ≥ 5 USDC and you have polled Lighter to discover its `accountIndex`. Snippet shows step 3 of the lifecycle above.

```javascript theme={null}
// 1. Auth-sign + fetch the create-account payload
const endpoint = 'api/2/perp/payloads/create-account';
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,
    dex: 'lighter',
    chainId: 'lighter:304',
    accountIndex,         // discovered from Lighter's /api/v1/account lookup
    apiKeyIndex: 100,     // any unused slot
  }),
}).then(r => r.json());

// 2. Mutate the envelope only if Lighter returns an L1 challenge
const parsed = JSON.parse(data.payloadStr);
let finalPayloadStr = data.payloadStr;

if (parsed.payload.MessageToSign) {
  parsed.payload.L1Sig = await wallet.signMessage(parsed.payload.MessageToSign);
  delete parsed.payload.MessageToSign;
  finalPayloadStr = JSON.stringify(parsed);
}

// 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,
    transport: data.transport,
    payloadStr: finalPayloadStr,
    timestamp: execTs,
    signature: execSig,
  }),
}).then(r => r.json());
```

### Helper — discover `accountIndex` after a deposit

```javascript theme={null}
async function pollLighterAccountIndex(eoaAddress, { intervalMs = 1000, maxRetries = 200 } = {}) {
  for (let i = 0; i < maxRetries; i++) {
    const res = await fetch(
      `https://mainnet.zklighter.elliot.ai/api/v1/account?by=l1_address&value=${eoaAddress}`,
      { headers: { Accept: 'application/json' } },
    ).then(r => r.json()).catch(() => null);

    if (res?.code === 200 && res.accounts?.[0]?.account_index != null) {
      return res.accounts[0].account_index;
    }
    await new Promise(r => setTimeout(r, intervalMs));
  }
  throw new Error(`Lighter accountIndex not found for ${eoaAddress} after ${maxRetries} polls`);
}
```


## OpenAPI

````yaml POST /2/perp/payloads/create-account
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-account:
    post:
      tags:
        - V2 - Perps
      summary: Build create-account payload
      description: >-
        Lighter-only. Provisions the signer's Lighter sub-account API key + auth
        token. Prerequisite: the EOA must have already deposited ≥ 5 USDC via
        /2/perp/payloads/deposit so Lighter has assigned an accountIndex to the
        L1 address. Discover the accountIndex via Lighter's GET
        /api/v1/account?by=l1_address&value=<EOA> (Mobula proxy endpoint coming
        soon), then call this endpoint with that accountIndex. Gains has no
        provisioning step.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                timestamp:
                  type: number
                  description: Unix ms timestamp; must be within 30s of server time.
                signature:
                  type: string
                  description: >-
                    Hex signature of `{endpoint}-{timestamp}`. Recovered signer
                    becomes the request user.
                dex:
                  type: string
                  enum:
                    - lighter
                chainId:
                  type: string
                  enum:
                    - lighter:304
                  description: >-
                    Must be `lighter:304` (only chain that accepts Lighter
                    create-account today).
                accountIndex:
                  type: integer
                  minimum: 0
                  description: >-
                    Lighter sub-account index. Discover via Lighter's
                    /api/v1/account?by=l1_address&value=<EOA> after the first
                    deposit settles.
                apiKeyIndex:
                  type: integer
                  minimum: 0
                  description: >-
                    Lighter API key slot to provision. Pick any unused slot.
                    Defaults server-side if omitted.
              required:
                - timestamp
                - signature
                - dex
                - chainId
                - accountIndex
      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

````