9/26/20265 min read

Know Your Whales: Turn Raw Wallet Addresses Into a Ranked Customer List via Arkham API

Know Your Whales: Turn Raw Wallet Addresses Into a Ranked Customer List via Arkham API

Every on-chain product has the same blind spot. Your database knows a wallet moved $50,000 through you last month. It has no idea if that is a retail trader's entire net worth or a fund's rounding error. The Arkham API fixes that gap directly, and once you have it, your whole user base turns into something you can actually segment and act on instead of a list of addresses.

I am using GMX as the demo product here since it is public, well-known, and already fully attributed on Arkham. The same script works for your own protocol; just swap the entity ID for yours.

First pull everyone who moved value with GMX in the last 30 days [limited to 100 for the test].


import requests

resp = requests.get(
    "https://api.arkm.com/counterparties/entity/gmx",
    params={"timeLast": "30d", "limit": 100},
    headers={"API-Key": "YOUR_API_KEY"},
)
counterparties = resp.json()

This comes back grouped by chain, each row a wallet with a dollar amount moved and a transaction count over the window. Skip anything with contract: true, that is other protocols' code, not a user. Skip anything with a depositServiceID too, that is just an exchange's own deposit address sweeping funds, not a person. Also skip the null address, 0x0000000000000000000000000000000000000000, if it shows up. It is a technical burn address, not a wallet, and its balance can come back as a nonsense number that will sit at the top of your ranking and throw off everything below it.

Now name whatever is left. One call handles up to 1000 addresses at once.


NULL_ADDRESS = "0x0000000000000000000000000000000000000000"

addresses = [
    row["address"]["address"]
    for row in counterparties["arbitrum_one"]
    if not row["address"].get("contract")
    and row["address"]["address"].lower() != NULL_ADDRESS.lower()
]

resp = requests.post(
    "https://api.arkm.com/intelligence/address_enriched/batch",
    headers={"API-Key": "YOUR_API_KEY"},
    json={"addresses": addresses},
)
named = resp.json()

Some of these come back as confirmed funds with a name, a website, a Twitter handle. Some come back with nothing but a tag showing which other DEX they also trade on. Either way, you have gone from a wallet address to something a sales or marketing person can actually work with.

One thing worth setting expectations on before you run this at scale. Running it against a real protocol, roughly 80 wallets came back from the counterparty pull, and every one of them returned a JSON entry from the naming call, but only about 15 percent actually had a readable name attached, an ENS handle or a username on some other platform Arkham indexes. The rest came back with no entity and no label at all, just the raw address again. That is normal, not a broken call. Retail wallets mostly are not named unless the owner attached something to them. Measure your own coverage before you build a workflow that assumes every wallet gets a name.

The part that actually matters for segmentation is comparing volume to balance, not looking at volume by itself. Pull what each wallet currently holds.


resp = requests.get(
    f"https://api.arkm.com/balances/address/{address}",
    headers={"API-Key": "YOUR_API_KEY"},
)
balance = resp.json()["totalBalance"]
total_held = sum(balance.values())

A wallet that moved $150M through you while holding $3M is a high frequency trader. Fees and execution speed are what they will notice first, so that is your product improvement outreach list. A wallet that moved $10M while holding $70M is mostly parked capital that trades occasionally. That is a completely different pitch, more relationship management than a latency pitch.

If Arkham returned an arkhamEntity for a wallet, pull the whole firm's balance too, not just the one address you happened to see.


resp = requests.get(
    f"https://api.arkm.com/balances/entity/{entity_id}",
    headers={"API-Key": "YOUR_API_KEY"},
)
firm_total = resp.json()["totalBalance"]

That single wallet you profiled might be a quarter of what the firm actually holds on chain. Sizing the wallet tells you who to reach out. Sizing the firm tells you how big the account could actually become.

arkham api - use case - identify whales.jpg

Once you have volume, balance, and a name for a batch of wallets, drop it into a spreadsheet and sort by the ratio of the two. The top of that list is who you talk to first. The bottom is who you leave alone until they show up again on their own.

Same four calls work for any product with an Arkham entity ID, or any contract address if you are not registered as an entity yet. Just swap /entity/gmx for /address/{your_contract} and the rest of the script does not change.

Full script:


"""
Full script for "Know Your Whales" - Arkham API blog post.

Pulls GMX's counterparties (last 30 days), names them via batch
enrichment, then values each wallet by comparing volume moved to
current balance held.

Set your real Arkham API key below before running.
"""

import requests

API_KEY = "your-top-secret-arkham-api-key"  # <-- paste your real Arkham API key here
HEADERS = {"API-Key": API_KEY}


#%% Step 1: pull GMX's counterparties over the last 30 days

resp = requests.get(
    "https://api.arkm.com/counterparties/entity/gmx",
    params={"timeLast": "30d", "limit": 100},
    headers=HEADERS,
)
resp.raise_for_status()
counterparties = resp.json()

print("Chains returned:", list(counterparties.keys()))


#%% Step 2: pull out real wallets, skip contracts and the null address

NULL_ADDRESS = "0x0000000000000000000000000000000000000000"

addresses = []
for chain, rows in counterparties.items():
    for row in rows:
        addr_info = row["address"]
        addr = addr_info["address"]
        if not addr_info.get("contract") and addr.lower() != NULL_ADDRESS.lower():
            addresses.append(addr)

print(f"Found {len(addresses)} non-contract wallet addresses")

if not addresses:
    print("No wallets found - stop here and check Step 1's output before continuing.")


#%% Step 3: batch-name them (up to 1000 per call, costs 500 credits)

resp = requests.post(
    "https://api.arkm.com/intelligence/address_enriched/batch",
    headers=HEADERS,
    json={"addresses": addresses},
)
resp.raise_for_status()
named = resp.json()["addresses"]

print(f"Named {len(named)} addresses")


#%% Step 4: for each named wallet, pull its current balance

results = []

for address, info in named.items():
    entity = info.get("arkhamEntity")
    label = info.get("arkhamLabel")
    name = entity["name"] if entity else (label["name"] if label else address)

    bal_resp = requests.get(
        f"https://api.arkm.com/balances/address/{address}",
        headers=HEADERS,
    )
    if bal_resp.status_code != 200:
        print(f"  balance lookup failed for {address}: {bal_resp.status_code}")
        continue

    total_balance = bal_resp.json()["totalBalance"]
    total_held = sum(total_balance.values())

    results.append({
        "address": address,
        "name": name,
        "held": total_held,
    })

print(f"Valued {len(results)} wallets")


#%% Step 5: print a simple ranked table

print(f"{'Name':30} {'Address':44} {'Held (USD)':>15}")
print("-" * 92)
for r in sorted(results, key=lambda x: x["held"], reverse=True):
    print(f"{r['name'][:30]:30} {r['address']:44} {r['held']:>15,.2f}")


#%% Step 6: save to CSV

import csv

out_path = "gmx_whales.csv"
with open(out_path, "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "address", "held"])
    writer.writeheader()
    writer.writerows(sorted(results, key=lambda x: x["held"], reverse=True))

print(f"
Saved {len(results)} rows to {out_path}")

ShareXLinkedIn
A
Adil KhanGrowth Lab Insights