Arkham API Portfolio Tracking: Rank Funds and Trading Firms by Weekly Balance Change

Rival firms do not publish their trades. But their wallets are attributed on Arkham, so their holdings are public. That lets you ask two questions about a whole group of firms at once. Whose portfolio actually moved this week, and which direction. And for the biggest movers, who were they trading with. The ranking here is by size of the move, not size of the portfolio, so a small fund that repositioned ranks above a large one that just sat still.
First check what categories you can rank. Fund, otc, mev, and individual are the ones that describe a trading firm rather than an exchange or a protocol.
import requests
resp = requests.get(
"https://api.arkm.com/intelligence/entity_types",
headers={"API-Key": "YOUR_API_KEY"},
)
types = resp.json()

Now rank the funds whose balance changed the most over the last week. Sort by balanceUsdChange so the ranking is on the move, not the portfolio size, and set a balanceMin floor so tiny portfolios do not crowd the results.
resp = requests.get(
"https://api.arkm.com/intelligence/entity_balance_changes",
params={
"entityTypes": "fund",
"interval": "7d",
"balanceMin": 50000000,
"orderBy": "balanceUsdChange",
"orderDir": "desc",
"limit": 20,
},
headers={"API-Key": "YOUR_API_KEY"},
)
gainers = resp.json()
Run the exact same call with orderDir=asc to get the portfolios that shrank the most. A rival selling is just as useful to know as a rival buying, and most people only check one direction.
The response includes a tokenBalances breakdown for each entity. This is the part that actually matters. Compare balanceUnit to prevBalanceUnit. If the token units did not change, only the price moved, and the firm did nothing at all that week despite showing a big dollar swing.
for entity in gainers:
for token in entity["tokenBalances"]:
if token["balanceUnit"] != token["prevBalanceUnit"]:
print(f"{entity['entityName']} actually traded {token['tokenSymbol']}")
else:
print(f"{entity['entityName']} balance moved on price only")
Now take one of the real movers and see who they traded with over the same window.
resp = requests.get(
"https://api.arkm.com/counterparties/entity/jump-trading",
params={"timeLast": "7d", "limit": 8},
headers={"API-Key": "YOUR_API_KEY"},
)
counterparties = resp.json()
Each row here is grouped by chain and flagged with a flow direction. A large row flowing out to a known exchange address confirms the sale actually landed somewhere real, rather than just moving to another wallet the same firm controls. This endpoint is rate limited to one call per second, so if you are tracing several movers in a loop, pace it out rather than firing all the requests at once.
Keep the interval the same across both calls, 7d for the ranking and 7d again for the counterparty check, so the trades you are looking at actually match the move you ranked in the first place. Run the whole thing weekly with the same settings and diff the results. A firm that flips from one end of the ranking to the other has reversed its position, and that is exactly the kind of thing you want to know before it shows up as a headline somewhere else.

Here is everything above stitched into one script you can actually run, top to bottom.
import requests
import time
API_KEY = "YOUR_API_KEY"
HEADERS = {"API-Key": API_KEY}
INTERVAL = "7d"
BALANCE_MIN = 50000000
LIMIT = 20
# Step 1: see what categories exist (informational, run once)
resp = requests.get(
"https://api.arkm.com/intelligence/entity_types",
headers=HEADERS,
)
resp.raise_for_status()
print(f"Entity types available: {resp.json()[:12]}...")
def get_balance_changes(order_dir):
resp = requests.get(
"https://api.arkm.com/intelligence/entity_balance_changes",
params={
"entityTypes": "fund",
"interval": INTERVAL,
"balanceMin": BALANCE_MIN,
"orderBy": "balanceUsdChange",
"orderDir": order_dir,
"limit": LIMIT,
},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()
# Step 2: rank the biggest gainers and biggest losers
gainers = get_balance_changes("desc")
losers = get_balance_changes("asc")
movers = gainers + losers
print(f"Pulled {len(gainers)} gainers and {len(losers)} losers")
# Step 3: separate real trades from price-only swings
real_movers = []
price_only = []
for entity in movers:
traded = False
for token in entity.get("tokenBalances", []):
if token["balanceUnit"] != token["prevBalanceUnit"]:
traded = True
break
(real_movers if traded else price_only).append(entity)
print(f"Real trades: {len(real_movers)}, price-only moves: {len(price_only)}")
# Step 4: for the real movers, check who they traded with
counterparty_map = {}
for entity in real_movers:
resp = requests.get(
f"https://api.arkm.com/counterparties/entity/{entity['entityId']}",
params={"timeLast": INTERVAL, "limit": 8},
headers=HEADERS,
)
if resp.status_code != 200:
print(f" skipped {entity['entityName']}: {resp.status_code}")
continue
counterparty_map[entity["entityId"]] = resp.json()
time.sleep(1.1) # this endpoint is rate limited to 1 call/second
# Step 5: print a ranked summary
print(f"
{'Entity':25} {'Balance Change':>18} Traded?")
print("-" * 60)
for entity in sorted(movers, key=lambda e: e["balanceUsd"] - e["prevBalanceUsd"], reverse=True):
change = entity["balanceUsd"] - entity["prevBalanceUsd"]
traded = entity in real_movers
print(f"{entity['entityName'][:25]:25} {change:>18,.2f} {'yes' if traded else 'price only'}")
https://info.arkm.com/research/api-use-cases-step-by-step-guides-for-on-chain-intelligence-workflows