check_holder_concentration
Analyzes token holder distribution to detect supply concentration risks, flagging rug pull indicators when top holders exceed defined thresholds.
Instructions
Analyze the distribution of token holders. Detects if supply is concentrated in a few wallets (rug pull indicator). Flags if top 1 holder >50%, top 5 >80%, or top 10 >90%.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mint | Yes | Solana token mint address |
Implementation Reference
- src/core/holder_analysis.ts:39-125 (handler)The core handler function `analyzeHolders` that executes the tool logic. It uses Solana's getTokenLargestAccounts and getTokenSupply to detect if supply is concentrated (top 1 >50%, top 5 >80%, top 10 >90%). Returns HolderResult with concentration flag, reason, top 10 holders, and stats.
export async function analyzeHolders( connection: Connection, mint: string, ): Promise<HolderResult> { try { const mintPk = new PublicKey(mint); // Get the 20 largest token accounts for this mint const largestAccounts = await connection.getTokenLargestAccounts(mintPk); if (!largestAccounts.value || largestAccounts.value.length === 0) { return { concentrated: false, reason: 'No token accounts found', topHolders: [], stats: { top1Pct: 0, top5Pct: 0, top10Pct: 0, totalSupply: 0 }, }; } // Get total supply const supplyInfo = await connection.getTokenSupply(mintPk); const totalSupply = Number(supplyInfo.value.amount); const decimals = supplyInfo.value.decimals; if (totalSupply === 0) { return { concentrated: false, reason: 'Zero supply token', topHolders: [], stats: { top1Pct: 0, top5Pct: 0, top10Pct: 0, totalSupply: 0 }, }; } // Build holder list sorted by amount (descending) const holders: HolderInfo[] = largestAccounts.value .map(acct => ({ address: acct.address.toBase58(), amount: Number(acct.amount), pct: (Number(acct.amount) / totalSupply) * 100, decimals, })) .sort((a, b) => b.amount - a.amount); // Calculate concentration metrics const top1Pct = holders[0]?.pct ?? 0; const top5Pct = holders.slice(0, 5).reduce((s, h) => s + h.pct, 0); const top10Pct = holders.slice(0, 10).reduce((s, h) => s + h.pct, 0); // Concentration thresholds const isConcentrated = top1Pct > 50 || // Single holder owns >50% top5Pct > 80 || // Top 5 own >80% top10Pct > 90; // Top 10 own >90% let reason: string; if (top1Pct > 50) { reason = `Top holder owns ${top1Pct.toFixed(1)}% — extreme concentration`; } else if (top5Pct > 80) { reason = `Top 5 holders own ${top5Pct.toFixed(1)}% — high concentration`; } else if (top10Pct > 90) { reason = `Top 10 holders own ${top10Pct.toFixed(1)}% — moderate concentration`; } else { reason = `Distribution OK — top holder ${top1Pct.toFixed(1)}%, top 5 ${top5Pct.toFixed(1)}%`; } return { concentrated: isConcentrated, reason, topHolders: holders.slice(0, 10), // return top 10 stats: { top1Pct: Math.round(top1Pct * 10) / 10, top5Pct: Math.round(top5Pct * 10) / 10, top10Pct: Math.round(top10Pct * 10) / 10, totalSupply, }, }; } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); return { concentrated: false, reason: `Holder analysis error: ${msg}`, topHolders: [], stats: { top1Pct: 0, top5Pct: 0, top10Pct: 0, totalSupply: 0 }, }; } } - src/core/holder_analysis.ts:13-30 (schema)Type definitions for HolderResult (output schema) and HolderInfo, defining the structure returned by the handler including concentration flag, reason, top holders list, and stats (top1Pct, top5Pct, top10Pct, totalSupply).
export interface HolderResult { concentrated: boolean; reason: string; topHolders: HolderInfo[]; stats: { top1Pct: number; top5Pct: number; top10Pct: number; totalSupply: number; }; } export interface HolderInfo { address: string; amount: number; pct: number; decimals: number; } - src/mcp/server.ts:136-157 (registration)MCP server registration of the 'check_holder_concentration' tool using server.tool(). Defines the tool name, description, input schema (mint address via Zod), and handler that calls analyzeHolders(connection, mint).
// ── Tool: check_holder_concentration ────────────────────────────────── server.tool( 'check_holder_concentration', `Analyze the distribution of token holders. Detects if supply is concentrated in a few wallets (rug pull indicator). Flags if top 1 holder >50%, top 5 >80%, or top 10 >90%.`, { mint: z.string().describe('Solana token mint address'), }, async ({ mint }) => { try { const result = await analyzeHolders(connection, mint); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], }; } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); return { content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }) }], isError: true, }; } }, ); - src/mcp/tools.ts:65-85 (schema)Declarative tool definition for 'check_holder_concentration' in the TOOLS export object. Provides description explaining concentration thresholds and inputSchema JSON Schema for the mint parameter.
check_holder_concentration: { name: 'check_holder_concentration', description: `Analyze the distribution of token holders for a Solana SPL token. Detects if supply is dangerously concentrated in a few wallets, which is a common rug pull setup. Flags as concentrated if: - Top 1 holder owns >50% of supply - Top 5 holders own >80% of supply - Top 10 holders own >90% of supply Returns the top 10 holders with their addresses, amounts, and percentage of total supply.`, inputSchema: { type: 'object' as const, properties: { mint: { type: 'string', description: 'Solana token mint address to analyze', }, }, required: ['mint'], }, }, - src/api/server.ts:474-498 (registration)REST API endpoint registration at POST /v1/holders that also calls analyzeHolders. Includes input validation, caching, and error handling.
// ── POST /v1/holders — Holder concentration check ──────────────────────── app.post('/v1/holders', async (req, res) => { try { const { mint } = req.body as { mint?: unknown }; if (!isValidMint(mint)) { res.status(400).json({ error: 'Invalid mint address' }); return; } const cached = holderCache.get(mint); if (cached) { res.json({ ...cached, cached: true }); return; } const result = await analyzeHolders(connection, mint); holderCache.set(mint, result); res.json({ ...result, cached: false }); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); res.status(500).json({ error: 'Internal server error', message: msg }); } });