Skip to main content

The use case

Two questions come up constantly in token research, risk scoring, and airdrop design:
  • How concentrated is this token? A handful of wallets controlling most of the supply is a governance and liquidity risk.
  • Is it getting more or less concentrated over time? Concentration trending up after an unlock or an airdrop is a red flag.
Two standard metrics answer this:
  • Gini coefficient: inequality across all holders. 0 means every holder owns an equal amount; 1 means a single wallet owns everything.
  • HHI (Herfindahl-Hirschman Index): the sum of squared ownership shares. It ranges from 1/N (perfectly even across N holders) to 1 (one holder owns everything), and it is dominated by the largest holders, so it is the sharper “whale detector” of the two.
Both metrics need the same raw inputs, for every holder of the token:
  • the holder’s balance,
  • the token’s total supply (to turn balances into ownership shares),
  • the holder count (N),
  • and, ideally, the ability to pull all of the above as of a past block so you can chart the trend.
This guide computes both metrics with the GoldRush Get token holders (v2) endpoint, then compares the approach to Etherscan’s token holder API.

The formulas

With holder balances bᵢ and shares sᵢ = bᵢ / Σ bⱼ:
  • HHI = Σ sᵢ²
  • Gini = Σᵢ (2i − N − 1) · b₍ᵢ₎ / (N · Σ b₍ⱼ₎), where b₍ᵢ₎ are balances sorted ascending and i runs from 1 to N.
Worked sanity check for three holders owning [20, 30, 50]: shares are [0.2, 0.3, 0.5], so HHI = 0.04 + 0.09 + 0.25 = 0.38 and Gini = 0.20.

What you need from an API

RequirementWhy it matters
Every holder’s balanceBoth metrics are sums over all holders, so a truncated list understates concentration.
Total supplyConverts raw balances into ownership shares.
Holder count (N)Sets the HHI floor (1/N) and normalizes the Gini.
Point-in-time holdersRequired to chart concentration over time, not just today.

Fetching holders and computing Gini & HHI with GoldRush

The GoldRush TypeScript SDK exposes getTokenHoldersV2ForTokenAddress as an async iterator that pages through all holders for you. Each item carries balance, total_supply, and contract_decimals inline, so you never need a second request to normalize the data.
import { GoldRushClient } from "@covalenthq/client-sdk";

const client = new GoldRushClient("<GOLDRUSH_API_KEY>");

// Pull every holder. The SDK auto-paginates via the async iterator;
// page-size 1000 (the max) keeps the number of round-trips low.
async function getAllBalances(chain: string, token: string) {
  const balances: number[] = [];
  let totalSupply = 0;
  let decimals = 0;

  for await (const page of client.BalanceService.getTokenHoldersV2ForTokenAddress(
    chain,
    token,
    { pageSize: 1000 },
  )) {
    if (page.error) throw new Error(page.error_message);
    for (const holder of page.data.items ?? []) {
      decimals = holder.contract_decimals ?? 0;
      totalSupply = Number(holder.total_supply);          // returned inline
      balances.push(Number(holder.balance) / 10 ** decimals);
    }
  }
  return { balances, totalSupply: totalSupply / 10 ** decimals };
}

function hhi(values: number[]): number {
  const sum = values.reduce((a, b) => a + b, 0);
  if (sum === 0) return 0;
  return values.reduce((acc, v) => acc + (v / sum) ** 2, 0);
}

function gini(values: number[]): number {
  const xs = [...values].sort((a, b) => a - b);
  const n = xs.length;
  const sum = xs.reduce((a, b) => a + b, 0);
  if (n === 0 || sum === 0) return 0;
  let weighted = 0;
  for (let i = 0; i < n; i++) weighted += (2 * (i + 1) - n - 1) * xs[i];
  return weighted / (n * sum);
}

const { balances, totalSupply } = await getAllBalances(
  "eth-mainnet",
  "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC
);

// Optional: share of total supply held by the top 10 holders.
const top10 =
  [...balances].sort((a, b) => b - a).slice(0, 10).reduce((a, b) => a + b, 0) /
  totalSupply;

console.log({
  holders: balances.length,
  gini: gini(balances).toFixed(4),
  hhi: hhi(balances).toFixed(6),
  top10ShareOfSupply: (top10 * 100).toFixed(2) + "%",
});
balance and total_supply are returned as raw strings in the token’s base units. The examples divide by 10 ^ contract_decimals for readability; for tokens whose supply exceeds JavaScript’s safe-integer range, sum the raw values with BigInt before taking ratios if you need exact precision.

The Etherscan approach

Etherscan exposes the equivalent data through tokenholderlist (module token, action tokenholderlist), paginated with page and offset:
curl "https://api.etherscan.io/api?module=token&action=tokenholderlist\
&contractaddress=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\
&page=1&offset=1000&apikey=<KEY>"
Each row returns only TokenHolderAddress and TokenHolderQuantity. To compute Gini or HHI you then have to:
  • fetch the total supply separately (the stats/tokensupply endpoint), since it is not in the holder response;
  • apply the token’s decimals separately (also not in the holder response);
  • accept that the list is current holders only, with no way to snapshot holders at a past block, so a concentration-over-time chart is not possible from this endpoint.
Note also that tokenholderlist and tokenholdercount are PRO endpoints, available only on Etherscan’s paid Standard plan and above.

Side-by-side

For a Gini/HHI pipelineGoldRush token_holders_v2Etherscan tokenholderlist
Total supply in the same responsetotal_supply on every item❌ separate tokensupply call
Decimals in the same responsecontract_decimals❌ separate lookup
Holder count (N)pagination.total_countSeparate tokenholdercount call
Point-in-time (historical) holdersblock-height / date❌ current only
Max page size10001000
Auto-pagination in the SDK✅ async iterator❌ manual page/offset loop
Multichain under one key✅ 100+ chainsPer-chain access
Access tierIncluded (0.02 credits/item)PRO (paid) endpoint

Where GoldRush pulls ahead

  1. One response has everything the math needs. balance, total_supply, and contract_decimals arrive together, so shares are a one-line calculation, with no second request and no separate supply/decimals bookkeeping.
  2. Concentration over time, not just a snapshot. block-height and date let you recompute Gini/HHI at any historical point on Foundational Chains and chart the trend across unlocks, airdrops, and listings.
  3. N comes for free. pagination.total_count gives the holder count in the same call, so the HHI floor (1/N) and Gini normalization need no extra request.
  4. Fewer round-trips. A page size of 1000 plus SDK auto-pagination enumerates every holder with minimal code.
  5. Same shape across 100+ chains. One API key and one code path cover Ethereum, Polygon, BSC, Base, Arbitrum, and more.

Where to start

Get token holders (v2)

Full parameter and response reference for the endpoint used above, including historical block-height and date queries.

Foundational API quickstart

Install the TypeScript SDK and make your first authenticated call in minutes.

Estimate your cost

Project the monthly cost of pulling full holder lists at 0.02 credits per item.

Supported chains

See which chains support the latest snapshot and which support historical holder queries.