import type { HolderData } from "./types";
const HELIUS_API = "https://mainnet.helius-rpc.com";
const HELIUS_KEY = process.env.HELIUS_API_KEY!;
export async function fetchHolders(mint: string): Promise<HolderData> {
const { data } = await axios.post(`${HELIUS_API}/?api-key=${HELIUS_KEY}`, {
jsonrpc: "2.0",
id: 1,
method: "getTokenAccounts",
params: { mint, limit: 1000 },
});
const accounts = data.result.token_accounts;
const totalSupply = accounts.reduce(
(sum: number, a: { amount: number }) => sum + a.amount,
0
);
const sorted = accounts.sort(
(a: { amount: number }, b: { amount: number }) => b.amount - a.amount
);
const top10 = sorted.slice(0, 10);
const top10Total = top10.reduce(
(sum: number, a: { amount: number }) => sum + a.amount,
0
);
return {
total: accounts.length,
top10Percentage: (top10Total / totalSupply) * 100,
top10Holders: top10.map((a: { address: string; amount: number }) => ({
address: a.address,
balance: a.amount,
percentage: (a.amount / totalSupply) * 100,
})),
};
}
import WebSocket from "ws";
import type { VolumeData } from "./types";
interface Trade {
timestamp: number;
volumeUsd: number;
side: "buy" | "sell";
}
const tradeHistory: Map<string, Trade[]> = new Map();
export function startVolumeTracker(mint: string): void {
const ws = new WebSocket(
`wss://stream.madeonsol.com/v1/dex-trades?apiKey=${API_KEY}`
);
ws.on("open", () => {
ws.send(JSON.stringify({
action: "subscribe",
filters: { token: mint },
}));
});
ws.on("message", (raw: Buffer) => {
const trade = JSON.parse(raw.toString());
const trades = tradeHistory.get(mint) || [];
trades.push({
timestamp: Date.now(),
volumeUsd: trade.volume_usd,
side: trade.side,
});
tradeHistory.set(mint, trades);
});
ws.on("close", () => {
setTimeout(() => startVolumeTracker(mint), 3000);
});
}
export function getVolume(mint: string): VolumeData {
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const trades = (tradeHistory.get(mint) || []).filter(
(t) => t.timestamp > cutoff
);
return {
volume24h: trades.reduce((sum, t) => sum + t.volumeUsd, 0),
buyVolume24h: trades
.filter((t) => t.side === "buy")
.reduce((sum, t) => sum + t.volumeUsd, 0),
sellVolume24h: trades
.filter((t) => t.side === "sell")
.reduce((sum, t) => sum + t.volumeUsd, 0),
tradeCount24h: trades.length,
};
}
Step 5: Fetch liquidity data
Liquidity depth determines how much slippage a trade will experience. DexScreener provides a convenient API for pool-level liquidity data:
import type { LiquidityData } from "./types";
export async function fetchLiquidity(mint: string): Promise<LiquidityData> {
const { data } = await axios.get(
`https://api.dexscreener.com/latest/dex/tokens/${mint}`
);
const pools = (data.pairs || []).map(
(pair: { dexId: string; pairAddress: string; liquidity: { usd: number } }) => ({
dex: pair.dexId,
pairAddress: pair.pairAddress,
liquidityUsd: pair.liquidity?.usd || 0,
})
);
return {
totalUsd: pools.reduce(
(sum: number, p: { liquidityUsd: number }) => sum + p.liquidityUsd,
0
),
pools: pools.slice(0, 10),
};
}
Step 6: Assemble the dashboard endpoint
Now tie everything together in a single aggregator that returns the full TokenMetrics object:
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(),
};
}
You can serve this from an Express or Fastify endpoint, or call it from a Next.js API route. The Promise.all call ensures price, holder, and liquidity data fetch in parallel, keeping latency low.
Adding a caching layer
Fetching all four data sources on every request is wasteful and will hit rate limits quickly. Add a simple in-memory cache with per-source TTLs:
const cache = new Map<string, { data: unknown; expiry: number }>();
function getCached<T>(key: string): T | null {
const entry = cache.get(key);
if (!entry || Date.now() > entry.expiry) return null;
return entry.data as T;
}
function setCache(key: string, data: unknown, ttlMs: number): void {
cache.set(key, { data, expiry: Date.now() + ttlMs });
}
Recommended TTLs: price data at 10 seconds, holder data at 5 minutes, liquidity at 60 seconds. Volume is already tracked in memory via the WebSocket stream, so it needs no cache.
Error handling and resilience
Production dashboards need to handle failures gracefully. Each data source can fail independently, so never let one source block the entire response:
async function safeFetch<T>(
fn: () => Promise<T>,
fallback: T,
label: string
): Promise<T> {
try {
return await fn();
} catch (err) {
console.error(`[${label}] fetch failed:`, err);
return fallback;
}
}
Wrap each data fetcher in safeFetch so the dashboard returns partial data rather than a 500 error when one provider is down.
For the WebSocket volume tracker, the reconnection logic in Step 4 handles disconnections automatically. In production, add exponential backoff and a maximum retry count.
Extending the dashboard
Once the core metrics are in place, consider these additions:
- Price charts: Store price snapshots in a time-series database (TimescaleDB or QuestDB) and render candlestick charts on the frontend.
- Whale alerts: Flag any trade above a USD threshold from the WebSocket stream. See our guide on reading Solana smart money for strategies.
- Fresh-launch filtering: Reuse the same DEX firehose from Step 4 to surface brand-new Pump.fun tokens instead of established ones — our guide to building a Solana memecoin sniper bot covers the filtering and execution pattern in full.
- gRPC streaming: For lower latency than WebSocket polling, connect directly to Solana validators via gRPC. Our Solana gRPC streaming guide covers the setup.
- Multi-token views: Track a watchlist of tokens and render a sortable table with all metrics side-by-side.
- Risk scoring: Combine holder concentration, liquidity depth, and volume patterns into a single risk score.
FAQ
What is the best API for building a Solana token analytics dashboard?
It depends on which metrics matter most. The MadeOnSol API covers price feeds and real-time DEX trade streaming across all major Solana programs. Helius is the strongest option for holder data and token account queries via the DAS API. DexScreener provides reliable liquidity and pair-level data. For most dashboards, combining two or three of these sources gives you complete coverage without redundancy.
How do I get real-time token price updates without polling?
The most efficient approach is subscribing to a DEX trade WebSocket stream and computing price from incoming trades. MadeOnSol provides a filtered WebSocket feed that covers Raydium, Jupiter, Orca, Pump.fun, and other Solana DEXs. Each trade event includes the USD volume and token amounts, so you can derive the latest price from the most recent swap. This eliminates polling entirely and gives you sub-second price updates.
How often should I refresh holder data for a Solana token?
Holder data changes much slower than price or volume, so refreshing every 5 to 15 minutes is sufficient for most use cases. Fetching holder lists is also one of the more expensive API calls in terms of rate limits, especially for tokens with thousands of holders. If you need near-real-time holder tracking, consider subscribing to token account changes via Solana gRPC streams rather than polling the full holder list repeatedly.
Can I build this dashboard without paying for any APIs?
You can get started on free tiers. Helius offers a free plan with enough requests for holder queries at moderate refresh rates. DexScreener's token endpoint is publicly accessible. The MadeOnSol API includes a free tier for basic price lookups. The main limitation on free plans is rate limits, which matter once you are tracking more than a handful of tokens or need sub-minute refresh intervals. For production use with real-time streaming, a paid plan on at least one provider is recommended.