Skip to main content
Learn how to build a real-time token holder table, a live view of who holds a token, with each holder position, bag percentage and unrealized PnL updating as trades happen. This guide walks the frontend logic end to end in TypeScript, with no CSS, so you can drop it into any framework and style it yourself.

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
Get your API key at apidashboard.cabalspy.xyz. The key is passed when you open the connection, so keep it out of source control and load it from an environment variable in production.

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
And the markup it writes into, the only HTML you need.
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.