> ## 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 Close-Position Payload

> Build a signed canonical payload to close (fully or partially) an open perpetual position on Gains Network or Lighter.

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

Builds the payload for closing an existing position. Close size is either explicit (`amountRaw`) or percentage-based (`closePercentage`).

### Request Body

<ParamField body="dex" type="string" required>`gains` or `lighter`.</ParamField>

<ParamField body="chainId" type="string" required>
  Chain of the position (e.g., `evm:42161`, `lighter:301`).
</ParamField>

<ParamField body="marketId" type="string" required>
  Mobula market identifier (e.g., `lighter-btc-usd`).
</ParamField>

<ParamField body="positionId" type="string">
  **Gains only — required.** The Gains *trade index*, sent as a **string that parses as a non-negative integer** (e.g. `"0"`, `"919"`).

  <Warning>
    The Gains positions endpoints (`GET /2/wallet/positions/perp/open`, `perp-positions-open` WSS channel) expose a **composite** position id of the form:

    ```
    pos-gains-<base>-<quote>-<collateral>-<wallet>-<tradeIndex>
    ```

    e.g. `pos-gains-inj-usd-usdc-0xaa0055ef84ef93138c7c11be1d19dac5dcd08741-0`.

    `payloads/close-position` does **not** accept this composite id — passing it returns `400 close-position payload generation failed: gains - positionId must be a non-negative integer, got "<composite>"`.

    **Extract the trailing trade index segment** (the final `-<integer>`) and send it as a string:

    ```typescript theme={null}
    // composite → trade index
    const tradeIndex = compositeId.split('-').pop(); // e.g. "0"
    // Send as string. A JS number is rejected by the zod layer
    // ("expected string, received number"); a string with leading zeros
    // or decimals fails the non-negative-integer business check.
    ```
  </Warning>

  Not used for Lighter.
</ParamField>

<ParamField body="closePercentage" type="number">
  Portion of the position to close, in percent (`0` \< value ≤ `100`). Use `100` for a full close. Mutually exclusive with `amountRaw`.
</ParamField>

<ParamField body="amountRaw" type="number">
  Raw base-token amount to close. Mutually exclusive with `closePercentage`.
</ParamField>

<ParamField body="params" type="object">
  Additional DEX-specific parameters. For Gains partial closes, the API transparently injects `currentCollateralRaw` from the position cache when available, so you do not need to supply it.
</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    | `close-position payload generation failed` — position not found, invalid close size, or DEX refusal |

### Full flow — close 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 close-position payload
const endpoint = 'api/2/perp/payloads/close-position';
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: 'gains',                    // or 'lighter'
    chainId: 'evm:42161',            // or 'lighter:301'
    marketId: 'gains-btc-usd',
    positionId: '12345',             // Gains only — Gains trade index
    closePercentage: 50,             // close 50%; or use amountRaw
  }),
}).then(r => r.json());

// 2. Branch on transport
let signedTx;
const finalPayloadStr = data.payloadStr; // close-position never mutates the envelope

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,
    gasLimit: txData.gas ? BigInt(txData.gas) : 1_500_000n,                 // Diamond proxy under-reports; floor at 1.5 M
    maxFeePerGas: txData.maxFeePerGas
      ? BigInt(txData.maxFeePerGas)
      : (feeData.maxFeePerGas ?? 0n) * 3n,                                  // headroom across the roundtrip
    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/close-position
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/close-position:
    post:
      tags:
        - V2 - Perps
      summary: Build close-position payload
      description: >-
        Build a signed canonical payload to close (fully or partially) an open
        perpetual position. Close size is either explicit (amountRaw) or
        percentage-based (closePercentage).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                timestamp:
                  type: number
                signature:
                  type: string
                dex:
                  type: string
                  enum:
                    - gains
                    - lighter
                chainId:
                  type: string
                marketId:
                  type: string
                positionId:
                  type: string
                  description: Gains trade index. Required for Gains.
                closePercentage:
                  type: number
                  description: >-
                    Portion to close (0 < value ≤ 100). Mutually exclusive with
                    amountRaw.
                amountRaw:
                  type: number
                  description: >-
                    Raw base-token amount to close. Mutually exclusive with
                    closePercentage.
                params:
                  type: object
                  properties: {}
              required:
                - timestamp
                - signature
                - dex
                - chainId
                - marketId
      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

````