NGMIT.FUN/ANALYSE
SCRIPT_LIBRARY
5 PYTHON SCRIPTS
๐
TOP HOLDERS
analyze_holders.py
Analyses a token's largest holders (liquidity pool excluded). For each one it rebuilds an average entry price from the wallet's swap history, then computes unrealized and realized PnL versus the current price. When no purchase can be found (airdrop, OTC transfer, etc.) it reports who sent the tokens instead. Results are printed as an aligned console table and exported to a formatted Excel file plus a CSV.
REQUIREMENTS
$ pip install requests pandas openpyxl
Needs a free Helius API key โ helius.dev
SOURCE (RAW)
raw / analyze_holders.py
"""
Top holders analysis for a Solana token.
For each of the largest holders (liquidity pool excluded) the script computes:
- average entry price
- gain / loss versus entry price (unrealized and realized)
- who sent the tokens if no purchase was found
Output:
- an aligned table in the console
- a formatted Excel file (holders_<start>-<end>.xlsx) + a CSV file
Requirements:
pip install requests pandas openpyxl
Free Helius API key: https://www.helius.dev (put it in HELIUS_API_KEY)
"""
import sys
import time
from datetime import datetime, timezone
import pandas as pd
import requests
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
# ----------------------------- CONFIG ---------------------------------
MINT = "" # <-- paste the token mint address here
HELIUS_API_KEY = "" # <-- paste your Helius API key here
# Holder range to analyse (rank 1 = biggest holder, liquidity pool excluded).
# Can be overridden from the command line: python analyze_holders.py 21 39
START_RANK = 1
END_RANK = 3
MAX_PAGES = 10 # 10 pages x 100 tx per wallet (increase for long histories)
MAX_ACCOUNT_PAGES = 200 # 200 pages x 1000 token accounts when ranking all holders
# Addresses to ignore (liquidity pool, etc.): owner wallet OR token account
EXCLUDE = {""} # <-- paste the liquidity pool address(es) to exclude here
# Output file names are built from the selected range (e.g. holders_21-39.xlsx)
# -----------------------------------------------------------------------
RPC_URL = f"https://mainnet.helius-rpc.com/?api-key={HELIUS_API_KEY}"
WSOL = "So11111111111111111111111111111111111111112"
STABLES = {
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", # USDC
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", # USDT
}
COLUMNS = [
"Rank", "Wallet", "Status", "Balance (tokens)", "Current value ($)",
"Avg entry price ($)", "Gain vs entry (%)", "Unrealized PnL ($)",
"Realized PnL ($)", "First received from (if no buy found)",
]
# ------------------------------ DATA ----------------------------------
def rpc(method, params):
r = requests.post(RPC_URL, json={"jsonrpc": "2.0", "id": 1,
"method": method, "params": params}, timeout=30)
r.raise_for_status()
data = r.json()
if "error" in data:
raise SystemExit(f"RPC error: {data['error']}")
return data["result"]
def get_market():
"""Current token price and SOL price in USD (DexScreener)."""
data = requests.get(f"https://api.dexscreener.com/latest/dex/tokens/{MINT}", timeout=30).json()
pairs = data.get("pairs") or []
if not pairs:
raise SystemExit("Token not found on DexScreener.")
pair = max(pairs, key=lambda p: (p.get("liquidity") or {}).get("usd", 0))
price_usd = float(pair["priceUsd"])
if pair["quoteToken"]["address"] == WSOL:
sol_usd = price_usd / float(pair["priceNative"])
else:
d = requests.get(f"https://api.dexscreener.com/latest/dex/tokens/{WSOL}", timeout=30).json()
sol_usd = float(max(d["pairs"], key=lambda p: (p.get("liquidity") or {}).get("usd", 0))["priceUsd"])
return pair["baseToken"].get("symbol", "?"), price_usd, sol_usd
def ranked_holders(start, end):
"""
Holders ranked by balance (liquidity pool excluded), for ranks start..end.
Uses the Helius DAS API to list ALL token accounts, so any rank can be reached
(the standard RPC only knows the 20 largest).
Returns a list of (rank, owner wallet, balance, token account).
"""
decimals = rpc("getTokenSupply", [MINT])["value"]["decimals"]
print("Fetching all token accounts (this can take a moment)...")
balances, biggest_account = {}, {}
cursor = None
for page in range(MAX_ACCOUNT_PAGES):
params = {"mint": MINT, "limit": 1000}
if cursor:
params["cursor"] = cursor
res = rpc("getTokenAccounts", params)
batch = res.get("token_accounts", [])
for acc in batch:
if acc["owner"] in EXCLUDE or acc["address"] in EXCLUDE:
continue
amount = int(acc["amount"])
balances[acc["owner"]] = balances.get(acc["owner"], 0) + amount
# keep the largest token account of each owner (used to find the sender)
if amount >= biggest_account.get(acc["owner"], (0, None))[0]:
biggest_account[acc["owner"]] = (amount, acc["address"])
print(f" page {page + 1}: {len(balances)} holders so far")
cursor = res.get("cursor")
if not batch or not cursor:
break
ranking = sorted(balances.items(), key=lambda x: x[1], reverse=True)
if start > len(ranking):
raise SystemExit(f"Only {len(ranking)} holders found: rank {start} does not exist.")
selected = []
for rank in range(start, min(end, len(ranking)) + 1):
owner, raw = ranking[rank - 1]
selected.append((rank, owner, raw / 10 ** decimals, biggest_account[owner][1]))
return selected
def fetch_transactions(address, only_swaps=False):
"""Transaction history of an address (Helius enhanced transactions API)."""
txs, before = [], None
for _ in range(MAX_PAGES):
params = {"api-key": HELIUS_API_KEY, "limit": 100}
if only_swaps:
params["type"] = "SWAP"
if before:
params["before"] = before
r = requests.get(f"https://api.helius.xyz/v0/addresses/{address}/transactions",
params=params, timeout=60)
if r.status_code != 200:
break
page = r.json()
if not page:
break
txs += page
before = page[-1]["signature"]
time.sleep(0.2)
return txs
def find_sender(token_account, wallet):
"""Address that sent the token to the wallet (first transfer received)."""
for tx in reversed(fetch_transactions(token_account)): # oldest first
for t in tx.get("tokenTransfers", []):
if t.get("mint") == MINT and t.get("toUserAccount") == wallet and t.get("fromUserAccount"):
return t["fromUserAccount"]
return None
def raw_amount(t):
r = t["rawTokenAmount"]
return int(r["tokenAmount"]) / 10 ** r["decimals"]
def analyse_wallet(wallet, sol_usd):
"""Returns (tokens bought, cost $, tokens sold, proceeds $)."""
bought = cost = sold = proceeds = 0.0
def usd_side(native, tokens):
"""USD value of the other side of the swap (native SOL, WSOL or stablecoin)."""
v = 0.0
if native and int(native["amount"]) > 0:
v += int(native["amount"]) / 1e9 * sol_usd
for t in tokens:
if t["mint"] == WSOL:
v += raw_amount(t) * sol_usd
elif t["mint"] in STABLES:
v += raw_amount(t)
return v
for tx in fetch_transactions(wallet, only_swaps=True):
swap = (tx.get("events") or {}).get("swap")
if not swap:
continue
tok_in = [t for t in swap.get("tokenInputs", []) if t["mint"] == MINT and t["userAccount"] == wallet]
tok_out = [t for t in swap.get("tokenOutputs", []) if t["mint"] == MINT and t["userAccount"] == wallet]
if tok_out: # BUY: wallet receives the token and pays in SOL/stablecoin
qty = sum(raw_amount(t) for t in tok_out)
paid = usd_side(swap.get("nativeInput"), swap.get("tokenInputs", []))
if paid > 0:
bought += qty
cost += paid
elif tok_in: # SELL: wallet gives the token and receives SOL/stablecoin
qty = sum(raw_amount(t) for t in tok_in)
got = usd_side(swap.get("nativeOutput"), swap.get("tokenOutputs", []))
if got > 0:
sold += qty
proceeds += got
return bought, cost, sold, proceeds
# ----------------------------- DISPLAY --------------------------------
def fmt(value, kind):
"""Formats a value for the console table."""
if value is None or pd.isna(value):
return "-"
if kind == "int":
return f"{int(value)}"
if kind == "num":
return f"{value:,.2f}"
if kind == "price":
return f"{value:.8f}"
if kind == "signed":
return f"{value:+,.2f}"
return str(value)
def print_table(df):
"""Prints an aligned table: text left-aligned, numbers right-aligned."""
kinds = ["int", "text", "text", "num", "num", "price", "signed", "signed", "signed", "text"]
table = [[fmt(v, k) for v, k in zip(row, kinds)] for row in df.itertuples(index=False)]
widths = [max(len(h), *(len(r[i]) for r in table)) for i, h in enumerate(COLUMNS)]
left = {1, 2, 9}
def line(cells):
return " | ".join(c.ljust(w) if i in left else c.rjust(w)
for i, (c, w) in enumerate(zip(cells, widths)))
sep = "-+-".join("-" * w for w in widths)
print(line(COLUMNS))
print(sep)
for r in table:
print(line(r))
print(sep)
def export_excel(df, symbol, price, sol_usd, path):
"""Writes a formatted Excel file with a data sheet and an info sheet."""
with pd.ExcelWriter(path, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Top holders")
ws = writer.sheets["Top holders"]
header_fill = PatternFill("solid", fgColor="1F3864")
thin = Side(style="thin", color="BFBFBF")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
widths = [7, 46, 22, 20, 18, 20, 17, 19, 17, 46]
formats = [None, None, None, "#,##0.00", "#,##0.00", "0.00000000",
"+0.0;-0.0;0.0", "+#,##0.00;-#,##0.00;0.00", "+#,##0.00;-#,##0.00;0.00", None]
for i, w in enumerate(widths, 1):
ws.column_dimensions[ws.cell(1, i).column_letter].width = w
for c in ws[1]:
c.font = Font(bold=True, color="FFFFFF")
c.fill = header_fill
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
c.border = border
ws.row_dimensions[1].height = 32
for row in ws.iter_rows(min_row=2):
for i, c in enumerate(row):
c.border = border
if formats[i]:
c.number_format = formats[i]
c.alignment = Alignment(horizontal="right")
if i in (6, 7, 8) and isinstance(c.value, (int, float)): # gain / PnL columns
c.font = Font(color="00B050" if c.value > 0 else "C00000" if c.value < 0 else "000000")
ws.freeze_panes = "C2"
ws.auto_filter.ref = ws.dimensions
info = pd.DataFrame({
"Item": ["Token", "Mint address", "Current price ($)", "SOL price ($)", "Generated (UTC)", "Notes"],
"Value": [symbol, MINT, f"{price:.8f}", f"{sol_usd:.2f}",
datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M"),
"Entry price = weighted average of detected swaps. SOL converted to $ at the "
"current SOL price. Purchases made through unrecognised aggregators may be missing."],
})
info.to_excel(writer, index=False, sheet_name="Info")
wi = writer.sheets["Info"]
wi.column_dimensions["A"].width = 20
wi.column_dimensions["B"].width = 90
for c in wi[1]:
c.font = Font(bold=True, color="FFFFFF")
c.fill = header_fill
# ------------------------------- MAIN ----------------------------------
def main():
if not HELIUS_API_KEY:
raise SystemExit("Please set HELIUS_API_KEY at the top of the script.")
start, end = START_RANK, END_RANK
if len(sys.argv) == 3: # python analyze_holders.py 21 39
start, end = int(sys.argv[1]), int(sys.argv[2])
elif len(sys.argv) == 1: # no arguments: ask the user
ans = input(f"Holder range 'start end' [{start} {end}]: ").split()
if len(ans) == 2:
start, end = int(ans[0]), int(ans[1])
if start < 1 or end < start:
raise SystemExit("Invalid range: use e.g. 2 20 or 21 39")
symbol, price, sol_usd = get_market()
print(f"Token: {symbol} | current price: {price:.8f} $ | SOL: {sol_usd:.2f} $")
print(f"Analysing holders ranked {start} to {end} (liquidity pool excluded)\n")
holders = ranked_holders(start, end)
rows = []
for n, (rank, wallet, balance, token_account) in enumerate(holders, 1):
print(f"[{n}/{len(holders)}] rank {rank}: analysing {wallet} ...")
bought, cost, sold, proceeds = analyse_wallet(wallet, sol_usd)
entry = cost / bought if bought else None
sender = None if bought else find_sender(token_account, wallet)
if entry:
unrealized = balance * (price - entry)
realized = proceeds - sold * entry
gain_pct = (price / entry - 1) * 100
else:
unrealized = realized = gain_pct = None
rows.append([
rank, wallet, "Bought" if bought else "No buy found", round(balance, 2),
round(balance * price, 2), entry,
None if gain_pct is None else round(gain_pct, 1),
None if unrealized is None else round(unrealized, 2),
None if realized is None else round(realized, 2),
sender,
])
df = pd.DataFrame(rows, columns=COLUMNS)
print("\n")
print_table(df)
base = f"holders_{start}-{end}"
df.to_csv(f"{base}.csv", index=False)
export_excel(df, symbol, price, sol_usd, f"{base}.xlsx")
print(f"\nExported: {base}.xlsx and {base}.csv")
if __name__ == "__main__":
main()
NGMIT.FUN ยทSCRIPTS PROVIDED AS-IS ยทPASTE YOUR OWN MINT / WALLET / API KEY BEFORE RUNNING