Building a token analytics dashboard is one of the most practical projects for anyone working in the Solana ecosystem. Traders want a unified view of price action, holder distribution, volume trends, and liquidity depth before making decisions. Developers building trading bots or portfolio trackers need the same data in a structured format.
This tutorial walks through building a Solana token analytics dashboard from scratch. You will pull data from multiple sources, normalize it, and render it in a single dashboard view. By the end, you will have a working TypeScript application that fetches and displays comprehensive token metrics.
Why build a custom Solana token analytics dashboard
Platforms like Birdeye and DexScreener provide excellent analytics UIs, but they serve a general audience. A custom dashboard lets you:
- Filter for specific tokens you care about, without noise
- Combine data sources that no single platform aggregates
- Trigger alerts based on custom thresholds (holder concentration, liquidity drops, volume spikes)
- Feed downstream systems like trading bots or risk engines
- Own your data pipeline without rate-limit surprises on free tiers
If you are building any kind of Solana trading tool, a token analytics layer is foundational. For a broader look at available data providers, see our guide on the best Solana API providers. And if you're still getting oriented on the categories — RPC versus enhanced versus data APIs — our primer on what a Solana API actually is lays out which type each metric below comes from. If you would rather track your own holdings than build per-token analytics, an off-the-shelf option like the one covered in our CoinStats portfolio-tracking review handles DeFi position detection across chains out of the box.
Architecture overview
The dashboard pulls four categories of data:
| Data type | Source | Update frequency |
|---|
| Price feed | MadeOnSol API / on-chain | Real-time or polling |
| Holder data | MadeOnSol /tokens/{mint}/holders (PRO) | Every 1-5 minutes |
| Trading volume | MadeOnSol DEX stream | Real-time via WebSocket |
| Liquidity | DexScreener or on-chain pools | Every 1-5 minutes |
The application is a Node.js backend that aggregates these feeds and exposes a normalized JSON endpoint. You can connect any frontend framework to consume it.
Prerequisites
- Node.js 18+
- TypeScript 5+
- A MadeOnSol API key -- get one here (PRO for the holders endpoint, ULTRA for the DEX firehose)
- Basic knowledge of Solana token standards (SPL tokens, mints)
Install the dependencies:
npm init -y
npm install @solana/web3.js axios ws dotenv
npm install -D typescript @types/node @types/ws tsx
Step 1: Project setup and types
Create a src/types.ts file to define the core data structures for your dashboard:
export interface TokenMetrics {
mint: string;
symbol: string;
name: string;
price: PriceData;
holders: HolderData;
volume: VolumeData;
liquidity: LiquidityData;
lastUpdated: number;
}
export interface PriceData {
current: number;
change24h: number;
high24h: number;
low24h: number;
}
export interface HolderData {
total: number;
top10Percentage: number;
top10Holders: { address: string; balance: number; percentage: number }[];
}
export interface VolumeData {
volume24h: number;
buyVolume24h: number;
sellVolume24h: number;
tradeCount24h: number;
}
export interface LiquidityData {
totalUsd: number;
pools: { dex: string; pairAddress: string; liquidityUsd: number }[];
}
Step 2: Fetch real-time price data
The MadeOnSol API provides token price data with a single call. This avoids the complexity of parsing on-chain pool state yourself.
import axios from "axios";
import type { PriceData } from "./types";
const MADEONSOL_API = "https://madeonsol.com/api/v1";
const API_KEY = process.env.MADEONSOL_API_KEY!;
export async function fetchPrice(mint: string): Promise<PriceData> {
const { data } = await axios.get(`${MADEONSOL_API}/tokens/${mint}/price`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
return {
current: data.priceUsd,
change24h: data.priceChange24h,
high24h: data.high24h,
low24h: data.low24h,
};
}
For real-time price updates without polling, you can connect to the MadeOnSol WebSocket DEX stream and calculate price from incoming trades. See How to Stream Solana DEX Trades with WebSockets for the full setup.
Step 3: Fetch holder distribution
Holder data tells you how concentrated ownership is. A token where the top 10 wallets hold 90% of supply carries different risk than one with broad distribution. The MadeOnSol API exposes this directly: GET /api/v1/tokens/{mint}/holders (PRO) runs a live, full holder census of the mint at read time and returns the exact holder_count, the top holders (merged per owner, with pool / bonding-curve / burn accounts already excluded and named), and a concentration block with top10_share, top20_share, deployer_pct, kol_pct, bundle_pct and more. No RPC plumbing on your side:
import type { HolderData } from "./types";
export async function fetchHolders(mint: string): Promise<HolderData> {
const { data } = await axios.get(`${MADEONSOL_API}/tokens/${mint}/holders`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
// holders[] is ranked, already merged per owner, pools/curves/burns excluded.
// concentration.holder_count is exact from the census (null only when the
// provider refuses the census for a mega-cap and the endpoint falls back to top-20).
const top10 = data.holders.slice(0, 10);
return {
total: data.concentration.holder_count ?? data.concentration.token_accounts_nonzero,
top10Percentage: data.concentration.top10_share,
top10Holders: top10.map((h: { owner: string; amount: number; pct_of_circulating: number }) => ({
address: h.owner,
balance: h.amount,
percentage: h.pct_of_circulating,
})),
};
}
Each holder row also carries labels (deployer, kol, early_buyer, bundle, dump_cluster) so you can flag risky concentration without a second lookup. The response is cached per mint for a short window, so polling every minute is fine.
For a deeper dive into indexing options if you also want raw token-account access, check out the best Solana data indexers.
import type { TokenMetrics } from "./types";
import { fetchPrice } from "./price";
import { fetchHolders } from "./holders";
import { getVolume } from "./volume";
import { fetchLiquidity } from "./liquidity";
export async function getTokenMetrics(
mint: string,
symbol: string,
name: string
): Promise<TokenMetrics> {
const [price, holders, liquidity] = await Promise.all([
fetchPrice(mint),
fetchHolders(mint),
fetchLiquidity(mint),
]);
const volume = getVolume(mint);
return {
mint,
symbol,
name,
price,
holders,
volume,
liquidity,
lastUpdated: Date.now(),
};
}