CabalSpy/kol-realtime-holder-table
The full styled demo is open source. This guide rebuilds its multi stream core in TypeScript.
Why three streams
Each stream answers a different question about the same wallet, and they all key on the wallet address, which is what makes the merge clean.
The holder stream is the backbone, it decides which rows exist. The other two decorate those rows. A balance or bundle update for a wallet you are not holding is simply ignored.
Architecture
Three subscriptions write into one store keyed by wallet. The render reads the merged rows.Step 1, the merged row type
One row holds everything about a wallet. Position fields come from the holder stream, balance from the balance stream, and the bundle fields from the bundle stream. The balance and bundle fields are optional, a row is valid with position alone.src/types.ts
Step 2, the merged store
The store keeps one Map keyed by wallet. Each stream has its own writer, and every writer only touches the fields it owns. This is the key idea, the holder writer never overwrites a balance, and the balance writer never creates a row on its own.src/store.ts
Step 3, a small multi subscribe helper
You open one WebSocket and send several subscribe messages on it, one per stream. This helper opens the socket, sends every subscription on open, and routes each message to your handler.src/stream.ts
Step 4, route each stream to the store
Subscribe to all three streams for the same token, then send each event to the writer that owns it. Note the channel prefix on each message tells you which stream it came from, which is how you route balance vs holder vs bundle.src/holders.ts
Step 5, render the merged row
Now each row can show the position from the holder stream, the native SOL balance from the balance stream, and a bundle flag from the bundle stream, all live.src/render.ts
Step 6, wire it together
src/main.ts
index.html
How the merge stays correct
A few rules keep the three streams from fighting each other. The holder stream owns the row set. Only holder events create or remove rows. Balance and bundle events only decorate a row that already exists, so a balance update for some unrelated wallet is harmless. Each writer owns its own fields. The holder writer sets position, the balance writer sets balance, the bundle writer sets the bundle flag. Because they never write each other fields, the order the events arrive in does not matter. Decorations survive a snapshot. When a holder init or position_update replaces the row set, the store carries the existing balance and bundle values forward, so a market cap tick does not wipe the balance you already received.Billing note
You are now subscribed to three channels, so you receive three kinds of events, and each delivered event counts against your plan. On a busy token, use mode events on the holder and bundle streams, or set mc_interval, to keep the market cap driven updates from using credits quickly.Reference
Holder stream
Positions, bag percentage and unrealized PnL.
Balance stream
Native SOL balance per wallet.
Bundle stream
Coordinated bundle detection per token.

