> ## Documentation Index
> Fetch the complete documentation index at: https://docs.4casters.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Price feed

The price feed streams market/orderbook updates as JSON messages over a raw WebSocket connection.

<CodeGroup>
  ```javascript JavaScript theme={null}
  // price_feed_quickstart.js 
  const WebSocket = require('ws'); 
  const token = process.env.FOURCASTERS_TOKEN; 
   
  function connectPriceFeed() { 
    const ws = new 
    WebSocket('wss://streaming-api.4casters.io/price-stream', { 
      headers: { Authorization: token }, 
    }); 
   
    let pingTimer; let lastPong = Date.now(); 
   
    ws.on('open', () => { 
      console.log('price stream connected'); 

      // Optional: narrow the feed to specific games / leagues / sports.
      // Omit this block to receive all market updates (default). 
      ws.send(JSON.stringify({ 
        type: 'subscribe', 
        gameIDs: [], 
        leagueIDs: ['NBA'], 
        sportIDs: [], 
        replace: true, 
      })); 

      pingTimer = setInterval(() => { 
        if (ws.readyState === WebSocket.OPEN) ws.ping(); 
        if (Date.now() - lastPong > 30000) { 
          console.warn('price stream: missed pong >30s, closing'); 
          ws.terminate(); 
        } 
      }, 10000); 
    }); 
   
    ws.on('pong', () => { lastPong = Date.now(); }); 
   
    ws.on('message', (buf) => { 
      try { 
        const msg = JSON.parse(buf.toString()); 
        console.log('price update:', JSON.stringify(msg, null, 2)); 
      } catch { console.log('price update (raw):', buf.toString()); } 
    }); 
   
    ws.on('error', (e) => console.error('price stream error:', e.message)); 
   
    ws.on('close', (code, reason) => { 
      console.log(`price stream closed: ${code} ${reason}`); 
      clearInterval(pingTimer); 
      setTimeout(connectPriceFeed, 1000); 
    }); 
  } 
  connectPriceFeed();
  ```
</CodeGroup>

## Ping/Pong

Both feeds support standard WebSocket `ping/pong`. You may proactively `ping()` on an interval and track `pong` to verify liveness.

## Subscriptions

By default a connected client receives every market update across all sports and leagues. To narrow the firehose, send one of the following commands as a JSON text frame after the socket is `open`. Commands take effect for the lifetime of the connection.

A client that never sends a subscribe command stays in broadcast-all mode, so existing integrations keep working without changes.

### Subscribe by gameID, league, or sport

Install a filter. `gameIDs` are 24-character Mongo ObjectID hex strings; `leagueIDs` are short codes like `NBA` / `MLB` / `EPL`; `sportIDs` are lowercase tokens like `basketball` / `baseball` / `soccer`.

<CodeGroup>
  ```json Replace (recommended) theme={null}
  {
    "type": "subscribe",
    "gameIDs": ["62619dce25e2fb049a71cc2e"],
    "leagueIDs": ["NBA"],
    "sportIDs": [],
    "replace": true
  }
  ```

  ```json Additive theme={null}
  {
    "type": "subscribe",
    "gameIDs": [],
    "leagueIDs": ["MLB"],
    "sportIDs": [],
    "replace": false
  }
  ```
</CodeGroup>

* `replace: true` clears any existing subscriptions on this connection and installs the new lists. Switches the connection into filtered mode.
* `replace: false` adds the new keys to whatever the connection is already subscribed to.

You will receive an update if its `gameID`, `parentGameID`, `league`, or `sport` matches any of your subscriptions.

### Unsubscribe

Remove specific keys. The connection stays in its current mode (filtered or subscribe-all).

<CodeGroup>
  ```json JSON theme={null}
  {
    "type": "unsubscribe",
    "gameIDs": [],
    "leagueIDs": ["MLB"],
    "sportIDs": []
  }
  ```
</CodeGroup>

### Subscribe to everything

Explicit catch-all. Useful if you previously narrowed via `subscribe` and want to re-broaden without reconnecting. Clears any per-key subscriptions on the connection.

<CodeGroup>
  ```json JSON theme={null}
  { "type": "subscribeAll" }
  ```
</CodeGroup>

### Routing semantics

* A `gameID` subscription also receives updates for child games whose `parentGameID` matches (e.g. F5-MLB derivatives reach subscribers of the parent game).
* League IDs are case-insensitive — `mlb` and `MLB` are equivalent.
* Sport IDs are case-insensitive — `baseball` and `Baseball` are equivalent.
* Empty strings inside the arrays are ignored.
* `subscribe` with `replace: false` **and** all-empty arrays is a no-op (a buggy client can't accidentally blackhole itself by sending an empty additive subscribe).
* `subscribe` with `replace: true` **and** all-empty arrays is the legitimate "clear all my subs" path — the connection enters filtered mode with zero subscriptions and receives nothing until you subscribe again or send `subscribeAll`.

## Messages

Every message on `/price-stream` arrives as a 2-tuple `[type, payload]`. Three message types are broadcast.

### `orderUpdate`

Emitted whenever an order in any market changes (new, edited, filled, or cancelled). The payload only includes `sideOrders` the receiving user is allowed to see (own orders plus matchable counterparties).

`sideOrders` entries for orders placed with `orderType: "postArb"` include `isPostArb: true`; the field is omitted otherwise.

<CodeGroup>
  ```json JSON theme={null}
  [
    "orderUpdate",
    {
      "epoch": 1234567,
      "gameID": "62619dce25e2fb049a71cc2e",
      "parentGameID": null,
      "sport": "basketball",
      "league": "NBA",
      "live": false,
      "type": "total",
      "participantID": null,
      "market": "main",
      "side": "under",
      "OU": "under",
      "total": 8.5,
      "spread": null,
      "mainHomeSpread": -6.5,
      "mainAwaySpread": 6.5,
      "mainTotal": 209.5,
      "sideOrders": [
        {
          "id": "62619e6a40e36d0494600f48",
          "type": "total",
          "sumUntaken": 255,
          "odds": 104,
          "bet": 265.2,
          "gameID": "62619dce25e2fb049a71cc2e",
          "takenRatio": 0,
          "participantID": null,
          "market": "main",
          "side": "under",
          "OU": "under",
          "total": 8.5,
          "spread": null,
          "gameStartExpiry": false,
          "expiry": "2022-04-21T23:40:12.000Z",
          "createdAt": "2022-04-21T18:11:54.614Z"
        }
      ]
    }
  ]
  ```
</CodeGroup>

#### `orderUpdate` for moneyline1x2 markets

For `moneyline1x2` (soccer 3-way: home / draw / away), the runner is identified by the **`market` + `side` pair** instead of `participantID` / `OU`:

* `market` — the outcome the market is about: a participant ObjectID hex (home or away team) or the literal string `"draw"`.
* `side` — `"yes"` or `"no"` on that outcome.
* `OU`, `total`, and `spread` are absent, and `participantID` is empty — use `market` + `side`.

Both fields appear at the update level (identifying which orderbook side changed) and on every entry in `sideOrders`.

<CodeGroup>
  ```json JSON theme={null}
  [
    "orderUpdate",
    {
      "epoch": 1234567,
      "gameID": "685e21aa0be1b7d5a3f9c4d2",
      "sport": "soccer",
      "league": "EPL",
      "live": false,
      "type": "moneyline1x2",
      "participantID": "",
      "market": "607349dc22a237cf46b021fb",
      "side": "yes",
      "sideOrders": [
        {
          "id": "685e2f1c40e36d0494601a22",
          "type": "moneyline1x2",
          "sumUntaken": 500,
          "odds": 120,
          "bet": 600,
          "gameID": "685e21aa0be1b7d5a3f9c4d2",
          "takenRatio": 0,
          "participantID": "",
          "market": "607349dc22a237cf46b021fb",
          "side": "yes",
          "gameStartExpiry": true,
          "expiry": "2026-07-04T18:55:00.000Z",
          "createdAt": "2026-07-03T14:20:11.204Z"
        }
      ]
    }
  ]
  ```

  ```json JSON (draw market) theme={null}
  [
    "orderUpdate",
    {
      "epoch": 1234567,
      "gameID": "685e21aa0be1b7d5a3f9c4d2",
      "sport": "soccer",
      "league": "EPL",
      "live": false,
      "type": "moneyline1x2",
      "participantID": "",
      "market": "draw",
      "side": "no",
      "sideOrders": [
        {
          "id": "685e301b40e36d0494601a9f",
          "type": "moneyline1x2",
          "sumUntaken": 250,
          "odds": -145,
          "bet": 250,
          "gameID": "685e21aa0be1b7d5a3f9c4d2",
          "takenRatio": 0,
          "participantID": "",
          "market": "draw",
          "side": "no",
          "gameStartExpiry": true,
          "expiry": "2026-07-04T18:55:00.000Z",
          "createdAt": "2026-07-03T14:22:47.910Z"
        }
      ]
    }
  ]
  ```
</CodeGroup>

### `gameUpdate`

Emitted when a game is created or its state changes (markets opening, closing, start time updates). The payload is the full rendered game.

<CodeGroup>
  ```json JSON theme={null}
  [
    "gameUpdate",
    {
      "id": "625ecb5f269b7ff13619ca7c",
      "parentGameID": null,
      "league": "NBA",
      "sport": "basketball",
      "start": "2022-04-22T01:00:00.000Z",
      "ended": false,
      "messageType": "marketOpen",
      "participants": [
        { "id": "607349dc22a237cf46b021fb", "longName": "Dallas Mavericks", "shortName": "DAL", "homeAway": "away", "rotationNumber": "571" },
        { "id": "60747bcde3b0844e56d2e7e8", "longName": "Utah Jazz",        "shortName": "UTA", "homeAway": "home", "rotationNumber": "572" }
      ],
      "awayMoneylines": [],
      "homeMoneylines": [],
      "awaySpreads": {},
      "homeSpreads": {},
      "over": {},
      "under": {},
      "mainHomeSpread": -6.5,
      "mainAwaySpread": 6.5,
      "mainTotal": 209.5
    }
  ]
  ```
</CodeGroup>

Known `messageType` values: `marketOpen`, `marketClosed`. Treat the field as extensible and ignore unknown values.

### `matchedVolumeUpdate`

Emitted when the total matched volume on a game changes.

<CodeGroup>
  ```json JSON theme={null}
  [
    "matchedVolumeUpdate",
    {
      "gameID": "625ecb5f269b7ff13619ca7c",
      "parentGameID": null,
      "league": "NBA",
      "sport": "basketball",
      "matchedVolume": 12450.75
    }
  ]
  ```
</CodeGroup>
