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

# Signal Stream

> Live WebSocket stream of entry and exit signals when clusters of tracked KOL or Smart Money wallets buy or sell the same token together.

Live trade signals as they form. When enough tracked wallets of a type buy the same token, an entry signal fires. When enough of them exit, an exit signal fires. Each signal carries the cluster of wallets, the token with live market cap, per wallet positions with unrealized PnL, and the trigger trade.

You define the thresholds when you subscribe, per wallet type, so the same stream can drive a simple two KOL alert or a gated KOL plus Smart Money strategy with win rate and token age filters.

## Endpoint

Connect to `wss://stream.cabalspy.xyz`.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const ws = new WebSocket("wss://stream.cabalspy.xyz?apiKey=YOUR_KEY");

  ws.onopen = () => {
    ws.send(JSON.stringify({
      op: "subscribe", stream: "signal", blockchain: "solana", token: "*",
      kol: { min_buy: 0.5, entry_at: [3, 5], exit_at: [1] },
      min_win_rate: 50,
    }));
  };

  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.event === "signal") {
      const d = msg.data;
      console.log(d.signal_kind, d.token.symbol, "wallets", d.cluster.qualifying_total);
    }
  };
  ```

  ```python Python theme={null}
  import json, websocket

  def on_open(ws):
      ws.send(json.dumps({
          "op": "subscribe", "stream": "signal", "blockchain": "solana", "token": "*",
          "kol": {"min_buy": 0.5, "entry_at": [3, 5], "exit_at": [1]},
          "min_win_rate": 50,
      }))

  def on_message(ws, message):
      msg = json.loads(message)
      if msg.get("event") == "signal":
          d = msg["data"]
          print(d["signal_kind"], d["token"]["symbol"])

  ws = websocket.WebSocketApp(
      "wss://stream.cabalspy.xyz?apiKey=YOUR_KEY",
      on_open=on_open, on_message=on_message,
  )
  ws.run_forever()
  ```
</CodeGroup>

## Authentication

Provide your API key as a query parameter, wss\://stream.cabalspy.xyz?apiKey=YOUR\_KEY, or as a header, Authorization: Bearer YOUR\_KEY. Invalid keys are closed with code 1008.

## Chains and wallet types

| blockchain | Wallet types      | Native currency |
| ---------- | ----------------- | --------------- |
| solana     | kol, smart, whale | SOL             |
| bnb        | kol, smart        | BNB             |
| base       | kol, smart        | ETH             |
| eth        | kol               | ETH             |
| rh         | kol, smart        | ETH             |

Smart Money is not available on eth, so a smart block on that chain has no effect. min\_buy and every value in the payload are denominated in the chain native currency, so a min\_buy of 0.5 means 0.5 SOL on Solana and 0.5 BNB on BNB Chain.

Live market cap, entry market cap and unrealized PnL are now delivered on **every chain**. On BNB, Base, Ethereum and Robinhood these fields used to be null and are now filled.

## Subscribe

To start receiving signals, send a subscribe message after the connection opens. You pick a chain, a token to watch, and the rules that decide when a signal fires.

The simplest subscription fires an entry when three or more KOL wallets buy the same token, and an exit when only one is left holding.

```json Simple KOL cluster theme={null}
{
  "op": "subscribe",
  "stream": "signal",
  "blockchain": "solana",
  "token": "*",
  "kol": { "entry_at": [3], "exit_at": [1] }
}
```

The token field is "\*" for all tokens, or a single token mint or contract address to watch one token. The rules live in a kol block and, if you want Smart Money too, a smart block. Each block has the same shape.

Inside a block, entry\_at is a list of wallet counts that each fire an entry signal. For example entry\_at of \[3, 5] fires once when the third qualifying wallet buys, and again when the fifth does. exit\_at is a list of remaining holder counts that fire an exit, so \[1] fires when only one qualifying wallet is still holding. min\_buy is the smallest buy, in native currency, for a wallet to count. max\_wallet\_buy is an optional upper limit per wallet.

```json KOL and Smart Money together, with filters theme={null}
{
  "op": "subscribe",
  "stream": "signal",
  "blockchain": "solana",
  "token": "*",
  "kol":   { "min_buy": 0.5, "entry_at": [3, 5], "exit_at": [1] },
  "smart": { "min_buy": 1.0, "entry_at": [2] },
  "min_win_rate": 50,
  "min_token_age": 1,
  "max_token_age": 48
}
```

```json The same cluster on BNB, min_buy is in BNB theme={null}
{
  "op": "subscribe",
  "stream": "signal",
  "blockchain": "bnb",
  "token": "*",
  "kol":   { "min_buy": 0.2, "entry_at": [3], "exit_at": [1] },
  "smart": { "min_buy": 0.5, "entry_at": [2] }
}
```

When you provide both a kol and a smart block, the entry gate is an AND. Every configured type must reach its own entry\_at minimum before an entry signal fires.

<ParamField body="op" type="string" required>
  The operation. Use subscribe to start.
</ParamField>

<ParamField body="stream" type="string" required>
  Set to signal for this stream.
</ParamField>

<ParamField body="blockchain" type="string" required>
  Chain. One of solana, bnb, base, eth, rh. Smart Money is not available on eth.
</ParamField>

<ParamField body="token" type="string">
  Which token to watch. Either "\*" for all tokens, or a single token mint or contract address.
</ParamField>

<ParamField body="kol" type="object">
  KOL rules. Fields: min\_buy (minimum buy per wallet, native currency), entry\_at (list of wallet counts that fire an entry, up to 3 values), exit\_at (list of remaining holder counts that fire an exit, up to 2 values), max\_wallet\_buy (optional per wallet cap).
</ParamField>

<ParamField body="smart" type="object">
  Smart Money rules, same shape as kol. Only where Smart Money is supported.
</ParamField>

<ParamField body="min_win_rate" type="number">
  Only count wallets whose lifetime win rate is at least this percentage.
</ParamField>

<ParamField body="include_wallets" type="array">
  Whitelist. Only these wallet addresses count toward a cluster.
</ParamField>

<ParamField body="exclude_wallets" type="array">
  Blacklist. These wallet addresses never count.
</ParamField>

<ParamField body="min_token_age" type="number">
  Only fire for tokens at least this many hours old, measured from the first tracked buy.
</ParamField>

<ParamField body="max_token_age" type="number">
  Only fire for tokens at most this many hours old.
</ParamField>

If you send threshold fields at the top level without a kol or smart block, they are treated as the KOL block, so older integrations keep working. The subscribe reply echoes back the rules it parsed and any warnings, for example an unknown field, or a win rate filter you asked for while win rate data is temporarily unavailable.

## Unsubscribe and other operations

To stop, send op unsubscribe with the same chain and token. This removes the subscription for that token regardless of which types you configured.

```json Unsubscribe theme={null}
{
  "op": "unsubscribe",
  "stream": "signal",
  "blockchain": "solana",
  "token": "*"
}
```

To see your active subscriptions, send op subscriptions. To check the connection is alive, send op ping and you get a pong back.

```json List your subscriptions theme={null}
{ "op": "subscriptions", "stream": "signal" }
```

## Event

Each fired signal delivers a signal event.

<ResponseField name="event" type="string">
  signal.
</ResponseField>

<ResponseField name="data" type="object">
  <Expandable title="data">
    <ResponseField name="signal_kind" type="string">
      entry or exit.
    </ResponseField>

    <ResponseField name="blockchain" type="string">
      The chain.
    </ResponseField>

    <ResponseField name="wallet_types" type="array">
      The wallet types the signal covers.
    </ResponseField>

    <ResponseField name="mint" type="string">
      The token mint or contract address.
    </ResponseField>

    <ResponseField name="token" type="object">
      Token block with mint, symbol, name, supply, decimals, and live market\_cap, market\_cap\_usd, market\_cap\_currency, pool, on\_curve and bonding\_curve\_progress. Delivered on every chain. bonding\_curve\_progress is Solana only and null elsewhere.
    </ResponseField>

    <ResponseField name="threshold" type="object">
      What fired, with fired\_on, exit\_type and exit\_remaining for exits, and the per type thresholds that were configured.
    </ResponseField>

    <ResponseField name="cluster" type="object">
      <Expandable title="cluster">
        <ResponseField name="qualifying_total" type="integer">
          Total qualifying wallets across all configured types.
        </ResponseField>

        <ResponseField name="total_invested" type="number">
          Total invested by the cluster in native currency.
        </ResponseField>

        <ResponseField name="total_invested_usd" type="number">
          Total invested in USD.
        </ResponseField>

        <ResponseField name="unrealized_pnl_sol" type="number">
          Sum of the cluster wallet unrealized PnL in the chain native currency. Despite the field name this carries BNB or ETH on those chains, read currency to label it.
        </ResponseField>

        <ResponseField name="unrealized_pnl_usd" type="number">
          Cluster unrealized PnL in USD.
        </ResponseField>

        <ResponseField name="unrealized_pnl_pct" type="number">
          Portfolio weighted unrealized PnL percentage.
        </ResponseField>

        <ResponseField name="currency" type="string">
          Native currency. SOL, BNB or ETH. Each configured type also appears here as an object with qualifying, sold and holding counts.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="trigger" type="object">
      The trade that fired the signal, with wallet, action, value, signature and created\_at.
    </ResponseField>

    <ResponseField name="wallets" type="array">
      <Expandable title="wallet in the cluster">
        <ResponseField name="wallet" type="string">
          Wallet address.
        </ResponseField>

        <ResponseField name="wallet_type" type="string">
          kol or smart.
        </ResponseField>

        <ResponseField name="win_rate" type="number">
          Lifetime win rate, or null.
        </ResponseField>

        <ResponseField name="profile" type="object">
          Wallet profile.
        </ResponseField>

        <ResponseField name="invested" type="number">
          Invested in native currency.
        </ResponseField>

        <ResponseField name="invested_usd" type="number">
          Invested in USD.
        </ResponseField>

        <ResponseField name="max_single_buy" type="number">
          Largest single buy.
        </ResponseField>

        <ResponseField name="buy_txn" type="integer">
          Number of buys.
        </ResponseField>

        <ResponseField name="sell_txn" type="integer">
          Number of sells.
        </ResponseField>

        <ResponseField name="sold_value" type="number">
          Total sold value.
        </ResponseField>

        <ResponseField name="sold" type="boolean">
          Whether the wallet has sold.
        </ResponseField>

        <ResponseField name="bag_pct" type="number">
          Percent of peak holding still held, or null.
        </ResponseField>

        <ResponseField name="first_buy_at" type="string">
          Timestamp of the first buy.
        </ResponseField>

        <ResponseField name="entry_market_cap" type="number">
          Market cap in the chain native currency when the wallet first bought, frozen. Delivered on every chain.
        </ResponseField>

        <ResponseField name="entry_market_cap_usd" type="number">
          Entry market cap in USD.
        </ResponseField>

        <ResponseField name="unrealized_pnl_sol" type="number">
          Unrealized PnL on the held bag in the chain native currency.
        </ResponseField>

        <ResponseField name="unrealized_pnl_pct" type="number">
          Unrealized PnL percentage.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<Note>
  Market cap, entry\_market\_cap and unrealized PnL are only filled while a current market cap for the token is known. If a token has not traded since the stream started, these stay null until its first price arrives.
</Note>

<ResponseExample>
  ```json signal theme={null}
  {
    "success": true,
    "channel": "signal.solana",
    "event": "signal",
    "data": {
      "signal_kind": "entry",
      "blockchain": "solana",
      "wallet_types": ["kol"],
      "mint": "3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
      "token": {
        "mint": "3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
        "symbol": "invisibull",
        "name": "invisibull",
        "supply": 1000000000,
        "decimals": 6,
        "market_cap": 40.08,
        "market_cap_usd": 3301.2,
        "market_cap_currency": "SOL",
        "pool": "pump",
        "on_curve": true,
        "bonding_curve_progress": 42.5
      },
      "threshold": {
        "fired_on": "entry",
        "exit_type": null,
        "exit_remaining": null,
        "currency": "SOL",
        "kol": { "min_buy": 0.5, "entry_at": [3, 5], "exit_at": [1], "max_wallet_buy": null }
      },
      "cluster": {
        "kol": { "qualifying": 5, "sold": 0, "holding": 5 },
        "qualifying_total": 5,
        "total_invested": 12.4,
        "total_invested_usd": 1021.4,
        "unrealized_pnl_sol": -0.4,
        "unrealized_pnl_usd": -32.9,
        "unrealized_pnl_pct": -3.2,
        "currency": "SOL"
      },
      "trigger": {
        "wallet": "7j7AA3HZR2zEjwAQEKPFh2qucLY4fqZpB9iodf39w8xW",
        "action": "buy",
        "value": 3.2,
        "signature": "5xr8abc...",
        "created_at": "2026-07-06T10:52:00Z"
      },
      "wallets": [
        {
          "wallet": "7j7AA3HZR2zEjwAQEKPFh2qucLY4fqZpB9iodf39w8xW",
          "wallet_type": "kol",
          "win_rate": 61.34,
          "profile": {
            "name": "Maze",
            "image_url": "https://cabalspy.xyz/images/7j7AA3HZR2zEjwAQEKPFh2qucLY4fqZpB9iodf39w8xW.png",
            "twitter": "https://x.com/MazeCCC",
            "telegram": "",
            "blockchain": "solana",
            "currency": "SOL",
            "type": "kol"
          },
          "invested": 3.2,
          "invested_usd": 263.6,
          "max_single_buy": 3.2,
          "buy_txn": 1,
          "sell_txn": 0,
          "sold_value": 0.0,
          "sold": false,
          "bag_pct": 100.0,
          "first_buy_at": "2026-07-06T10:20:00Z",
          "entry_market_cap": 44.6,
          "entry_market_cap_usd": 3673.2,
          "unrealized_pnl_sol": -0.1,
          "unrealized_pnl_pct": -3.2
        }
      ]
    },
    "meta": {
      "request_id": "req_5162c7cf7f87",
      "version": "2.0.0",
      "timestamp": "2026-07-06T11:01:57Z"
    }
  }
  ```

  ```json signal on BNB, every value in BNB theme={null}
  {
    "success": true,
    "channel": "signal.bnb",
    "event": "signal",
    "data": {
      "signal_kind": "entry",
      "blockchain": "bnb",
      "wallet_types": ["kol"],
      "mint": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
      "token": {
        "mint": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
        "symbol": "FOUR",
        "name": "Four Token",
        "supply": 1000000000,
        "decimals": 18,
        "market_cap": 1250.5,
        "market_cap_usd": 750300.0,
        "market_cap_currency": "BNB",
        "pool": "0xpool1234567890abcdef1234567890abcdef1234",
        "on_curve": true,
        "bonding_curve_progress": null
      },
      "threshold": {
        "fired_on": "entry",
        "exit_type": null,
        "exit_remaining": null,
        "currency": "BNB",
        "kol": { "min_buy": 0.2, "entry_at": [3], "exit_at": [1], "max_wallet_buy": null }
      },
      "cluster": {
        "kol": { "qualifying": 3, "sold": 0, "holding": 3 },
        "qualifying_total": 3,
        "total_invested": 2.1,
        "total_invested_usd": 1260.0,
        "unrealized_pnl_sol": 0.35,
        "unrealized_pnl_usd": 210.0,
        "unrealized_pnl_pct": 16.6,
        "currency": "BNB"
      },
      "trigger": {
        "wallet": "0x7a16ff8270133f063aab6c9977183d9e72835428",
        "action": "buy",
        "value": 0.5,
        "signature": "0xabc123...",
        "created_at": "2026-07-17T09:12:04Z"
      },
      "wallets": [
        {
          "wallet": "0x7a16ff8270133f063aab6c9977183d9e72835428",
          "wallet_type": "kol",
          "win_rate": 58.2,
          "profile": {
            "name": "BNB Whale",
            "image_url": null,
            "twitter": "",
            "telegram": "",
            "blockchain": "bnb",
            "currency": "BNB",
            "type": "kol"
          },
          "invested": 0.5,
          "invested_usd": 300.0,
          "max_single_buy": 0.5,
          "buy_txn": 1,
          "sell_txn": 0,
          "sold_value": 0.0,
          "sold": false,
          "bag_pct": 100.0,
          "first_buy_at": "2026-07-17T09:12:04Z",
          "entry_market_cap": 1070.0,
          "entry_market_cap_usd": 642000.0,
          "unrealized_pnl_sol": 0.084,
          "unrealized_pnl_pct": 16.8
        }
      ]
    },
    "meta": {
      "request_id": "req_9a1b2c3d4e5f",
      "version": "2.0.0",
      "timestamp": "2026-07-17T09:12:05Z"
    }
  }
  ```
</ResponseExample>

## Billing

Signals are billed more heavily than a single event because each one aggregates a whole cluster. Every delivered signal deducts a fixed number of credits and is logged.

## Heartbeat

The server sends WebSocket pings and expects pong, which standard clients answer automatically. You can also send op ping at any time.
