// Token holders: holder count and concentration for a Solana token. Zero dependencies. import { pathToFileURL } from "node:url"; const JUP = "https://lite-api.jup.ag"; const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; const num = (v) => (v === null || v === undefined || !Number.isFinite(Number(v)) ? null : Number(v)); async function rpc(url, method, params) { const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), }); const body = await res.json().catch(() => null); if (!res.ok || body?.error) throw new Error(body?.error?.message ?? `RPC answered ${res.status}`); return body.result; } async function largestAccounts(rpcUrl, mint, supply) { const largest = await rpc(rpcUrl, "getTokenLargestAccounts", [mint]); const accounts = (largest?.value ?? []).slice(0, 20); const infos = await rpc(rpcUrl, "getMultipleAccounts", [accounts.map((a) => a.address), { encoding: "jsonParsed" }]); return accounts.map((a, i) => { const amount = num(a.uiAmountString ?? a.uiAmount); return { rank: i + 1, tokenAccount: a.address, owner: infos?.value?.[i]?.data?.parsed?.info?.owner ?? null, amount, pctOfSupply: supply && amount !== null ? (amount / supply) * 100 : null, }; }); } export async function holders(mint, { rpcUrl = process.env.SOLANA_RPC_URL } = {}) { if (!MINT_RE.test(String(mint ?? ""))) throw new Error("Usage: holders.mjs "); const res = await fetch(`${JUP}/tokens/v2/search?query=${mint}`, { headers: { accept: "application/json" } }); if (!res.ok) throw new Error(`Jupiter answered ${res.status}`); const list = await res.json(); const t = (Array.isArray(list) ? list : []).find((x) => x?.id === mint); if (!t) throw new Error(`Jupiter has no data for mint ${mint}`); let largest = null; let largestError = null; if (rpcUrl) { try { largest = await largestAccounts(rpcUrl, mint, num(t.totalSupply)); } catch (e) { largestError = String(e?.message ?? e); } } return { source: rpcUrl ? "jupiter + your RPC" : "jupiter", fetchedAt: new Date().toISOString(), mint, symbol: t.symbol ?? null, name: t.name ?? null, holderCount: num(t.holderCount), holderChange24hPct: num(t.stats24h?.holderChange), topHoldersPct: num(t.audit?.topHoldersPercentage), devMints: num(t.audit?.devMints), mintAuthorityDisabled: t.audit?.mintAuthorityDisabled ?? null, freezeAuthorityDisabled: t.audit?.freezeAuthorityDisabled ?? null, organicScore: num(t.organicScore), isVerified: t.isVerified ?? null, totalSupply: num(t.totalSupply), largest, ...(largestError ? { largestError } : {}), }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { holders(process.argv[2]) .then((r) => console.log(JSON.stringify(r, null, 2))) .catch((e) => { console.log(JSON.stringify({ error: String(e?.message ?? e) })); process.exit(1); }); }