> For the complete documentation index, see [llms.txt](https://cabalspy.gitbook.io/cabalspy-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cabalspy.gitbook.io/cabalspy-docs/data-stream/bundle.md).

# Bundle

KOL bundle / side-wallet detection on Solana, grouped per token.

## Bundle Stream — KOL Bundle / Side-Wallet Detection (WebSocket)

> Real-time WebSocket stream of \*\*KOL bundles\*\*. CabalSpy detects wallets that a KOL controls or coordinates with — "side wallets" that buy the same token in the same Jito bundle or the same block as the KOL — and delivers them grouped per token, each with a full live position (bought, sold, held, unrealized PnL, remaining value).\
> \
> \*\*Solana only.\*\* A bundle only exists because a KOL bought the token, so the token is always tracked and its market cap is always available — which is why positions and unrealized PnL can be computed for the bundle wallets too.\
> \
> \## Whole-token delivery\
> You always receive the \*\*whole token\*\*, not a single bundle. Every message carries the complete current picture for one token: the \`bundles\` list holds \*\*all\*\* bundles detected in that token (a token can have several, each with a different KOL), and each bundle holds all of its wallets. When anything changes, the entire token is re-sent, so you simply replace your view of that token with the latest message — no need to merge partial updates.\
> \
> \## Each bundle carries its own KOL\
> A token can contain bundles from several different KOLs. Every entry in \`bundles\` therefore has its \*\*own\*\* \`kol\_wallet\` and \`kol\_profile\`, and the KOL is also included inside that bundle's \`bundle\_wallets\` list with \`is\_kol: true\` and the same position fields as the side wallets. Take the KOL identity from each bundle's own \`kol\_wallet\` / \`kol\_profile\` — there is no token-level KOL, because a token can have many.\
> \
> \## Confidence, not two event types\
> Confirmed side wallets and structural leads are merged into one shape, distinguished by \`confidence\`: \`low\` | \`medium\` | \`high\` | \`verified\`. \`verified\` means Jito-confirmed (\`jito\_confirmed: true\`, with a \`bundle\_id\`); lower tiers are on-chain footprint leads (adjacent in block, matching fee, recurrence) that are not Jito-confirmed. Each bundle also carries a human-readable \`proof\` and a \`verify\_hint\`. A wallet's \`transaction\_type\` is \`buy\` or \`sell\`; there is no separate sell event.\
> \
> \## Two subscription modes — this controls how much you receive (and pay)\
> Every delivered message counts as \*\*one credit\*\*. The \`mode\` you subscribe with decides how many messages you get:\
> \
> \- \*\*\`mode: "full"\` (default)\*\* — you receive the token again on every relevant\
> &#x20; change, INCLUDING marketcap ticks, so \`unrealized\_pnl\_\*\` and \`remaining\_\*\`\
> &#x20; stay live to the second. Ideal for a live holder/positions table, but\
> &#x20; chatty: an active token can produce thousands of updates (and thus\
> &#x20; thousands of credits) per hour.\
> \
> \- \*\*\`mode: "events"\`\*\* — you receive the token only on \*\*real events\*\*: a new\
> &#x20; bundle is detected, or a bundle wallet buys or sells. Marketcap-driven\
> &#x20; updates are NOT sent. Positions are still included, computed at the moment\
> &#x20; of the event. This is the economical choice when you only need to know\
> &#x20; \*that\* a bundle exists and \*when\* wallets trade. Example: 3 bundles and 10\
> &#x20; sells on a token cost \*\*13 credits\*\* instead of thousands.\
> \
> \
> Combine \`mode\` with the token selector freely: one token + full (live table), one token + events (cheap event feed), \`\*\` + full (everything live — very high volume), \`\*\` + events (every new bundle network-wide, events only).\
> \
> \## Message events\
> \- \`init\` — one-time snapshot of all known bundles for the token at subscribe\
> &#x20; time, rebuilt from CabalSpy's store (survives restarts; positions recomputed\
> &#x20; with the current market cap). Sent in both modes.\
> \
> \- \`kol\_bundle\` — an update. In \`full\` mode this fires on new detections,\
> &#x20; trades, AND marketcap moves. In \`events\` mode it fires ONLY on new\
> &#x20; detections and trades.\
> \
> \
> \## Connect\
> Open a WebSocket to \`wss\://stream.cabalspy.xyz\` with your API key as a query parameter (\`?apiKey=YOUR\_KEY\`) or a Bearer header (\`Authorization: Bearer YOUR\_KEY\`). You receive a \`{"type":"connected"}\` welcome.\
> \
> \## Subscribe\
> \`{ "op":"subscribe", "stream":"bundle", "blockchain":"solana", "token":"\<mint>", "mode":"events" }\`. Bundles are always token-scoped. Use \`"\*"\` to receive bundles across all tokens. A concrete mint returns an \`init\` snapshot first. Omit \`mode\` (or set \`"full"\`) for live marketcap-driven updates. The server replies with a \`subscribed\` ack echoing the \`filters\` (including the resolved \`mode\`).\
> \
> \## Unsubscribe\
> \`{ "op":"unsubscribe", "stream":"bundle", "token":"\<mint>" }\` — stops all updates for that token.\
> \
> \## List / Ping\
> \`{ "op":"subscriptions" }\` lists active subscriptions; \`{ "op":"ping" }\` returns a pong.\
> \
> \## Envelope\
> Every message: \`{ "success":true, "channel":"bundle.solana", "event":"init|kol\_bundle", "data":{ ... }, "meta":{ ... } }\`.<br>

```json
{"openapi":"3.0.3","info":{"title":"CabalSpy API — Bundle Stream (KOL Bundle / Side-Wallet Detection)","version":"2.1.0"},"tags":[{"name":"Bundle","description":"KOL bundle / side-wallet detection on Solana, grouped per token."}],"servers":[{"url":"wss://stream.cabalspy.xyz","description":"Production WebSocket gateway"}],"security":[{"bearerAuth":[]},{"apiKeyQuery":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer YOUR_KEY"},"apiKeyQuery":{"type":"apiKey","in":"query","name":"apiKey"}},"schemas":{"SubscribeBundleMessage":{"type":"object","description":"Client -> server. Subscribe to a token's bundles; token-scoped. Filters persist until unsubscribe.","properties":{"op":{"type":"string","enum":["subscribe"]},"stream":{"type":"string","enum":["bundle"]},"blockchain":{"type":"string","enum":["solana"],"description":"Must be solana."},"token":{"type":"string","description":"A mint to watch one token, or \"*\" for all tokens."},"mode":{"type":"string","enum":["events","full"],"default":"full","description":"'events' = only real events (new bundle, buy, sell); no marketcap updates; cheapest. 'full' = adds live marketcap-driven position updates (uPNL/remaining stay live); higher credit usage.\n"},"channel":{"type":"string","description":"Alternative to blockchain+token: bundle.solana.<mint> or bundle.solana for all. (Pass mode as a separate field.)","nullable":true}},"required":["op","stream","token"]},"BundleEnvelope":{"type":"object","description":"One bundle message — init snapshot or live kol_bundle update. ALWAYS the whole token: `bundles` contains every bundle detected in the token, each with its own KOL and wallets. Replace your view of the token with each message.\n","properties":{"success":{"type":"boolean"},"channel":{"type":"string"},"event":{"type":"string","enum":["init","kol_bundle"]},"data":{"type":"object","properties":{"blockchain":{"type":"string","enum":["solana"]},"token":{"$ref":"#/components/schemas/BundleToken"},"bundles":{"type":"array","description":"All bundles detected in this token (a token can have several, each with a different KOL).","items":{"$ref":"#/components/schemas/Bundle"}}}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"version":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}},"BundleToken":{"type":"object","description":"Token identity plus live market-cap / price state.","properties":{"mint":{"type":"string"},"symbol":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"supply":{"type":"number","nullable":true},"market_cap":{"type":"number","nullable":true,"description":"Market cap in SOL (price x supply)."},"market_cap_usd":{"type":"number","nullable":true},"market_cap_currency":{"type":"string","enum":["SOL"],"nullable":true},"price":{"type":"number","nullable":true},"price_usd":{"type":"number","nullable":true},"sol_price_usd":{"type":"number","nullable":true},"pool":{"type":"string","nullable":true},"on_curve":{"type":"boolean","nullable":true},"bonding_curve_progress":{"type":"number","nullable":true}}},"Bundle":{"type":"object","description":"One detected bundle within a token — its own KOL, metadata once, wallets listed.","properties":{"bundle_id":{"type":"string","nullable":true,"description":"Jito bundle id when verified; null for structural leads."},"kol_wallet":{"type":"string","description":"The KOL this bundle belongs to."},"kol_profile":{"allOf":[{"$ref":"#/components/schemas/Profile"}],"nullable":true,"description":"Profile of THIS bundle's KOL. Each bundle has its own KOL","so each carries its own profile.":null},"confidence":{"type":"string","enum":["low","medium","high","verified"]},"jito_confirmed":{"type":"boolean"},"proof_type":{"type":"string","description":"jito_bundle, same_transaction, on_chain_footprint, …"},"slot":{"type":"integer","nullable":true},"wallet_count":{"type":"integer"},"proof":{"type":"string","nullable":true,"description":"Human-readable","independently verifiable proof of the link.":null},"verify_hint":{"type":"string","nullable":true,"description":"How to verify the link on-chain yourself."},"detected_at":{"type":"string","nullable":true},"bundle_wallets":{"type":"array","description":"All wallets in this bundle, including the KOL (is_kol:true).","items":{"$ref":"#/components/schemas/BundleWallet"}}}},"Profile":{"type":"object","description":"Identity and classification of a KOL or tracked wallet.","properties":{"name":{"type":"string"},"image_url":{"type":"string"},"twitter":{"type":"string"},"telegram":{"type":"string"},"blockchain":{"type":"string","enum":["solana"]},"currency":{"type":"string","enum":["SOL"]},"type":{"type":"string","enum":["kol","smart","whale"],"nullable":true}}},"BundleWallet":{"type":"object","description":"One member of a bundle — a side wallet or the KOL (is_kol:true).","properties":{"address":{"type":"string"},"is_kol":{"type":"boolean","description":"True for the KOL member of the bundle."},"transaction_type":{"type":"string","enum":["buy","sell"],"description":"The wallet's most recent action in this token."},"signature":{"type":"string","nullable":true},"fee_lamports":{"type":"integer","nullable":true,"description":"Transaction fee (side wallets only) — an on-chain footprint signal."},"block_index":{"type":"integer","nullable":true,"description":"Position within the block (side wallets only)."},"adjacent_to_kol":{"type":"boolean","nullable":true,"description":"Sits directly next to the KOL in the block (side wallets only)."},"same_fee":{"type":"boolean","nullable":true,"description":"Fee identical to the KOL's (side wallets only)."},"occurrences":{"type":"integer","nullable":true,"description":"How often this pair has been seen bundling together (side wallets only)."},"entry_source":{"type":"string","enum":["at_bundle","at_discovery"],"nullable":true,"description":"at_bundle = seen from the bundle (uPNL exact); at_discovery = found later."},"position_source":{"type":"string","enum":["live"],"nullable":true},"profile":{"allOf":[{"$ref":"#/components/schemas/Profile"}],"nullable":true,"description":"Present for the KOL and for side wallets that are themselves tracked; otherwise null."},"position":{"$ref":"#/components/schemas/BundlePosition"}}},"BundlePosition":{"type":"object","description":"A bundle wallet's (or the KOL's) full position in the token. Same definitions as the Holder stream.","properties":{"held":{"type":"number"},"peak":{"type":"number"},"bought_tokens":{"type":"number"},"sold_tokens":{"type":"number"},"bag_pct":{"type":"number","nullable":true},"supply_pct":{"type":"number","nullable":true},"invested":{"type":"number","description":"Cumulative buy (SOL)."},"invested_usd":{"type":"number","nullable":true},"buy_txn":{"type":"integer"},"sell_txn":{"type":"integer"},"sold_value":{"type":"number","description":"Cumulative sold (SOL)."},"sold_value_usd":{"type":"number","nullable":true},"sold":{"type":"boolean"},"realized_pnl_sol":{"type":"number","description":"sold_value − invested (SOL)."},"realized_pnl_usd":{"type":"number","nullable":true},"entry_market_cap":{"type":"number","nullable":true,"description":"Market cap (SOL) at entry. Display only."},"entry_market_cap_usd":{"type":"number","nullable":true},"unrealized_pnl_sol":{"type":"number","nullable":true,"description":"Held bag value − open net cost basis (invested − sold_value). Live in full mode; at event time in events mode."},"unrealized_pnl_usd":{"type":"number","nullable":true},"unrealized_pnl_pct":{"type":"number","nullable":true},"remaining_sol":{"type":"number","nullable":true,"description":"Current value of held bag (SOL). Never negative."},"remaining_usd":{"type":"number","nullable":true},"first_buy_at":{"type":"string","nullable":true},"last_activity_at":{"type":"string","nullable":true}}}}},"paths":{"/bundle":{"get":{"tags":["Bundle"],"summary":"Bundle Stream — KOL Bundle / Side-Wallet Detection (WebSocket)","operationId":"streamBundle","description":"Real-time WebSocket stream of **KOL bundles**. CabalSpy detects wallets that a KOL controls or coordinates with — \"side wallets\" that buy the same token in the same Jito bundle or the same block as the KOL — and delivers them grouped per token, each with a full live position (bought, sold, held, unrealized PnL, remaining value).\n\n**Solana only.** A bundle only exists because a KOL bought the token, so the token is always tracked and its market cap is always available — which is why positions and unrealized PnL can be computed for the bundle wallets too.\n\n## Whole-token delivery\nYou always receive the **whole token**, not a single bundle. Every message carries the complete current picture for one token: the `bundles` list holds **all** bundles detected in that token (a token can have several, each with a different KOL), and each bundle holds all of its wallets. When anything changes, the entire token is re-sent, so you simply replace your view of that token with the latest message — no need to merge partial updates.\n\n## Each bundle carries its own KOL\nA token can contain bundles from several different KOLs. Every entry in `bundles` therefore has its **own** `kol_wallet` and `kol_profile`, and the KOL is also included inside that bundle's `bundle_wallets` list with `is_kol: true` and the same position fields as the side wallets. Take the KOL identity from each bundle's own `kol_wallet` / `kol_profile` — there is no token-level KOL, because a token can have many.\n\n## Confidence, not two event types\nConfirmed side wallets and structural leads are merged into one shape, distinguished by `confidence`: `low` | `medium` | `high` | `verified`. `verified` means Jito-confirmed (`jito_confirmed: true`, with a `bundle_id`); lower tiers are on-chain footprint leads (adjacent in block, matching fee, recurrence) that are not Jito-confirmed. Each bundle also carries a human-readable `proof` and a `verify_hint`. A wallet's `transaction_type` is `buy` or `sell`; there is no separate sell event.\n\n## Two subscription modes — this controls how much you receive (and pay)\nEvery delivered message counts as **one credit**. The `mode` you subscribe with decides how many messages you get:\n\n- **`mode: \"full\"` (default)** — you receive the token again on every relevant\n  change, INCLUDING marketcap ticks, so `unrealized_pnl_*` and `remaining_*`\n  stay live to the second. Ideal for a live holder/positions table, but\n  chatty: an active token can produce thousands of updates (and thus\n  thousands of credits) per hour.\n\n- **`mode: \"events\"`** — you receive the token only on **real events**: a new\n  bundle is detected, or a bundle wallet buys or sells. Marketcap-driven\n  updates are NOT sent. Positions are still included, computed at the moment\n  of the event. This is the economical choice when you only need to know\n  *that* a bundle exists and *when* wallets trade. Example: 3 bundles and 10\n  sells on a token cost **13 credits** instead of thousands.\n\n\nCombine `mode` with the token selector freely: one token + full (live table), one token + events (cheap event feed), `*` + full (everything live — very high volume), `*` + events (every new bundle network-wide, events only).\n\n## Message events\n- `init` — one-time snapshot of all known bundles for the token at subscribe\n  time, rebuilt from CabalSpy's store (survives restarts; positions recomputed\n  with the current market cap). Sent in both modes.\n\n- `kol_bundle` — an update. In `full` mode this fires on new detections,\n  trades, AND marketcap moves. In `events` mode it fires ONLY on new\n  detections and trades.\n\n\n## Connect\nOpen a WebSocket to `wss://stream.cabalspy.xyz` with your API key as a query parameter (`?apiKey=YOUR_KEY`) or a Bearer header (`Authorization: Bearer YOUR_KEY`). You receive a `{\"type\":\"connected\"}` welcome.\n\n## Subscribe\n`{ \"op\":\"subscribe\", \"stream\":\"bundle\", \"blockchain\":\"solana\", \"token\":\"<mint>\", \"mode\":\"events\" }`. Bundles are always token-scoped. Use `\"*\"` to receive bundles across all tokens. A concrete mint returns an `init` snapshot first. Omit `mode` (or set `\"full\"`) for live marketcap-driven updates. The server replies with a `subscribed` ack echoing the `filters` (including the resolved `mode`).\n\n## Unsubscribe\n`{ \"op\":\"unsubscribe\", \"stream\":\"bundle\", \"token\":\"<mint>\" }` — stops all updates for that token.\n\n## List / Ping\n`{ \"op\":\"subscriptions\" }` lists active subscriptions; `{ \"op\":\"ping\" }` returns a pong.\n\n## Envelope\nEvery message: `{ \"success\":true, \"channel\":\"bundle.solana\", \"event\":\"init|kol_bundle\", \"data\":{ ... }, \"meta\":{ ... } }`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeBundleMessage"}}}},"responses":{"200":{"description":"`init` and `kol_bundle` messages. (The connection also delivers a `connected` welcome, a `subscribed` ack per subscribe, and `error` messages — see schemas.)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleEnvelope"}}}}}}}}}
```
