Solana Holder Analysis with getTokenAccountsCount Developer Guide
In this guide, we will learn how to use getTokenAccountsCount and getTokenLargestAccounts to make a simple "holder analysis" frontend. The frontend will display the total number of holders, as well as the 20 biggest ones. This is a useful thing to do in many contexts, like trading terminals where users would want to know how healthy a token's distribution is.
Reference documentation: getTokenAccountsCount
Before We Begin
Before reading this guide, it helps to know how token accounts are laid out, and how the 20 largest holders of a mint are fetched. Our getTokenLargestAccounts developer guide covers both, and getProgramAccounts explains the expensive way of doing what we are about to do cheaply.
Our free API keys are sufficient for this guide. You can get one by logging in to FluxRPC, navigating to the API Keys page, and selecting "Create API Key". There is no need to copy-paste it: navigate back to this page while logged in and we will use the key you select in every example below.
In this guide, we will accomplish the following:
- Count the holders of a token without running an indexer
- Separate the accounts that still hold a balance from the empty ones
- Fetch the 20 largest holders
- Build a Holder Analysis panel for a trading dashboard, in plain JavaScript
We will provide examples in JavaScript in this guide.
Why Count Holders?
In this case we'll lead with an example. If you enter a mint and an API key below, we'll display the type of display we'll be working toward.
The interactive Holder Analysis panel needs JavaScript. It takes a token mint address and reports the total number of holders, how many of those hold a balance above zero, and the 20 largest token accounts. It runs the same three RPC calls this guide builds by hand below.
A trading UI, token screener, or trading bot might need this data! However, getting the number of accounts that hold a token is a pain. There were two choices:
- Build an indexer yourself, and update it with all new token account creation and deletion (a lot of infrastructure for a simple app!).
- Run getProgramAccounts with the mint as a memcmp filter on the Token program, and count the accounts that come back (expensive and slow!).
So to help you out, FluxRPC maintains that index, so you can request the number you need with a simple & cheap RPC call.
curl "{RPC_URL}" -s -X POST \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsCount",
"params": [
"FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX"
]
}'
result is just a number, the count of token accounts that exist for that mint:
{
"jsonrpc": "2.0",
"id": 1,
"result": 24310
}
getTokenAccountsCount is a FluxRPC custom RPC method. It uses around 43 bytes of bandwidth instead of the many megabytes a getProgramAccounts-based solution can use.
In the example above, we are counting ALL token accounts for a token, including accounts with zero balance. That's often undesirable though, so we provide a method to filter those out.
Leaving Out the Zero-Balance Accounts
The count above includes every token account that currently exists for the mint, and many of those may hold no tokens! Selling, transferring or burning all your tokens does not close the token account. It stays at a zero balance until you close it to reclaim the rent.
So the raw count answers "how many token accounts exist for this mint?", which is a different question from "how many accounts hold tokens right now?". The excludeZero config flag lets you easily answer either question:
curl "{RPC_URL}" -s -X POST \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsCount",
"params": [
"FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX",
{ "excludeZero": true }
]
}'
Same mint as before, but this time the empty accounts are left out:
{
"jsonrpc": "2.0",
"id": 1,
"result": 16579
}
Most of the time, you'll want to set excludeZero to true, because usually you'll be interested in the number of people actually holding a token. However, it's still useful to know both numbers in many contexts.
The 20 Largest Holders
getTokenAccountsCount tells you how wide a token's distribution is, not how concentrated it is. For that we want the biggest holders, which is what getTokenLargestAccounts returns:
curl "{RPC_URL}" -s -X POST \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenLargestAccounts",
"params": [
"FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX"
]
}'
value holds 20 entries, each with the token account address, the raw amount in lamports, the mint decimals, and the same amount formatted as uiAmount and uiAmountString:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {
"slot": 448020718
},
"value": [
{
"address": "9ewheAbsXjj1F73oeKKQ3KvxH97E13i5kcBx1w5onzTC",
"amount": "9974379487807",
"decimals": 5,
"uiAmount": 99743794.87807,
"uiAmountString": "99743794.87807"
}
]
}
}
Remember that these are token accounts, not wallets! If you want the owning wallet, you can run getMultipleAccounts on them and extract it from the response (bytes 32-63).
Our getTokenLargestAccounts guide shows how to read those owners with getMultipleAccounts and a dataSlice, or how to get the same list with owners already attached from a RugCheck report.
Now we have all the data needed to build our holder display app!
Create the Two Files
Make a new folder anywhere, and create two empty files inside it with your editor.
holder-analysis/
├── index.html
└── holders.js
Put this in index.html. The IDs match up with the JS code.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FluxRPC Holder Analysis</title>
</head>
<body>
<main>
<h1>FluxRPC Holder Analysis</h1>
<input id="mint-input" type="text" size="50" placeholder="Token mint address" />
<button id="analyze-button" type="button">Analyze</button>
<p id="status-line" role="status"></p>
<table>
<tbody>
<tr>
<th scope="row">Total holders</th>
<td id="total-holders">-</td>
</tr>
<tr>
<th scope="row">Holders > 0 balance</th>
<td id="nonzero-holders">-</td>
</tr>
<tr>
<th scope="row">Empty accounts</th>
<td id="empty-accounts">-</td>
</tr>
</tbody>
</table>
<h2>Top 20 holders</h2>
<table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Token account</th>
<th scope="col">Balance</th>
<th scope="col">Share of top 20</th>
</tr>
</thead>
<tbody id="top-holders-body"></tbody>
</table>
<script src="./holders.js" defer></script>
</main>
</body>
</html>
holders.js contains all the JavaScript logic. The sections after this one build it up one part at a time and explain each. If you would rather copy the entire file once, it's the first code block below:
Add the Shield Key
Start holders.js with your Shield key and the Shield host. If you're signed in, those values should already be filled in, otherwise replace the placeholders by hand. We're using shielded API keys here, because this avoids revealing your real API key in the frontend. They have a per-IP rate limit.
Our getBalance guide covers Shield keys in more detail.
The rest is just adding the element IDs from the HTML.
const FLUXRPC_SHIELD_KEY = "{{SHIELD_KEY}}";
const FLUXRPC_SHIELD_RPC_URL = "{{SHIELD_RPC_URL}}";
const mintInput = document.getElementById("mint-input");
const analyzeButton = document.getElementById("analyze-button");
const statusLine = document.getElementById("status-line");
const totalHoldersCell = document.getElementById("total-holders");
const nonZeroHoldersCell = document.getElementById("nonzero-holders");
const emptyAccountsCell = document.getElementById("empty-accounts");
const topHoldersBody = document.getElementById("top-holders-body");
Send One JSON-RPC Request
All three of our calls are just HTTPS requests with a different method name and parameter list, so they get one function. It posts JSON-RPC to the Shield endpoint, raises anything the node reports as an error, and hands back the result field.
It's not safe to skip the error check! An RPC failure, such as a malformed mint or an expired key, arrives with an HTTP 200 and an error object in the body. If you only check response.ok, you'll read the response as successful and just fail later in the code.
// One JSON-RPC request. The three calls this app makes differ only in method and params.
async function rpc(method, params) {
const response = await fetch(
FLUXRPC_SHIELD_RPC_URL + "?key=" + encodeURIComponent(FLUXRPC_SHIELD_KEY),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
},
);
const payload = await response.json();
// JSON-RPC reports failures in the body, not in the HTTP status code.
if (payload.error) {
throw new Error(payload.error.message || payload.error);
}
return payload.result;
}
Make the Three Calls at Once
Now the three HTTPS calls. They are completely independent, so they go out together under Promise.all and the UI waits for the slowest one rather than the sum of all three.
emptyAccounts is just the difference between the two counts, which is the number of token accounts sitting at a zero balance.
async function analyzeMint(mint) {
// Three independent reads, so send them together instead of awaiting one at a time.
const [totalHolders, nonZeroHolders, largest] = await Promise.all([
rpc("getTokenAccountsCount", [mint]),
rpc("getTokenAccountsCount", [mint, { excludeZero: true }]),
rpc("getTokenLargestAccounts", [mint]),
]);
return {
totalHolders,
nonZeroHolders,
emptyAccounts: totalHolders - nonZeroHolders,
topHolders: largest.value,
};
}
Render the Summary
The three headline numbers. toLocaleString adds the thousands separators, which matters for a count that often runs to five or six digits.
function renderSummary(analysis) {
totalHoldersCell.textContent = analysis.totalHolders.toLocaleString();
nonZeroHoldersCell.textContent = analysis.nonZeroHolders.toLocaleString();
emptyAccountsCell.textContent = analysis.emptyAccounts.toLocaleString();
}
Render the Top 20
Each row in the UI gets the rank, the token account, its formatted balance, and its % share of the total balance of the top 20 holders.
For a share of the token's total supply instead, getTokenSupply is the method you'll need to use to calculate that.
The sum is taken in BigInt. Token amounts are integers of the mint's base units, and a supply of a billion tokens with 9 decimals is 10^18 base units, past the point where a JS number stays exact.
function renderTopHolders(topHolders) {
// Balances are lamports, so they are summed as BigInt: a mint
// with 9 decimals and a large supply passes what a JS number holds exactly.
const topTotal = topHolders.reduce((sum, holder) => sum + BigInt(holder.amount), 0n);
topHoldersBody.replaceChildren();
topHolders.forEach((holder, index) => {
const share = topTotal === 0n ? 0 : Number((BigInt(holder.amount) * 10000n) / topTotal) / 100;
const row = document.createElement("tr");
[String(index + 1), holder.address, holder.uiAmountString, share.toFixed(2) + "%"].forEach(
(value) => {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
},
);
topHoldersBody.append(row);
});
}
Wire the Button
Finally, we connect the button. It reports what it is doing, disables the button while the requests are in flight (so a second click cannot fire another three RPC calls), and prints the error message if anything bad happens.
analyzeButton.addEventListener("click", async () => {
const mint = mintInput.value.trim();
if (!mint) {
statusLine.textContent = "Enter a token mint address first.";
return;
}
statusLine.textContent = "Reading holders through FluxRPC...";
analyzeButton.disabled = true;
try {
const analysis = await analyzeMint(mint);
renderSummary(analysis);
renderTopHolders(analysis.topHolders);
statusLine.textContent = "Done - 3 RPC calls.";
} catch (error) {
statusLine.textContent = error.message;
} finally {
analyzeButton.disabled = false;
}
});
Run It on localhost
Run one of these inside the folder holding your two files to launch a temporary web server:
# if you have Python
python3 -m http.server 8000
# the same command, on Windows
py -m http.server 8000
# if you have Node.js instead
npx serve . --listen 8000
Then open http://localhost:8000 in your browser, paste a mint address into the box, and press Analyze. The summary table fills in with the three counts, and the table below it lists the 20 largest token accounts with their balances.
If you prefer not to use a terminal
In VS Code, install the Live Server extension, then right-click
index.htmland choose Open with Live Server. It serves the folder onlocalhostand opens the browser for you.
Tips & Tricks
- Use the Shield key in browser code, never the account API key, and treat it as public: a key shipped to browsers is readable by definition.
- A holder count usually changes more slowly than a price does, so it is worth caching in a dashboard, although the RPC call is very cheap to make.
- If you need the wallets behind the top 20 rather than their token accounts, add the getMultipleAccounts call with a dataSlice over bytes 32-63, or read them straight out of a RugCheck report as our getTokenLargestAccounts guide shows.
Next Steps
A holder panel is just one feature on a trading dashboard. getPriorityFeeEstimate is what you'll need if you want to start placing trades rather than just watching them!