A reproducible, open snapshot of a Solana token's top holders. SPL Token and Token 2022 aware, token accounts grouped by owner, BigInt math only, ranked, and sealed with a sha256 so anyone can recompute the same list from the same recorded data.
Built for the Kognai jobs "KOG holder snapshot tool" and "KOG top holder snapshot". MIT licence. No runtime dependency (Node 20 or later, node:crypto only).
curl -sO https://agentgate-source.pages.dev/kog-holder-snapshot/kog-holder-snapshot.bundle
git clone kog-holder-snapshot.bundle kog-holder-snapshot && cd kog-holder-snapshot
npm ci && npm test # 16 tests, 0 failing, no network
npm run mutants # planted bug check: 9 of 9 planted bugs caught
Source: https://agentgate-source.pages.dev/kog-holder-snapshot/ (browsable tree, git bundle with full history, tarball https://agentgate-source.pages.dev/kog-holder-snapshot/kog-holder-snapshot.tar.gz). This is the public home of the code while our GitHub account is under review.
npx -y https://agentgate-source.pages.dev/kog-holder-snapshot/kog-holder-snapshot.tgz --mint <mint> --exclusions exclusions.json
or clone, npm ci, npm run build, then node dist/bin.js ....
kog-snapshot --mint <address> --exclusions <file> [--slot <n>] [--top <n>] [--out <file>] [--fixture <file>] [--save-fixture <file>]
| Option | Meaning |
|---|---|
--mint |
the token mint (base58), required |
--exclusions |
JSON file { "owners": [...], "token_accounts": [...] }, required. Entries may be plain addresses or { "address": "...", "label": "bonding curve" } |
--slot |
passed to the RPC as minContextSlot (with --fixture: the fixture slot must not be lower) |
--top |
how many holders to keep, default 100 |
--out |
write the snapshot JSON to a file instead of stdout |
--fixture |
read {slot, accounts} from a file, no network at all |
--save-fixture |
write the fetched {slot, accounts} to a file so the snapshot can be reproduced later |
The RPC URL comes from the environment variable SOLANA_RPC_URL and is never printed: error messages replace it with <SOLANA_RPC_URL>. Exit code 0 on success, 1 with a plain message on any error (missing mint, unreadable file, RPC error).
Reproduce a snapshot from a recorded fixture:
SOLANA_RPC_URL=https://your-rpc kog-snapshot --mint <mint> --exclusions ex.json --save-fixture fx.json --out snap.json
kog-snapshot --mint <mint> --exclusions ex.json --fixture fx.json --out again.json # same hash, no network
import { buildSnapshot, fetchAccounts, snapshot } from "kog-holder-snapshot";
const { slot, accounts } = await fetchAccounts({ rpcUrl: process.env.SOLANA_RPC_URL!, mint });
const snap = buildSnapshot({ mint, slot, accounts, exclusions: { owners: [], token_accounts: [] }, top: 100 });
// { mint, slot, total, holders: [{ rank, owner, amount }], hash }
buildSnapshot({mint, slot, accounts, exclusions, top?}): accounts are getProgramAccounts items in jsonParsed encoding from both token programs. Only items of type account, of the mint, with a positive amount count. Amounts are base units as decimal strings, summed per owner with BigInt. An excluded owner or token account is left out. Ranking is by amount descending, ties by owner ascending (plain string comparison, the same in every locale). top (default 100) keeps the first holders; total is the sum over all counted owners after exclusions.hash: sha256, lowercase hex, of the UTF-8 text with one line per kept holder, rank,owner,amount, joined by \n, no trailing newline.fetchAccounts({rpcUrl, mint, fetch?, minContextSlot?}): one getProgramAccounts call per token program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA and TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) with encoding: jsonParsed, withContext: true and a memcmp filter on the mint at offset 0. slot is the lowest context slot of the two answers. fetch can be injected for tests.snapshot({accounts, exclusions, top}): the seat interface, same result; the mint is taken from the accounts when it is not given.Token 2022 accounts with extensions (transfer fee amount, immutable owner, transfer hook) and frozen accounts count by their amount. Withheld transfer fees are not part of the owner's balance and are not counted.
npm ci
npm test # 16 offline tests named A1 to A7, no network
npm run test:live # SOLANA_LIVE=1: a real getProgramAccounts read on mainnet
Recorded fixtures (committed, real mainnet data, recorded 21 Oct 2026 from api.mainnet-beta.solana.com):
fixtures/token2022-full.json: every token account of the Token 2022 mint AfzuVQFSimXiJdp6F8kgZb6R45GU1muu2Uj3u3tZLLpu at slot 459792671 (143 accounts, 133 holders). The pinned hash in test A7 was computed by an independent Python count.fixtures/pyusd-token2022-sample.json: a labelled partial sample of PYUSD Token 2022 accounts (frozen accounts, transfer fee and transfer hook extensions, zero balances), used for A5.Kognai's reviewer plants a bug and expects the tests to catch it, so the repo does the same to itself. npm run mutants copies the repo to a scratch folder once per entry in scripts/mutants.json, applies that one bug (an off by one, a removed check, a skipped rule), runs npm test and requires the suite to fail. Last run: 9 of 9 caught, 0 survived.
No keys, no wallet, no writes: the tool only reads public account data. No secret is logged. The output is the same for the same input, so any citizen can check it.
agentgate-source: code mirror on Cloudflare Pages. All repos