> For the complete documentation index, see [llms.txt](https://docs.forepaas.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.forepaas.org/v2/advanced/websocket-api.md).

# WebSocket API

Public market events plus signed order book, price, and account WebSocket channels

ForeGate exposes two WebSocket families:

| Family                       | Base URL                       | Authentication                                              | Subscription model                                             |
| ---------------------------- | ------------------------------ | ----------------------------------------------------------- | -------------------------------------------------------------- |
| Public market data           | `wss://openapiws.foregate.com` | None                                                        | Connect to `/ws/market`, then send token IDs in a JSON frame   |
| PaaS account and market data | `wss://openapi.foregate.com`   | Gateway HMAC + API key; user channels also require a cookie | Put the subscription identifiers in the handshake query string |

The public market channel is documented first because its connection and event model are different from the signed `/socket/*` channels. See the [AsyncAPI Specification](/v2/resources/asyncapi-specification.md) for its complete message contract.

## Public Market Channel

`wss://openapiws.foregate.com/ws/market` is an unauthenticated public stream for full order-book snapshots, price-level changes, trade executions, tick-size changes, best quotes, and market lifecycle events. Subscribe with CLOB token IDs (also called asset IDs), not ForeGate market IDs.

| Property                    | Value                                                                     |
| --------------------------- | ------------------------------------------------------------------------- |
| **Protocol**                | WebSocket (wss)                                                           |
| **Full address**            | `wss://openapiws.foregate.com/ws/market`                                  |
| **Authentication**          | None                                                                      |
| **Subscription identifier** | CLOB token ID: `assets_ids` in client frames, `asset_id` in server events |
| **Related HTTP operation**  | `GET https://openapiws.foregate.com/prices-history?market=…&interval=1d`  |

The related HTTP route returns historical `{t,p}` price points for the same CLOB token ID used as `assets_ids` in subscriptions and `asset_id` in events. See [Public Market Data](/v2/api-reference/api-reference/public-market-data.md).

### Connect and subscribe

Send the initial JSON subscription immediately after the connection opens:

```json
{
  "assets_ids": [
    "65818619657568813474341868652308942079804919287380422192892211131408793125422",
    "52114319501245915516055106046884209969926127482827954674443846427813813222426"
  ],
  "type": "market",
  "initial_dump": true,
  "level": 2,
  "custom_feature_enabled": true
}
```

| Field                    | Type      | Required | Default | Description                                                 |
| ------------------------ | --------- | :------: | ------- | ----------------------------------------------------------- |
| `assets_ids`             | string\[] |    Yes   | —       | CLOB token IDs to subscribe to.                             |
| `type`                   | string    |    Yes   | —       | Must be `market`.                                           |
| `initial_dump`           | boolean   |    No    | `true`  | Send an initial `book` snapshot.                            |
| `level`                  | integer   |    No    | `2`     | Subscription level: `1`, `2`, or `3`.                       |
| `custom_feature_enabled` | boolean   |    No    | `false` | Enable `best_bid_ask`, `new_market`, and `market_resolved`. |

Add or remove token IDs without reconnecting:

```json
{
  "operation": "subscribe",
  "assets_ids": [
    "71321045679252212594626385532706912750332728571942532289631379312455583992563"
  ]
}
```

`operation` is either `subscribe` or `unsubscribe`. Subscription-update frames require `operation` and `assets_ids`; they may also carry `level` and `custom_feature_enabled`.

### Heartbeat and client example

Send the text frame `PING` every 10 seconds. The server replies with the text frame `PONG`; these are plain text, not JSON.

```javascript
const ws = new WebSocket('wss://openapiws.foregate.com/ws/market');
let heartbeat;

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({
    assets_ids: ['<CLOB token ID>'],
    type: 'market',
    initial_dump: true,
    custom_feature_enabled: true,
  }));

  heartbeat = setInterval(() => ws.send('PING'), 10_000);
});

ws.addEventListener('message', ({ data }) => {
  if (data === 'PONG') return;

  const frame = JSON.parse(data);
  const events = Array.isArray(frame) ? frame : [frame];

  for (const event of events) {
    switch (event.event_type) {
      case 'book':
      case 'price_change':
      case 'last_trade_price':
      case 'tick_size_change':
      case 'best_bid_ask':
      case 'new_market':
      case 'market_resolved':
        console.log(event.event_type, event);
        break;
      default:
        console.warn('Unknown market event', event);
    }
  }
});

ws.addEventListener('close', () => clearInterval(heartbeat));
```

### Event catalog

All timestamps are Unix milliseconds encoded as strings. Prices and sizes are decimal strings; keep them as strings or parse them with a decimal library. The `market` field is the condition ID, while `id` on lifecycle events is the market ID. A JSON frame can contain one event object or an array of event objects, so normalize both forms before dispatching.

| `event_type`       | When emitted                                                   | Main payload                                                              |
| ------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `book`             | On subscription when `initial_dump` is true, and after a trade | Full aggregated `bids` and `asks` for one `asset_id`                      |
| `price_change`     | An order is placed or cancelled                                | One or more new aggregate sizes in `price_changes`                        |
| `last_trade_price` | A trade executes                                               | Executed `price`, `size`, and `side`                                      |
| `tick_size_change` | The allowed price increment changes near a price limit         | `old_tick_size` and `new_tick_size`                                       |
| `best_bid_ask`     | Best quote changes                                             | `best_bid`, `best_ask`, and `spread`; requires custom features            |
| `new_market`       | A market is created                                            | Market identity, assets, outcomes, and metadata; requires custom features |
| `market_resolved`  | A market resolves                                              | Winning asset and outcome; requires custom features                       |

#### `book`

The published fields are `event_type`, `asset_id`, `market`, `bids`, `asks`, `timestamp`, and `hash`. Each bid or ask has a decimal-string `price` and aggregate `size`. Compatibility frames may also include `min_order_size`, `tick_size`, `neg_risk`, and `last_trade_price`; official clients tolerate an absent or null `hash` or `timestamp`.

```json
{
  "event_type": "book",
  "asset_id": "65818619657568813474341868652308942079804919287380422192892211131408793125422",
  "market": "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
  "bids": [{ "price": "0.49", "size": "20" }],
  "asks": [{ "price": "0.52", "size": "25" }],
  "timestamp": "1757908892351",
  "hash": "0xabc123"
}
```

#### `price_change`

Required top-level fields are `event_type`, `market`, `price_changes`, and `timestamp`. Each current-format change contains `asset_id`, `price`, `size`, `side` (`BUY` or `SELL`), and `hash`; `best_bid` and `best_ask` are optional. `size: "0"` means the price level was removed. Compatibility clients allow `hash`, `best_bid`, and `best_ask` to be absent or null.

```json
{
  "event_type": "price_change",
  "market": "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
  "price_changes": [{
    "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
    "price": "0.5",
    "size": "200",
    "side": "BUY",
    "hash": "56621a121a47ed9333273e21c83b660cff37ae50",
    "best_bid": "0.5",
    "best_ask": "1"
  }],
  "timestamp": "1757908892351"
}
```

#### `last_trade_price`

The compatibility-safe required fields are `event_type`, `asset_id`, `market`, `price`, and `timestamp`. Newer frames add `size`, `side` (`BUY` or `SELL`, from the taker's perspective), and optionally `fee_rate_bps` and `transaction_hash`; legacy frames may omit `size`, `side`, and `fee_rate_bps`.

#### `tick_size_change`

Fields are `event_type`, `asset_id`, `market`, `old_tick_size`, `new_tick_size`, and `timestamp`. Tick sizes are decimal strings. Compatibility frames may omit or null `old_tick_size`; always replace the cached value with `new_tick_size`.

#### `best_bid_ask`

This event requires `custom_feature_enabled: true`. Fields are `event_type`, `asset_id`, `market`, `best_bid`, `best_ask`, `spread`, and `timestamp`; quote values are decimal strings. When one side of the book is empty, official clients allow the corresponding best quote and spread to be absent, null, or an empty string.

#### `new_market`

This event requires `custom_feature_enabled: true`. Its required fields are:

| Field        | Type      | Description                               |
| ------------ | --------- | ----------------------------------------- |
| `event_type` | string    | `new_market`                              |
| `id`         | string    | Market ID                                 |
| `question`   | string    | Market question                           |
| `market`     | string    | Condition ID                              |
| `slug`       | string    | Market slug                               |
| `assets_ids` | string\[] | CLOB token IDs                            |
| `outcomes`   | string\[] | Outcome labels aligned with the token IDs |
| `timestamp`  | string    | Unix milliseconds                         |

Optional fields are `description`, `event_message` (parent `id`, `ticker`, `slug`, `title`, and `description`), `tags`, `condition_id`, `active`, `clob_token_ids`, `sports_market_type`, `line`, `game_start_time`, `order_price_min_tick_size`, `group_item_title`, `taker_base_fee`, `fees_enabled`, and evolving `fee_schedule` metadata.

#### `market_resolved`

This event requires `custom_feature_enabled: true`. Published fields are `event_type`, `id`, `market`, `assets_ids`, `winning_asset_id`, `winning_outcome`, and `timestamp`. Optional fields are `event_message` and `tags`. Compatibility frames can use `asset_ids` as an alias for `assets_ids` and may add `question`, `slug`, `description`, and `outcomes`.

{% hint style="info" %}
Use `event_type` as the discriminator, but retain an unknown-event fallback so clients remain forward compatible. See the [AsyncAPI Specification](/v2/resources/asyncapi-specification.md) for required fields, enums, and nested objects for every message.
{% endhint %}

***

## Signed PaaS Channels

The `/socket/*` module is WebSocket-only. A WebSocket upgrade is an HTTP `GET` request with an `Upgrade: websocket` header, and the Aliyun gateway validates the [HMAC signature](/v2/getting-started/authentication.md) on that handshake exactly as it does for REST calls. Once the upgrade succeeds, frames are not signed — the service transparently proxies them to the internal upstream socket for each channel.

All WebSocket identifiers (for example market, outcome, option, order, and user IDs) are strings. Event timestamps use the format documented by each channel.

## Authentication Layers

| Layer          | Applies to         | Requirement                                                                               |
| -------------- | ------------------ | ----------------------------------------------------------------------------------------- |
| Gateway HMAC   | All channels       | `x-ca-key`, `x-ca-signature`, `x-ca-signature-headers`, `date`, `accept` on the handshake |
| API key        | All channels       | `x-api-key` header, participating in the signature                                        |
| Session cookie | User channels only | `Cookie` header, **also participating in the signature**                                  |

{% hint style="warning" %}
All custom headers that participate in the signature must be listed in `x-ca-signature-headers` in **lowercase**, **alphabetically sorted**, comma-separated. Market channels use `x-api-key`; user channels use `cookie,x-api-key` (alphabetical, so `cookie` comes first). Uppercase names produce `400 Invalid Signature` — the gateway normalizes header names to lowercase server-side before rebuilding the string-to-sign.
{% endhint %}

### Signed channel directory

| Channel                                                     | Path                        | Query parameters                    | Cookie |
| ----------------------------------------------------------- | --------------------------- | ----------------------------------- | :----: |
| [CLOB price-level deltas](#1-clob-price-level-delta-stream) | `/socket/clob-price-update` | `marketId`, `outcomeId`, `optionId` |    —   |
| [Market price curve](#2-market-price-stream)                | `/socket/market-price`      | `marketId`, `interval`              |    —   |
| [User account info](#3-user-info-stream)                    | `/socket/user-info`         | `userId`                            |    ✅   |
| [User positions](#4-user-positions-stream)                  | `/socket/user-positions`    | `marketId`, `userId`                |    ✅   |
| [User open orders](#5-user-open-orders-stream)              | `/socket/user-open-orders`  | `marketId`, `userId`                |    ✅   |

The string-to-sign for a handshake is the `GET` form — note the two empty lines (`content-md5`, `content-type`) and the query string appended to the path **sorted alphabetically by key, not URL-encoded**:

```
GET
application/json


<date>
x-api-key:<X-API-KEY>
/socket/clob-price-update?marketId=<v>&optionId=<v>&outcomeId=<v>
```

For user channels the signed cookie line comes before the API-key line (alphabetical):

```
GET
application/json


<date>
cookie:<session cookie>
x-api-key:<X-API-KEY>
/socket/user-info?userId=<userId>
```

A reusable Node.js connection helper is provided in [Code Examples](/v2/advanced/code-examples.md#websocket-connection-helper-nodejs).

### Handshake Errors (all channels)

Handshake failures come back as HTTP responses, never as WebSocket frames:

| Response                                                                                                                | Meaning                                                                                                                                    |
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request`, body `Invalid Signature`, header `x-ca-error-message: Invalid Signature. Server StringToSign:\`…\`\` | Gateway HMAC mismatch — diff the server's string-to-sign against yours.                                                                    |
| `401 Unauthorized`, body `Invalid Key`                                                                                  | APP Key unknown or not authorized.                                                                                                         |
| `{"code":1401,"message":"unauthorized: Missing X-API-KEY header","data":null}`                                          | Passed the gateway but the business layer needs `x-api-key`.                                                                               |
| `{"code":1401,"message":"unauthorized: Missing Cookie header","data":null}`                                             | User channel called without a session cookie (checked before the upgrade).                                                                 |
| WebSocket `Close(1011, "upstream unavailable")`                                                                         | Upgrade succeeded but the internal upstream socket could not be reached (including sessions the upstream rejects, e.g. an expired cookie). |

***

## 1. CLOB Price-Level Delta Stream

Pushes real-time price-level changes (incremental updates) for the CLOB order book. Subscription dimension: `(marketId, outcomeId, optionId)`.

| Property           | Value                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------- |
| **Protocol**       | WebSocket (wss)                                                                         |
| **Path**           | `/socket/clob-price-update`                                                             |
| **Full address**   | `wss://openapi.foregate.com/socket/clob-price-update?marketId=…&outcomeId=…&optionId=…` |
| **Authentication** | Gateway signature + API key                                                             |

*Gateway behavior: proxied to the internal `/socket/clob-order-book` upstream after the upgrade.*

**Query Parameters:**

| Parameter   | Type   | Required | Description |
| ----------- | ------ | -------- | ----------- |
| `marketId`  | string | Yes      | Market ID   |
| `outcomeId` | string | Yes      | Outcome ID  |
| `optionId`  | string | Yes      | Option ID   |

**Connection example (Node.js, `ws`):**

```javascript
import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://openapi.foregate.com/socket/clob-price-update?marketId=9390&outcomeId=27566&optionId=55445',
  {
    headers: {
      accept: 'application/json',
      date: '2026-05-29 10:00:00',
      'x-ca-key': '<APP Key>',
      'x-ca-signature': '<base64 HMAC-SHA256>',
      'x-ca-signature-headers': 'x-api-key',
      'x-api-key': '<X-API-KEY>',
    },
  }
);

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
ws.on('close', (code, reason) => console.log('closed', code, reason.toString()));
ws.on('error', (err) => console.error('error', err));
```

### Building a Local Order Book (required reading)

This channel pushes **pure deltas** — the server does not send a full snapshot after the upgrade. A client that only consumes this stream will never have complete data. Combine it with the REST snapshot:

1. Call [`GET /orderbook/book?marketId=…&optionId=…&outcomeId=…`](/v2/api-reference/api-reference/orderbook.md) to fetch the current full book as your local baseline.
2. Subscribe to `/socket/clob-price-update` and apply each delta to the local book keyed by `(optionId, side, price)` according to its `eventType` (add / remove / update).

**The snapshot–subscribe gap.** If you fetch HTTP first and subscribe second, there is a brief window in which fills/cancellations can be missed. For strong consistency (matching or market-making), use a double-buffer sequence:

1. Subscribe to the WebSocket first; buffer incoming deltas without applying them.
2. Fetch the `GET /orderbook/book` baseline snapshot.
3. Replay buffered deltas that are **newer than the snapshot** (compare the message `timestamp` field) onto the baseline.
4. Apply all subsequent messages directly; the buffer is no longer used.

Display-grade clients that tolerate second-level inconsistency can use the simple fetch-then-subscribe flow.

### Push Messages

Each message describes **one side only** (`asks` or `bids`, never both) and carries the latest state of the affected price levels. You never need to send business frames — protocol-level ping/pong handles keep-alive.

| Field                           | Type    | Description                                                                                                |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `optionId`                      | string  | Option ID, matching your subscription                                                                      |
| `eventType`                     | string  | Price-level event type; typical values `added` / `removed` / `updated`                                     |
| `timestamp`                     | string  | Event time in ISO 8601 UTC at second precision (e.g. `2026-05-29T06:30:53Z`)                               |
| `asks`                          | array   | Ask-side price levels (mutually exclusive with `bids` — a message contains only one of the two)            |
| `bids`                          | array   | Bid-side price levels                                                                                      |
| `asks[].price` / `bids[].price` | decimal | Price level                                                                                                |
| `asks[].size` / `bids[].size`   | decimal | Quantity at that level. `size == 0` usually pairs with `eventType: removed`, meaning the level was cleared |

**Example — ask level removed:**

```json
{
  "asks": [
    { "price": 0.01, "size": 0.00 }
  ],
  "optionId": "55445",
  "eventType": "removed",
  "timestamp": "2026-05-29T06:30:53Z"
}
```

**Example — bid level added:**

```json
{
  "bids": [
    { "price": 0.007, "size": 6.49 }
  ],
  "optionId": "55445",
  "eventType": "added",
  "timestamp": "2026-05-29T06:30:58Z"
}
```

{% hint style="info" %}
When both sides change at once, the upstream splits the update into two messages. Price and size fields are transmitted as big-number strings upstream to avoid precision loss — deserialize with `decimal.js` / `BigDecimal` rather than native floats.
{% endhint %}

***

## 2. Market Price Stream

Pushes the market's price (implied probability) curve at a chosen aggregation granularity.

| Property           | Value                                                                  |
| ------------------ | ---------------------------------------------------------------------- |
| **Protocol**       | WebSocket (wss)                                                        |
| **Path**           | `/socket/market-price`                                                 |
| **Full address**   | `wss://openapi.foregate.com/socket/market-price?marketId=…&interval=…` |
| **Authentication** | Gateway signature + API key                                            |

*Gateway behavior: proxied to the internal `/socket/market-chance` upstream after the upgrade.*

**Query Parameters:**

| Parameter  | Type   | Required | Description                                                                  |
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `marketId` | string | Yes      | Market ID                                                                    |
| `interval` | enum   | Yes      | Aggregation granularity. Allowed values: `ALL`, `1H`, `6H`, `1D`, `1W`, `1M` |

An invalid `interval` is rejected with `400 Bad Request` before the WebSocket upgrade takes place.

**Connection example (Node.js, `ws`):**

```javascript
import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://openapi.foregate.com/socket/market-price?marketId=9390&interval=ALL',
  {
    headers: {
      accept: 'application/json',
      date: '2026-05-29 10:00:00',
      'x-ca-key': '<APP Key>',
      'x-ca-signature': '<base64 HMAC-SHA256>',
      'x-ca-signature-headers': 'x-api-key',
      'x-api-key': '<X-API-KEY>',
    },
  }
);

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
```

### Push Messages

Each message is a **JSON array** (even for a single market — if the subscription matches several markets they arrive together in one message). Fields per element:

| Field                                    | Type    | Description                                                                 |
| ---------------------------------------- | ------- | --------------------------------------------------------------------------- |
| `marketId`                               | string  | Market ID, matching your subscription                                       |
| `outcomeId`                              | string  | Outcome ID. `"-1"` means "whole-market level, not split per outcome".       |
| `outcomeName`                            | string  | Outcome name; usually an empty string when `outcomeId` is `-1`              |
| `optionChanceHistoryVos`                 | array   | Price curves per option in this market                                      |
| `optionChanceHistoryVos[].optionId`      | string  | Option ID                                                                   |
| `optionChanceHistoryVos[].optionName`    | string  | Option name (may be an empty string upstream)                               |
| `optionChanceHistoryVos[].chanceHistory` | array   | Price points over time                                                      |
| `chanceHistory[].chance`                 | decimal | Implied probability of the option at that point in time (percentage, 0–100) |
| `chanceHistory[].optionTvl`              | decimal | Option TVL (Total Value Locked) at that point                               |
| `chanceHistory[].timestamp`              | string  | Point time in ISO 8601 UTC at second precision.                             |

**Example:**

```json
[
  {
    "marketId": "9390",
    "outcomeId": "-1",
    "outcomeName": "",
    "optionChanceHistoryVos": [
      {
        "optionId": "55496",
        "optionName": "",
        "chanceHistory": [
          { "chance": 95.85, "optionTvl": 0, "timestamp": "2026-05-29T06:33:03Z" }
        ]
      }
    ]
  }
]
```

{% hint style="info" %}
The `interval` affects the density of `chanceHistory` (`1H` is denser than `1D`; `ALL` returns full history), but the array length in a single message is not fixed — never assume a specific count. New trades/price updates arrive as one or more points appended at the end; sort by `timestamp` and append. Parse `chance` / `optionTvl` as big decimals to avoid precision loss.
{% endhint %}

***

## 3. User Info Stream

Pushes the current user's account information in real time — total balance, available balance, frozen amount, and a per-coin balance breakdown. Subscription dimension: `userId`; the user's identity is verified upstream via the session cookie.

| Property           | Value                                                        |
| ------------------ | ------------------------------------------------------------ |
| **Protocol**       | WebSocket (wss)                                              |
| **Path**           | `/socket/user-info`                                          |
| **Full address**   | `wss://openapi.foregate.com/socket/user-info?userId=…`       |
| **Authentication** | Gateway signature + API key + session cookie (cookie signed) |

*Gateway behavior: proxied to the internal `/socket/user` upstream after the upgrade, forwarding your Cookie.*

**Handshake requirements** (differences from market channels):

* The `Cookie` header is mandatory and **participates in the signature**.
* `x-ca-signature-headers` is exactly `cookie,x-api-key` (alphabetical, lowercase, comma-separated).
* A missing cookie is rejected with the business-layer 401 before the upgrade.

**Query Parameters:**

| Parameter | Type   | Required | Description                                                                                           |
| --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `userId`  | string | Yes      | User ID. The upstream additionally verifies that this `userId` belongs to the current cookie session. |

**Connection example (Node.js, `ws`):**

```javascript
import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://openapi.foregate.com/socket/user-info?userId=39589',
  {
    headers: {
      accept: 'application/json',
      date: '2026-05-29 10:00:00',
      'x-ca-key': '<APP Key>',
      'x-ca-signature': '<base64 HMAC-SHA256>',
      'x-ca-signature-headers': 'cookie,x-api-key',
      'x-api-key': '<X-API-KEY>',
      cookie: '<session cookie>',
    },
  }
);

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
```

### Push Messages

Sent whenever the account changes:

| Field                              | Type    | Description                                  |
| ---------------------------------- | ------- | -------------------------------------------- |
| `userBalance`                      | decimal | Total balance (default denomination)         |
| `availableAmount`                  | decimal | Available balance                            |
| `frozenAmount`                     | decimal | Frozen amount                                |
| `currentAmount`                    | decimal | Current balance (including open-order holds) |
| `profitOrLostValue`                | decimal | Current P\&L                                 |
| `trialUserBalance`                 | decimal | Trial-funds total balance                    |
| `trialAvailableAmount`             | decimal | Trial-funds available balance                |
| `trialFreezeAmount`                | decimal | Trial-funds frozen amount                    |
| `wmsBalanceList`                   | array   | Per-coin balance breakdown                   |
| `wmsBalanceList[].id`              | int     | Asset record ID                              |
| `wmsBalanceList[].userId`          | string  | User ID (internal)                           |
| `wmsBalanceList[].coinId`          | string  | Coin ID                                      |
| `wmsBalanceList[].userBalance`     | decimal | Total balance for that coin                  |
| `wmsBalanceList[].availableAmount` | decimal | Available balance for that coin              |
| `wmsBalanceList[].freezeAmount`    | decimal | Frozen amount for that coin                  |

**Example:**

```json
{
  "trialFreezeAmount": 0,
  "profitOrLostValue": 0,
  "trialAvailableAmount": 396.878928000000000000,
  "availableAmount": 0,
  "wmsBalanceList": [
    { "availableAmount": "0", "coinId": "1", "freezeAmount": "0", "id": "237", "userBalance": "0", "userId": "Y16GFbATG4Ce6Ft1e8sIitq1LhezLbhK" },
    { "availableAmount": "0", "coinId": "2", "freezeAmount": "0", "id": "238", "userBalance": "0", "userId": "Y16GFbATG4Ce6Ft1e8sIitq1LhezLbhK" },
    { "availableAmount": "396.878928000000000000", "coinId": "99", "freezeAmount": "0", "id": "236", "userBalance": "396.878928000000000000", "userId": "Y16GFbATG4Ce6Ft1e8sIitq1LhezLbhK" }
  ],
  "currentAmount": 0,
  "userBalance": 0,
  "trialUserBalance": 396.878928000000000000,
  "frozenAmount": 0
}
```

{% hint style="warning" %}
Decimal fields arrive as big-number strings (e.g. `0E-18`, `396.878928000000000000`) precisely to avoid IEEE 754 precision loss — deserialize with `BigDecimal` / `decimal.js`, not native numbers.
{% endhint %}

***

## 4. User Positions Stream

Pushes the current user's position changes for a given market in real time. Subscription dimension: `(marketId, userId)`; identity verified upstream via the cookie.

| Property           | Value                                                                  |
| ------------------ | ---------------------------------------------------------------------- |
| **Protocol**       | WebSocket (wss)                                                        |
| **Path**           | `/socket/user-positions`                                               |
| **Full address**   | `wss://openapi.foregate.com/socket/user-positions?marketId=…&userId=…` |
| **Authentication** | Gateway signature + API key + session cookie (cookie signed)           |

*Gateway behavior: proxied to the internal `/socket/user-position` upstream (note: singular `user-position`) after the upgrade, forwarding your Cookie.*

Handshake requirements are identical to [`/socket/user-info`](#3-user-info-stream): gateway HMAC, signed `x-api-key`, signed `cookie`, `x-ca-signature-headers: cookie,x-api-key`. The string-to-sign path sorts the query alphabetically — `marketId` before `userId`:

```
GET
application/json


<date>
cookie:<session cookie>
x-api-key:<X-API-KEY>
/socket/user-positions?marketId=<marketId>&userId=<userId>
```

**Query Parameters:**

| Parameter  | Type   | Required | Description                                                                                           |
| ---------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `marketId` | string | Yes      | Market ID                                                                                             |
| `userId`   | string | Yes      | User ID. The upstream additionally verifies that this `userId` belongs to the current cookie session. |

**Connection example (Node.js, `ws`):**

```javascript
import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://openapi.foregate.com/socket/user-positions?marketId=9390&userId=39589',
  {
    headers: {
      accept: 'application/json',
      date: '2026-05-29 10:00:00',
      'x-ca-key': '<APP Key>',
      'x-ca-signature': '<base64 HMAC-SHA256>',
      'x-ca-signature-headers': 'cookie,x-api-key',
      'x-api-key': '<X-API-KEY>',
      cookie: '<session cookie>',
    },
  }
);

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
```

### Push Messages

The upstream pushes JSON messages on position changes. The exact fields follow the upstream `/socket/user-position` protocol, and pushes may be snapshots or increments. Treat decimal amount fields as strings when parsing (same precision caveat as [`/socket/user-info`](#3-user-info-stream)).

Errors are identical to [`/socket/user-info`](#3-user-info-stream).

***

## 5. User Open Orders Stream

Pushes the current user's active (unfilled) orders for a given market as paginated snapshots. Subscription dimension: `(marketId, userId)`; identity verified upstream via the cookie.

| Property           | Value                                                                    |
| ------------------ | ------------------------------------------------------------------------ |
| **Protocol**       | WebSocket (wss)                                                          |
| **Path**           | `/socket/user-open-orders`                                               |
| **Full address**   | `wss://openapi.foregate.com/socket/user-open-orders?marketId=…&userId=…` |
| **Authentication** | Gateway signature + API key + session cookie (cookie signed)             |

*Gateway behavior: proxied to the internal `/socket/open-orders` upstream after the upgrade, forwarding your Cookie.*

Handshake requirements are identical to [`/socket/user-info`](#3-user-info-stream) (`x-ca-signature-headers: cookie,x-api-key`; query sorted `marketId` before `userId`).

**Query Parameters:**

| Parameter  | Type   | Required | Description                                                                                           |
| ---------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `marketId` | string | Yes      | Market ID                                                                                             |
| `userId`   | string | Yes      | User ID. The upstream additionally verifies that this `userId` belongs to the current cookie session. |

**Connection example (Node.js, `ws`):**

```javascript
import WebSocket from 'ws';

const ws = new WebSocket(
  'wss://openapi.foregate.com/socket/user-open-orders?marketId=9390&userId=39589',
  {
    headers: {
      accept: 'application/json',
      date: '2026-05-29 10:00:00',
      'x-ca-key': '<APP Key>',
      'x-ca-signature': '<base64 HMAC-SHA256>',
      'x-ca-signature-headers': 'cookie,x-api-key',
      'x-api-key': '<X-API-KEY>',
      cookie: '<session cookie>',
    },
  }
);

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
```

### Push Messages

Messages use a paginated-response format:

| Field     | Type    | Description                                                                                    |
| --------- | ------- | ---------------------------------------------------------------------------------------------- |
| `code`    | int     | Upstream status code (`200` = success)                                                         |
| `msg`     | string  | Upstream message (usually `"success"`)                                                         |
| `data`    | array   | Open orders on the current page; sub-fields follow the upstream `/socket/open-orders` protocol |
| `current` | int     | Current page number (1-based)                                                                  |
| `size`    | int     | Page size                                                                                      |
| `total`   | int     | Total item count                                                                               |
| `pages`   | int     | Total page count                                                                               |
| `empty`   | boolean | Whether `data` is empty                                                                        |

**Example (user currently has no open orders):**

```json
{
  "code": 200,
  "current": 1,
  "data": [],
  "empty": true,
  "msg": "success",
  "pages": 0,
  "size": 10,
  "total": 0
}
```

When `data` is non-empty, item fields follow the upstream protocol; parse monetary fields as strings to preserve precision.

Errors are identical to [`/socket/user-info`](#3-user-info-stream).
