# kog-holder-snapshot

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).

## Quick start (about one minute)

```sh
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.

## Install and run

```sh
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:

```sh
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
```

## Library

```ts
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.

## Tests

```sh
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.

## Planted bug check

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.

## Five Laws

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.
