> For the complete documentation index, see [llms.txt](https://docs.dimes.fi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dimes.fi/for-developers/api-and-events/websocket.md).

# WebSocket Events

> **SDK support:** The `@dimes-dot-fi/sdk/ws` entry point ships `PositionSocket` and `MarketSocket` clients for the **user gateways** (JWT auth). They wrap Socket.IO, manage reconnection, and camelize every payload to match the rest of the SDK. Prefer them over wiring up Socket.IO by hand — see [Using the SDK](#using-the-sdk) below. For the **partner gateways** (API key auth) the SDK has no client yet; use Socket.IO directly as shown in the raw examples. Runnable: [`05-websocket.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/05-websocket.ts) (Node) and [`react/streams.tsx`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/react/streams.tsx) (React reconciliation).

Two WebSocket gateways are available. Both deliver the same event types and payload shapes — they differ only in authentication and scope.

| Gateway | Namespace                                  | Auth     | Scope                                     |
| ------- | ------------------------------------------ | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/positions`      | API key  | All positions for the partner             |
| User    | `/v1/ws/prediction-markets/user-positions` | User JWT | Only positions for the authenticated user |

***

### Using the SDK

For the **user gateways**, the SDK's `@dimes-dot-fi/sdk/ws` entry point gives you typed clients so you don't touch Socket.IO directly. Both clients take a `getToken` callback (so they always reconnect with a fresh JWT) and an optional `baseUrl` (defaults to `https://api.dimes.fi` — set it to `https://api-sandbox.dimes.fi` for sandbox). Payloads arrive **camelCased** (`event.createdAt`, `event.data`) to match the REST types returned elsewhere by the SDK.

#### Position events

`PositionSocket` connects to the user-positions gateway. Subscribe to a specific event type, to `"*"` for all of them, or to `onNotification` for the user-gateway notification stream. Every `on*` call returns an unsubscribe function.

```typescript
import { PositionSocket } from "@dimes-dot-fi/sdk/ws";

const socket = new PositionSocket({
  getToken: () => currentJwt, // called on every (re)connect
  // baseUrl: "https://api-sandbox.dimes.fi", // for sandbox
});

socket.onConnect(() => console.log("connected"));

const off = socket.on("position.opened", (event) => {
  // event.data is a full Position (camelCased), same shape as the REST API
  addOpenPosition(event.data);
});

socket.on("position.opening", (event) => showFinalizing(event.data.id));

// Transient, informational messages (user gateway only)
socket.onNotification((event) => showToast(event.data.message));

socket.onError((err) => console.error("ws error", err));

socket.connect();

// later: off(); socket.disconnect();
```

When the JWT rotates, call `socket.reconnectWithToken()` to drop and re-establish the connection with the new token.

#### Market events

`MarketSocket` connects to the user-markets gateway with the same API. Remember that market event `data` is **always an array** (see [Market events](#market-events) below):

```typescript
import { MarketSocket } from "@dimes-dot-fi/sdk/ws";

const markets = new MarketSocket({ getToken: () => currentJwt });

markets.on("market.discovered", (event) => {
  for (const market of event.data) upsertMarket(market); // full Market objects
});

markets.on("market.eligibility_changed", (event) => {
  for (const delta of event.data) mergeMarketFields(delta.id, delta); // deltas
});

markets.connect();
```

The raw Socket.IO examples below remain accurate for the partner gateways and non-JS clients; the SDK clients simply wrap that same protocol for the user gateways.

***

### Partner gateway

**Namespace:**

| Environment | WebSocket URL                                                   |
| ----------- | --------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/positions` |

Sandbox emits the same event types and payload shapes as production — only the host and the key prefix differ. See [Environments](/for-developers/getting-started/environments.md).

**Transport:** Socket.IO

**Auth:** Partner API key, passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { apiKey: "dm_live_skey_..." }` (or `dm_sbx_skey_...` in sandbox). Transmitted in the handshake body — never appears in URLs, server logs, or browser history.
* **HTTP header:** `Authorization: Api-Key dm_live_skey_...` (or `dm_sbx_skey_...` in sandbox). For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the partner. All events for positions created through that partner's API keys are delivered.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/positions", {
  auth: {apiKey: process.env.DIMES_API_KEY},
});

socket.on("connect", () => {
  console.log("Connected to position events");
});

socket.on("position.opened", (event) => {
  console.log("Position opened:", event.data.id);
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

***

### User gateway

| Environment | WebSocket URL                                                        |
| ----------- | -------------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/user-positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-positions` |

**Transport:** Socket.IO

**Auth:** User JWT (the same token from `POST /tokens`), passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { token: "<jwt>" }`.
* **HTTP header:** `Authorization: Bearer <jwt>`. For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the authenticated user. Only events for positions belonging to that specific wallet address and partner are delivered — other users' positions are never visible.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-positions", {
  auth: {token: userJwt},
});

socket.on("position.opening", (event) => {
  // Early signal — position is being finalized on-chain
  showFinalizing(event.data.id);
});

socket.on("position.opened", (event) => {
  // Position fully open — display position card
  addOpenPosition(event.data);
});
```

***

### Authentication errors

If authentication fails on either gateway, the server emits an `error` event and disconnects:

```json
{
  "error": {
    "type": "AUTHENTICATION_ERROR",
    "code": "unauthorized",
    "message": "Unauthorized"
  }
}
```

***

### Event envelope

All events follow the `resource.action` naming pattern with a consistent envelope:

```json
{
  "id": "evt_abc123",
  "type": "position.opened",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    ...
  }
}
```

| Field        | Type              | Description                                                                          |
| ------------ | ----------------- | ------------------------------------------------------------------------------------ |
| `id`         | string            | Unique event identifier (prefixed `evt_`)                                            |
| `type`       | string            | Event type in `resource.action` format                                               |
| `created_at` | string (ISO 8601) | When the event was emitted                                                           |
| `data`       | object            | Event payload — always contains the full position object matching the REST API shape |

The `data` object contains the same representation you would get from `GET /positions/{id}`. This means event consumers can reuse the same types/parsers as their REST integration.

***

### Event types

| Event Type                         | Trigger                                                                     |
| ---------------------------------- | --------------------------------------------------------------------------- |
| `position.created`                 | On-chain position account created (user submitted tx)                       |
| `position.opening`                 | Finalize-open tx confirmed on-chain (early signal, before indexer picks up) |
| `position.opened`                  | Position finalized on-chain, tokens acquired (indexer confirmed)            |
| `position.close_requested`         | User submitted close request                                                |
| `position.closed`                  | Close finalized, proceeds distributed                                       |
| `position.settled`                 | Market resolved, position settled                                           |
| `position.liquidated`              | Position liquidated due to insufficient margin                              |
| `position.force_unwound`           | Position partially unwound to reduce leverage (position remains open)       |
| `position.partial_close_requested` | Owner requested a partial close; slice queued (position remains open)       |
| `position.partial_close_initiated` | Partial-close slice withdrawn, sell order in flight (position remains open) |
| `position.partial_closed`          | Partial close finalized; position remains open at reduced size              |
| `position.partial_close_aborted`   | Partial-close slice never filled; position restored to original size        |
| `position.reverted`                | Position open failed, collateral refunded                                   |
| `position.cancelled`               | Position cancelled before opening (EVM only)                                |

***

### Event payloads

The `data` field in every event is a full position object — the same shape returned by the REST API. See the [Positions](https://docs.dimes.fi/for-developers/api-and-events/pages/NdOdqtZSHifVN4PbuN3l#4.-positions-user-jwt) section of the API Reference for complete field definitions.

| Event                              | `status`     | Position shape                                                                        |
| ---------------------------------- | ------------ | ------------------------------------------------------------------------------------- |
| `position.created`                 | `pending`    | Open shape (`entry`, `current`, `risk`, `fees`, `timing`)                             |
| `position.opening`                 | `pending`    | Open shape (early signal — position not yet indexed)                                  |
| `position.opened`                  | `open`       | Open shape                                                                            |
| `position.close_requested`         | `closing`    | Open shape                                                                            |
| `position.closed`                  | `closed`     | Closed shape — `close_reason: "closed"`, `entry`, `result`, `fees`                    |
| `position.settled`                 | `settled`    | Closed shape — `close_reason: "settled"`, `entry`, `result`, `fees`                   |
| `position.liquidated`              | `liquidated` | Closed shape — `close_reason: "liquidated"`, `entry`, `result`, `fees`                |
| `position.force_unwound`           | `open`       | Open shape (reduced size — `current` values reflect new position)                     |
| `position.partial_close_requested` | `open`       | Open shape (slice queued — size unchanged until initiated)                            |
| `position.partial_close_initiated` | `open`       | Open shape (`current` reflects the survivor while the slice sells)                    |
| `position.partial_closed`          | `open`       | Open shape (reduced size — `current` values reflect new position)                     |
| `position.partial_close_aborted`   | `open`       | Open shape (slice never filled — size restored to original)                           |
| `position.reverted`                | `cancelled`  | Closed shape — `close_reason: "reverted"`, `revert_reason`, `entry`, `result`, `fees` |
| `position.cancelled`               | `cancelled`  | Closed shape — `close_reason: "reverted"`, `entry`, `result`, `fees`                  |

Open positions include `current`, `risk`, `fees`, and `timing` sections. Closed, settled, and liquidated positions replace those with `close_reason` and `result` (including `collected_liquidation_fee_*` for liquidations).

The `position.reverted` payload additionally carries `revert_reason`, classifying why the open failed before it landed: `exchange_unavailable` (the prediction-market venue was temporarily unavailable — safe to retry), `slippage_exceeded` (price moved beyond your slippage tolerance before the order filled), or `unknown`. It is non-null only when `close_reason` is `reverted`; collateral is always returned regardless of reason. Surface `exchange_unavailable` as a transient, retryable issue and `slippage_exceeded` as a price-moved message.

The four `position.partial_close_*` events track an owner-requested [partial close](/for-developers/order-types/partial-close.md) — the position stays `open` throughout and ends at a reduced size. Treat `position.partial_closed` like `position.force_unwound`: re-render the still-open position from the smaller `current.*` values.

#### `position.opening` vs `position.opened`

`position.opening` fires as soon as the finalize-open transaction is confirmed on-chain — **before** the event indexer picks it up. This gives the UI an early signal (\~5-15 seconds faster) that the position is about to open.

`position.opened` fires later when the indexer delivers the confirmed `PositionOpened` event.

**Recommended UI behavior:** On `position.opening`, show a "finalizing" state. On `position.opened`, transition to the full open position view with live data. If `position.reverted` arrives instead, the open failed — show the error state.

***

### Listening to events

Subscribe to specific event types using Socket.IO's standard event listeners:

```javascript
socket.on("position.created", (event) => {
  updatePositionStatus(event.data.id, "pending");
});

socket.on("position.opening", (event) => {
  // Early signal — show "finalizing" state
  updatePositionStatus(event.data.id, "finalizing");
});

socket.on("position.opened", (event) => {
  addOpenPosition(event.data);
});

socket.on("position.close_requested", (event) => {
  updatePositionStatus(event.data.id, "closing");
});

socket.on("position.closed", (event) => {
  showSettlement(event.data);
});

socket.on("position.settled", (event) => {
  showSettlement(event.data);
});

socket.on("position.liquidated", (event) => {
  alertLiquidation(event.data);
});

socket.on("position.force_unwound", (event) => {
  // Position still open but reduced in size
  updateOpenPosition(event.data);
});

socket.on("position.partial_closed", (event) => {
  // Owner-requested partial close finalized — position still open, now smaller
  updateOpenPosition(event.data);
});

socket.on("position.reverted", (event) => {
  // Open failed — show error
  showOpenFailed(event.data);
});

socket.on("position.cancelled", (event) => {
  showCancelled(event.data);
});
```

***

### Notification events (user gateway only)

In addition to position state-transition events, the user gateway emits `notification` events. These are informational messages that provide feedback during asynchronous operations — they are not errors and do not indicate a state change.

**Envelope:**

```json
{
  "id": "evt_abc123",
  "type": "notification",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    "code": "ORDER_FULFILLMENT_RETRYING",
    "message": "Order fulfillment not possible at current slippage tolerance. Retrying.",
    "params": {
      "position_id": "dm_pos_abc123",
      "slippage_bps": 150
    }
  }
}
```

| Field          | Type   | Description                                      |
| -------------- | ------ | ------------------------------------------------ |
| `data.code`    | string | Machine-readable notification code               |
| `data.message` | string | Human-readable description                       |
| `data.params`  | object | Additional context (varies by notification code) |

**Notification codes:**

| Code                         | When                                                                            | Params                                                                                     |
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `ORDER_FULFILLMENT_RETRYING` | Order could not be filled at current slippage; retrying                         | `position_id`, `slippage_bps`                                                              |
| `PARTIAL_OPEN_PROGRESS`      | Floor or retry leg of a FAK partial-open fulfilled; reports cumulative progress | `position_id`, `cumulative_filled_making_amount`, `target_making_amount`, `attempt_number` |
| `PARTIAL_OPEN_FLOOR_MISSED`  | Floor FOK of a FAK partial-open failed to fill after retries; position reverted | `position_id`                                                                              |

**Listening:**

```javascript
socket.on("notification", (event) => {
  showToast(event.data.message);
});
```

Notifications are transient — display them as a toast or inline status message. They do not require user action.

***

## Market events

In addition to position events, two WebSocket gateways stream **market** updates: new markets coming online, eligibility changes, and max-leverage changes. They mirror the position gateways exactly — same Socket.IO transport, same auth, same envelope — and differ only in scope.

| Gateway | Namespace                                | Auth     | Scope                                     |
| ------- | ---------------------------------------- | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/markets`      | API key  | All markets visible to the partner        |
| User    | `/v1/ws/prediction-markets/user-markets` | User JWT | Markets visible to the authenticated user |

Auth is identical to the position gateways: the partner gateway takes an API key (`auth: { apiKey: "dm_live_skey_..." }` or `Authorization: Api-Key ...`), and the user gateway takes a user JWT (`auth: { token: "<jwt>" }` or `Authorization: Bearer <jwt>`). See [Partner gateway](#partner-gateway) and [User gateway](#user-gateway) above for the full handshake details — they apply unchanged.

| Environment | Partner URL                                                   | User URL                                                           |
| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------ |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/markets`         | `wss://api.dimes.fi/v1/ws/prediction-markets/user-markets`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/markets` | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-markets` |

### `data` is always an array

> **Important:** Unlike position events — where `data` is a single object — market events always deliver `data` as an **array**. A single market change arrives as an array of one; when many markets change at once (e.g. a batch eligibility sweep), they are batched into a single event. **Always iterate `data` as an array.**

The envelope is otherwise identical to position events:

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    { ... },
    { ... }
  ]
}
```

### Market event types

| Event Type                    | Trigger                                                     | `data` items                                                                 |
| ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `market.discovered`           | A new market comes online                                   | **Full** market objects (same shape as `GET /v1/markets`)                    |
| `market.eligibility_changed`  | A market's tradeability / capacity / notional limits change | **Delta** objects — only the changed fields                                  |
| `market.max_leverage_changed` | An eligible market's max leverage changes                   | **Delta** objects — `id`, `polymarket`, `leverage` (no `min_bps`/`step_bps`) |

`market.discovered` carries the **complete** market payload — identical to an item from `GET /v1/markets`, including the optional `prices` block when available. The two `*_changed` events carry **deltas**: each item contains the market `id`, the always-present `polymarket` identifiers (so you can map the delta onto your cached market), and **only the fields that actually changed**. Merge those fields into your cached copy of the market by `id`.

#### `market.discovered`

`data` is an array of full market objects, each identical to an item from `GET /v1/markets`. See the [Markets](/for-developers/api-and-events/api-reference.md) section of the API Reference for complete field definitions.

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "category": "politics",
      "provider": "polymarket",
      "status": "active",
      "ticker": "will-trump-win-the-2024-election",
      "title": "Will Trump win the 2024 election?",
      "accepting_new_positions": true,
      "min_notional_usd": "5.00",
      "min_notional_usd_pips": "50000",
      "max_notional_yes_usd": "50.00",
      "max_notional_no_usd": "50.00",
      "sided_eligibility": { "yes": { "...": "..." }, "no": { "...": "..." } },
      "leverage": { "...": "..." },
      "fees": { "...": "..." },
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "prices": {
        "yes_bid_price_usd": "0.49",
        "yes_ask_price_usd": "0.51",
        "no_bid_price_usd": "0.49",
        "no_ask_price_usd": "0.51"
      }
    }
  ]
}
```

#### `market.eligibility_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, ...only the changed fields }`. Changed fields may include any of: `accepting_new_positions`, `rejection_reason_code`, `sided_eligibility`, `max_notional_yes_usd(_pips)`, `max_notional_no_usd(_pips)`, `capacity_max_notional_yes_usd(_pips)`, `capacity_max_notional_no_usd(_pips)`, `slippage_max_notional_yes_usd(_pips)`, `slippage_max_notional_no_usd(_pips)`. It **never** includes `leverage`.

```json
{
  "id": "evt_mkt_def456",
  "type": "market.eligibility_changed",
  "created_at": "2025-06-01T10:01:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "accepting_new_positions": false,
      "rejection_reason_code": "QUOTE_MARKET_NOT_ELIGIBLE"
    }
  ]
}
```

#### `market.max_leverage_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, leverage }`, where `leverage` carries the same fields as the `leverage` block in a full market object **except** the constants `min_bps` and `step_bps` — those never change, so read them from the market discovery payload. Only emitted for markets that are currently allowed/eligible.

```json
{
  "id": "evt_mkt_ghi789",
  "type": "market.max_leverage_changed",
  "created_at": "2025-06-01T10:02:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "leverage": {
        "max_bps": 50000,
        "max_yes_bps": 50000,
        "max_no_bps": 30000,
        "max_market_leverage_per_notional": {
          "yes": { "at100_usd_bps": 50000, "at500_usd_bps": 40000, "at1000_usd_bps": 30000, "at10000_usd_bps": 20000 },
          "no": { "at100_usd_bps": 30000, "at500_usd_bps": 25000, "at1000_usd_bps": 20000, "at10000_usd_bps": 15000 }
        }
      }
    }
  ]
}
```

### Connecting to market events

The connection flow is identical to the position gateways — only the namespace changes. Remember to iterate `data`:

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-markets", {
  auth: {token: userJwt},
});

socket.on("connect", () => {
  console.log("Connected to market events");
});

socket.on("market.discovered", (event) => {
  // Full market objects — add/refresh them in your local store
  for (const market of event.data) {
    upsertMarket(market);
  }
});

socket.on("market.eligibility_changed", (event) => {
  // Deltas — merge only the changed fields into the cached market by id
  for (const delta of event.data) {
    mergeMarketFields(delta.id, delta);
  }
});

socket.on("market.max_leverage_changed", (event) => {
  for (const delta of event.data) {
    mergeMarketFields(delta.id, {leverage: delta.leverage});
  }
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

For the partner gateway, swap the namespace to `/v1/ws/prediction-markets/markets` and authenticate with your API key (`auth: {apiKey: process.env.DIMES_API_KEY}`) exactly as shown for the [partner position gateway](#partner-gateway).

***

### Best practices

**Deduplicate on event `id`.** Store processed event IDs and skip duplicates. The same event may be delivered more than once.

**Handle events out of order.** A `position.closed` event could arrive before `position.close_requested` due to network timing. Use the `created_at` timestamp and the position status from the REST API as the source of truth if ordering matters.

**Reconnect on disconnect.** Configure your client's reconnection settings for production reliability.

**Use REST API as fallback.** If your WebSocket connection drops, use `GET /positions` to catch up on any missed state changes.
