CabalSpy/kol-realtime-holder-table
The full styled demo is open source. This guide rebuilds its core logic from scratch.
What you will build
By the end you will have a holder table with an initial snapshot fetched over the holder stream, live updates as wallets trade, live price driven updates as the market cap moves, and a small store that keeps it all in sync.How CabalSpy differs from a raw swap feed
If you have built a holder table on a raw swap feed before, you had to track each wallet balance yourself, apply every buy and sell, and guard against drift. The CabalSpy holder stream does that work server side. It sends you a full position per holder, already computed, then keeps sending the updated position. You never add up trades by hand, you just replace a holder position with the latest one you receive. That makes the whole thing simpler. The three events you handle are init for the starting snapshot, holder_update when a single holder trades, and position_update when the market cap moves and the unrealized numbers change.Architecture overview
One WebSocket connection carries everything. A small store holds the holder list keyed by wallet, and three event handlers write into it.Step 1, project setup
Any bundler works. Starting from scratch with Vite and TypeScript is enough, there are no dependencies beyond the browser WebSocket.Terminal
Step 2, type the data
Type the two shapes you receive, a holder and the position it carries. These mirror the fields the holder stream sends.src/types.ts
Step 3, the holder store
The store keeps holders in a Map keyed by wallet address, so an update is a single replace. Keeping a Map, rather than an array, means an update from a trade is O(1) and never duplicates a wallet.src/store.ts
Step 4, connect and subscribe
Open one WebSocket, authenticate with the key in the query string, and subscribe to a token. Subscribing to a single mint returns a starting snapshot, so you get the whole table on connect and live updates after.src/stream.ts
Step 5, handle the three events
This is the heart of it. Route each event to the store. init replaces the list, holder_update upserts the one holder that traded, and position_update refreshes the list when the market cap moves.src/holders.ts
Step 6, render the table
The store hands you a plain array, so rendering is up to you and your framework. Here is a dependency free version that writes rows into a table body. No CSS, style it later.src/render.ts
Step 7, wire it together
A minimal entry point. Give it an API key and a mint, and the table fills in and stays live. The returned function closes the connection when you are done, for example on a route change.src/main.ts
index.html
Choosing a mode
The mode you pass on subscribe controls how much you receive, and how many credits you spend.
With mode full you can pass mc_interval, a number of seconds from 1 to 30, to receive at most one market cap update per token in that window. It is the simplest way to keep a busy token from using credits quickly while still staying live.
Lessons worth knowing
A few things that save you time. The snapshot arrives for free. Because you subscribe to a single mint, the init event gives you the whole current table before any live update, so there is no separate REST call to seed the list. You never compute balances. Unlike a raw swap feed, every holder_update and position_update carries the full, server computed position. Always replace, never add up trades yourself, and you cannot drift. Reconnect cleanly. If the socket closes, reopen and resubscribe. The next init gives you a fresh snapshot, so you recover the correct state automatically without patching up missed messages.Reference
WSS holder stream
The full holder stream reference, every field on init, holder_update and position_update.

