Is there any other method to retrieve SPL token holders?
I'm currently using the Helius API, but it's giving me an "exceeded limit" error.
To preface, I am an employee at Helius.
The DAS API only returns up to 1000 records at a time. If you need to retrieve more than 1000 items, then you'll need to paginate your records. This is achieved by making multiple API calls and crawling through "pages" of data. We support both page-based and keyset pagination. You can read more about that here. For most tokens, you can expect to have more than 1000 holders.
The Get Token Accounts
method is useful for retrieving information about all token accounts for a specific mint or owner. You can use pagination with this method to fetch all the holders of a given SPL token. We have the following example from our docs, which uses page-based pagination to get all the holders of $BONK and writes their pubkey to a new file:
const url = `https://mainnet.helius-rpc.com/?api-key=<API-KEY>`;
const fs = require("fs");
const getTokenAccounts = async () => {
const fetch = (await import("node-fetch")).default;
let page = 1;
let allOwners = new Set();
while (true) {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "getTokenAccounts",
id: "helius-test",
params: {
page: page,
limit: 1000,
displayOptions: {},
mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
},
}),
});
const data = await response.json();
if (!data.result || data.result.token_accounts.length === 0) {
console.log(`No more results. Total pages: ${page - 1}`);
break;
}
console.log(`Processing results from page ${page}`);
data.result.token_accounts.forEach((account) =>
allOwners.add(account.owner)
);
page++;
}
fs.writeFileSync(
"output.json",
JSON.stringify(Array.from(allOwners), null, 2)
);
};
getTokenAccounts();
If you have any additional questions, feel free to ask away here or in the Helius Discord!
ok for those of you don't want to use Helius, this is how I got it.
const { Connection, PublicKey } = require('@solana/web3.js');
const { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint } = require('@solana/spl-token');
const fs = require('fs');
const path = require('path');
const apiKey = // NODE API KEY HERE e.g. https://<YADAYADA>.solana-mainnet.quicknode.pro/<YADAYADA>
const tokenContracts = [{ address: "ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx", ticker: "ATLAS", tokenName: "AtlasToken", decimals: 8 }];
// Initialize Solana connection
const solanaConnection = new Connection(apiKey);
async function getAllTokenHoldersForContract({ address, ticker, tokenName, decimals }) {
try {
const mintPublicKey = new PublicKey(address);
// Fetch the total supply of the token
const mintAccountInfo = await solanaConnection.getParsedAccountInfo(mintPublicKey);
const totalSupply = mintAccountInfo.value.data.parsed.info.supply;
const minBalanceThreshold = 0;
// Fetch the current slot (block number)
const currentSlot = await solanaConnection.getSlot();
const programId = TOKEN_PROGRAM_ID; // change this to TOKEN_2022_PROGRAM_ID if needed
// Fetch all token accounts for the given mint
const tokenAccounts = await solanaConnection.getParsedProgramAccounts(
programId,
{
filters: [
{
dataSize: 165, // Size of the token account data
},
{
memcmp: {
offset: 0, // Start of the mint address in the account data
bytes: mintPublicKey.toBase58(), // The mint address to filter by
},
},
],
}
);
if (tokenAccounts.length === 0) {
console.log(`No token holders found for contract ${tokenName} (${ticker}).`);
return;
}
// Filter out accounts with a non-zero balance greater than the threshold
const holders = tokenAccounts
.map((account) => {
const accountInfo = account.account.data.parsed.info;
return {
holderAddress: accountInfo.owner,
amountHeld: accountInfo.tokenAmount.uiAmount,
};
})
.filter((holder) => holder.amountHeld > minBalanceThreshold); // Filter out accounts below the threshold
// Prepare CSV data
const csvData = [
["Holder Address", "Amount Held", "Total Supply", "Contract Address", "Ticker", "Token Name", "Slot"],
...holders.map(holder => [
holder.holderAddress,
holder.amountHeld,
totalSupply / Math.pow(10, decimals),
address,
ticker,
tokenName,
currentSlot,
]),
];
// Convert array to CSV format
const csvContent = csvData.map(row => row.join(",")).join("\n");
// Save the CSV file with the token name and slot number
const filePath = path.join(__dirname, `csv/${tokenName}_slot${currentSlot}.csv`);
fs.writeFileSync(filePath, csvContent);
console.log(`CSV file saved as ${filePath}`);
} catch (error) {
console.error(`Error fetching token holders and balances for ${tokenName} (${ticker}):`, error);
}
}
async function processTokenContracts() {
for (const contract of tokenContracts) {
await getAllTokenHoldersForContract(contract);
}
}
// Run the function to process all token contracts
processTokenContracts();