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

# Build a Signal Service

> Turn wallet activity into trade signals, surface tokens where several tracked wallets buy together, and stream live entry and exit alerts. Step by step in TypeScript.

Learn how to turn raw wallet activity into signals, the tokens where several tracked wallets are buying at once, and live entry and exit alerts as they form. This guide covers both the REST cluster scan and the live signal stream in TypeScript, with no CSS, so you can wire it into a dashboard, a Telegram bot or a trading agent.

## Two levels of signal

Start by knowing which you need.

| Source                          | What it gives you                                         | Best for                                         |
| ------------------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| REST /v1/signals (mode cluster) | A snapshot of tokens where several wallets are buying now | A signals page you poll on a schedule            |
| WSS signal stream               | Live entry and exit events with your own thresholds       | Instant alerts pushed the moment a cluster forms |

A common setup uses both, the REST scan to populate a signals page on load, and the stream to push new alerts live on top.

## Part 1, scan clusters over REST

A cluster is a token that several tracked wallets are buying inside a time window. The REST endpoint returns the current clusters in one call.

### Step 1, type the cluster

```typescript src/types.ts theme={null}
export interface ClusterWallet {
  wallet: string;
  profile: { name: string };
  invested?: number;
}

export interface Cluster {
  token: { mint: string; token_name: string };
  cluster: {
    wallet_count: number;
    total_invested: number;
    total_invested_usd: number;
  };
  wallets: ClusterWallet[];
}
```

### Step 2, fetch the clusters

Pass mode cluster, and tune it with min\_wallets (how many wallets make a cluster) and hours (the window). type picks the wallet class.

```typescript src/clusters.ts theme={null}
const API_BASE = 'https://api.cabalspy.xyz/v1';

export async function fetchClusters(minWallets = 3, hours = 6): Promise<import('./types').Cluster[]> {
  const params = new URLSearchParams({
    blockchain: 'solana',
    type: 'kol',
    mode: 'cluster',
    min_wallets: String(minWallets),
    hours: String(hours),
    api_key: import.meta.env.VITE_CABALSPY_KEY as string,
  });
  const res = await fetch(`${API_BASE}/signals?${params}`);
  const body = await res.json();
  if (!body.success) throw new Error(body.error?.message || 'signals failed');
  return body.data.signals;
}
```

### Step 3, render the signals page

Each cluster is a token plus the wallets buying it. Show the count, the money in, and who is buying.

```typescript src/render-clusters.ts theme={null}
import type { Cluster } from './types';

export function renderClusters(el: HTMLElement, clusters: Cluster[]): void {
  el.innerHTML = clusters.map((c) => `
    <div class="signal">
      <strong>${c.token.token_name}</strong>
      bought by ${c.cluster.wallet_count} wallets,
      $${c.cluster.total_invested_usd.toLocaleString()} in
      <div>${c.wallets.map((w) => w.profile.name).join(', ')}</div>
    </div>`).join('');
}
```

Poll this on a timer if you want the page to refresh, the same pattern as any REST snapshot.

## Part 2, live signals over WebSocket

The stream is where the signal service comes alive. You define the rules when you subscribe, and the server pushes an event the moment they are met, an entry when enough wallets buy, an exit when they leave.

### Step 1, understand the thresholds

Thresholds live in a block per wallet type, kol and smart. Inside a block, entry\_at is a list of wallet counts that each fire an entry, and exit\_at is a list of remaining holder counts that each fire an exit. min\_buy is the smallest buy for a wallet to count.

An entry\_at of \[3, 5] fires once when the third qualifying wallet buys, and again at the fifth. If you set both a kol and a smart block, the entry gate is an AND, every type must reach its own count.

### Step 2, connect with your rules

```typescript src/signal-stream.ts theme={null}
export interface SignalThresholds {
  kol?: { min_buy?: number; entry_at?: number[]; exit_at?: number[] };
  smart?: { min_buy?: number; entry_at?: number[]; exit_at?: number[] };
  min_win_rate?: number;
}

export interface Signal {
  signal_kind: 'entry' | 'exit';
  token: { symbol: string; mint: string };
  cluster: { qualifying_total: number };
}

export function connectSignalStream(
  thresholds: SignalThresholds,
  onSignal: (s: Signal) => void,
): () => void {
  const key = import.meta.env.VITE_CABALSPY_KEY as string;
  const ws = new WebSocket(`wss://stream.cabalspy.xyz?apiKey=${encodeURIComponent(key)}`);

  ws.onopen = () => {
    ws.send(JSON.stringify({
      op: 'subscribe', stream: 'signal', blockchain: 'solana', token: '*',
      ...thresholds,
    }));
  };
  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.event === 'signal') onSignal(msg.data as Signal);
  };
  return () => { try { ws.close(); } catch { /* already closing */ } };
}
```

### Step 3, handle the alerts

Now you decide what a signal does. Here it prepends to an alerts feed, but this is where you would fire a Telegram message, a sound, or a webhook.

```typescript src/alerts.ts theme={null}
import { connectSignalStream } from './signal-stream';

export function startAlerts(feed: HTMLElement): () => void {
  return connectSignalStream(
    {
      kol: { min_buy: 0.5, entry_at: [3, 5], exit_at: [1] },
      min_win_rate: 50,
    },
    (s) => {
      const row = document.createElement('div');
      row.textContent = `${s.signal_kind.toUpperCase()} ${s.token.symbol} , ${s.cluster.qualifying_total} wallets`;
      feed.prepend(row);
    },
  );
}
```

### Step 4, wire it together

```typescript src/main.ts theme={null}
import { fetchClusters } from './clusters';
import { renderClusters } from './render-clusters';
import { startAlerts } from './alerts';

async function main() {
  // Populate the page with current clusters.
  renderClusters(document.querySelector('#clusters')!, await fetchClusters());

  // Then stream live entry and exit alerts on top.
  startAlerts(document.querySelector('#alerts')!);
}

main();
```

## Tuning the signal

The thresholds are the whole product, and small changes matter.

Raise entry\_at for stronger, rarer signals. entry\_at of \[5] only fires when five wallets have piled in, fewer false positives, later entries. Lower it for earlier, noisier signals.

Use min\_win\_rate to filter quality. Only count wallets whose lifetime win rate clears a bar, so a cluster of proven wallets is worth more than a cluster of random ones.

Gate on two types for conviction. Set both a kol and a smart block and the signal only fires when KOLs and Smart Money agree, the AND gate turns two feeds into one high conviction trigger.

## Billing note

The REST scan is one request per call. On the stream, each delivered signal is billed a little more than a plain event because it aggregates a whole cluster, so scope your token filter when you can.

## Reference

<CardGroup cols={2}>
  <Card title="GET /v1/signals" href="/api-reference/signals">
    The REST signal endpoint with cluster, entry and exit modes.
  </Card>

  <Card title="WSS signal stream" href="/websockets/signal">
    Live entry and exit signals with per type thresholds.
  </Card>
</CardGroup>
