Skip to main content
Axiom’s Holders tab gives traders a real-time view of who holds a token — including balance changes, PnL, buy/sell activity, and LP reserves — all updating live as trades happen. In this guide, you’ll learn how to build a production-ready Holders feature using Mobula’s open-source MTT (Mobula Trader Terminal) codebase.
Live Demo: Check out the working implementation at mtt.gg — navigate to any token pair page to see the holders tab in action.Source Code: The complete codebase is available at github.com/MobulaFi/MTT

What You’ll Build

By the end of this guide, you’ll have a holders tab with:
  • Initial holder data fetched via REST API (positions, PnL, labels)
  • Real-time balance updates via the Multi-Events Stream (swap-enriched events)
  • Authoritative post-balance tracking — no drift from incremental calculations
  • LP reserve sync from Market Details stream (no double-counting)
  • Trade deduplication to prevent over-counting during REST resyncs
  • Periodic REST resync every 30s to catch transfers and missed events

Architecture Overview

The MTT holders feature combines three data sources for accurate, real-time tracking:

Why Three Data Sources?


Step 1: Project Setup

Clone the MTT repository or start from scratch:
Or add the required packages to an existing project:
Configure the SDK client:
Get your free API key at admin.mobula.io

Step 2: Define the Holder Store

The Zustand store manages holder positions, sorting, filtering, and real-time trade application:

Key Design Decisions

Hash-based deduplication prevents the same trade from being applied twice. This is critical because the stream subscription starts before the REST fetch — some trades may arrive via both channels:
LP reserve sync uses the Market Details stream’s approximateReserveToken field as the authoritative LP balance — avoiding double-counting from incremental updates:
Source: src/features/pair/store/usePairHolderStore.ts in the MTT repository.

Step 3: Map Blockchain to Stream Config

The Multi-Events Stream uses separate endpoints for SVM (Solana) and EVM chains, and requires chainId in a specific format:
Important: Solana uses solana:solana as the chainId — not solana:mainnet. EVM chains use the evm:{chainId} format (e.g., evm:1 for Ethereum, evm:56 for BSC).

Step 4: Map Stream Events to Trade Events

The Multi-Events Stream’s swap-enriched events contain raw post-balance fields for both the sender and the swap recipient. You need to resolve which is the base token:

Understanding Post-Balance Fields

The multi-event stream provides four raw post-balance fields per swap event: By comparing baseToken with addressToken0, you determine whether the base token is token0 or token1, and select the correct post-balance fields accordingly.
Source: src/features/pair/hooks/useCombinedHolders.ts in the MTT repository.

Step 5: Build the Combined Holders Hook

This is the core hook that orchestrates REST fetching, stream subscription, and periodic resync:

Why Subscribe Before Fetching?

The stream subscription starts before the REST fetch. This ensures zero trade gaps:
  1. Stream subscription begins (trades start queueing)
  2. REST API fetches the current snapshot
  3. Queued trades get deduped against the snapshot via transaction hash
  4. From this point on, every new trade is applied immediately

Step 6: Apply Trades to Positions

The applyTradesToPositions utility is the core engine that updates holder balances from stream trades. It uses authoritative post-balances when available and falls back to incremental calculations:
Source: src/utils/applyTradesToPositions.ts in the MTT repository.

Step 7: Sync LP Balance from Market Details

The liquidity pool balance must come from an authoritative source — not from incrementally adding/subtracting trade amounts. MTT uses the Market Details stream for this:

Why Not Increment LP Balance From Trades?

Incrementally updating the LP balance from trade amounts causes double-counting:
  1. Stream delivers a trade → LP balance incremented by tradeAmount
  2. REST resync fires 30s later → LP balance is set from REST data (which already includes the trade)
  3. Result: LP balance counted twice, showing > 100% of supply
Using pairData.base.approximateReserveToken from the Market Details stream gives you the on-chain reserve directly — no accumulation, no drift.

Step 8: Update Batching for 60fps Performance

High-frequency streams can fire hundreds of events per second. The UpdateBatcher coalesces updates into a single requestAnimationFrame callback:
This ensures React re-renders at most once per frame, regardless of how many trades arrive.

Step 9: Multi-Events Stream Subscription Details

The Multi-Events Stream is the key ingredient. Here’s the subscription payload format:

Solana (SVM)

EVM (Ethereum, BSC, Base, etc.)

Stream Endpoints

Swap-Enriched Event Data Fields

Each swap-enriched event includes:
For the complete Multi-Events Stream documentation, see the Multi-Events Stream API reference.

Pitfalls and Lessons Learned

Building real-time holders tracking involves several subtle challenges. Here’s what we learned building MTT:

1. Subscribe Before Fetch

Always start the stream subscription before the REST fetch. Otherwise, trades that happen during the fetch window are lost. The dedup mechanism (hash Set) handles any overlap.

2. Post-Balance Resolution

The stream provides raw post-balances as BigInt strings. You need to convert them to human-readable numbers using the tokenAmountRaw / tokenAmount ratio as a decimals factor. If conversion fails, fall back to incremental calculation.

3. Sender vs Recipient

On many DEXes, sender and swapRecipient are different addresses (e.g., router contracts). The Multi-Events Stream provides separate post-balances for each:
  • Sender: rawPostBalance0 / rawPostBalance1
  • Recipient: rawPostBalanceRecipient0 / rawPostBalanceRecipient1

4. LP Double-Counting

Never increment the LP balance from individual trades. The LP pool receives tokens on sells and sends tokens on buys, but these amounts are already reflected in the periodic REST resync. Use the Market Details stream’s approximateReserveToken as the single source of truth.

5. ChainId Format

  • Solana: solana:solana (not solana:mainnet)
  • EVM: evm:{chainId} (e.g., evm:1, evm:56, evm:8453)

Conclusion

You now have a production-ready, real-time holders tab built with:
  • REST API for initial holder snapshots with PnL and labels
  • Multi-Events Stream for instant balance updates with authoritative post-balances
  • Market Details Stream for accurate LP pool balance tracking
  • Hash-based deduplication to prevent double-counting across data sources
  • rAF batching for smooth 60fps rendering under high trade volume

Relevant API Endpoints


Get Started

Create a free Mobula API key and start building your own real-time holders tab!

Live Demo

See the holders tab in action on any pair page

Source Code

Explore the complete MTT codebase

Multi-Events Stream

Full stream API documentation

Holder Positions API

REST API for initial holder data