Find who is buying a token, and who they are
Screen Ethereum tokens for smart-money inflow, take the top token's biggest buyers and label them in one batch call, flagging the ones that are not exchanges, protocols or bots.
Three routes chained: /tokens/screener ranks tokens by smart-money
inflow over 7 days; /tokens/{token_address}/transfers
with tab=bought returns that token's top buyers; /labels/batch says
who each buyer is.
export VIKE_API_KEY=...
python oc-early-buyers.py # real run: needs the key
python oc-early-buyers.py --dry-run # prints the requests it would make, calls nothingEach buyer prints with its USD spent, the tokens it kept (net_tokens) and its label. A buyer that kept less than
1% of what it received is marked "passed it on"; otherwise it is marked "look here" when it is unlabelled or its
wallet_type is not cex, defi or mev. That rule is the recipe's, not the API's: an exchange or a lending
protocol is usually plumbing rather than a decision to buy.
"""Who is buying the token smart money is moving into? Screen, take the top token, label its biggest buyers.
Run: VIKE_API_KEY=... python oc-early-buyers.py (--dry-run lists the requests and makes none)
"""
import os
import sys
from urllib.parse import urlencode
NAME = "oc-early-buyers"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
# Wallet types that are intermediaries, not an owner deciding to buy; everything else is worth a look.
PLUMBING = {"cex", "defi", "mev"}
def fail(why):
sys.exit(f"{NAME}: {why}")
def call(path, method="GET", **params):
url = BASE + path + ("?" + urlencode(params) if params else "")
if DRY:
print(method, url)
return None
key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
import requests
r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
if r.status_code != 200:
fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
return r.json()
def main():
# Ethereum by default. A token is identified by address only: the screener's symbol is empty.
screen = call("/v1/onchain/tokens/screener", sort="smart_inflow", window="7d", limit=5)
# --dry-run has no response to read
rows = [{"token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "volume_usd": 0}] if DRY else screen
# The zero address is the chain's native coin (ETH), which often tops the screen; /transfers lists no buyers for it.
rows = [r for r in rows or [] if int(r["token_address"], 16)]
if not rows:
fail("the screener returned no token contracts")
# Ten rows, one side per call, and no limit parameter. usd_spent and net_usd are plain USD.
for row in rows:
token = row["token_address"]
buyers = call(f"/v1/onchain/tokens/{token}/transfers", period="7d", tab="bought")
if DRY or buyers:
break
print(f"{token} has no buyers in the window, trying the next token")
else:
fail(f"none of the top {len(rows)} screened tokens has buyers in the window")
wallets = ["0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb", "0x06cff7088619c7178f5e14f0b119458d08d2f5ef"]
if not DRY:
print(f"top smart-money inflow over 7d: {token} (${row['volume_usd']:,.0f} volume)")
wallets = [b["address"] for b in buyers]
# A POST with no body: the addresses ride in the query string, at most 100 per call.
labels = call("/v1/onchain/labels/batch", method="POST", addresses=",".join(wallets))
if DRY:
return
if len(labels) != len(wallets):
fail(f"/labels/batch returned {len(labels)} rows for {len(wallets)} wallets")
by_wallet = {row["wallet"]: row for row in labels}
for b in buyers:
row = by_wallet.get(b["address"].lower(), {})
kind = (row.get("wallet_type") or "") if row.get("found") else ""
who = f"{row['entity']} ({kind})" if kind else "unlabelled"
# bought counts every transfer in, so a flash-loan loop or a router "buys" billions and keeps nothing
passed_on = b["net_tokens"] < 0.01 * b["bought"]
flag = " (passed it on)" if passed_on else "" if kind in PLUMBING else " <- look here"
print(f" {b['address']} bought ${b['usd_spent']:,.0f} kept {b['net_tokens']:,.0f} {who}{flag}")
if __name__ == "__main__":
try:
main()
except Exception as e: # a shape the recipe does not expect fails by name, not by traceback
fail(f"{type(e).__name__}: {e}")Read it this way
- Tokens are addresses. The screener's
symbolis empty andnameisnull, so the script carries thetoken_addressthrough. Resolve a name with/tokens/searchif you want one. - The native coin can top the screen. ETH ranks as the zero address
0x0000…0000, and the transfers route lists no buyers for it, so the script skips it and walks down the screen to the first token that has buyers. - A top buyer can be a loop, not a buyer.
boughtcounts every transfer a wallet received. On 2026-10-18 the first token past ETH was USDT, and its top buyer was a contract that a bot had cycled about $40M of flash-loaned USDT through 137 times per transaction. A wallet that keeps what it receives has a largenet_tokens; one that only passes it on hasbought≈sold. - The transfers route returns exactly ten rows and takes no
limit, one side per call.usd_spentandboughthave been identical on every captured row, so do not treatusd_spentas independently priced. /labels/batchis a POST with no body. The addresses ride in the query string, at most 100 per call, and a row for an address it does not know comes back withfound: falsein its input position.- Ethereum only here. The screener and the transfers route default to
ethereum, and/labels/batchreads Ethereum labels whateverchainsays.
wallet_type describes behaviour, not ownership
vitalik.eth is typed dex on /labels/batch because of where its
transfers land. The "look here" filter is a first pass, not a verdict: read
/labels/{address}/provenance for a buyer you care about,
as in Whose wallet is this.
Whose wallet is this
Put a name to an Ethereum address with its label, the sources behind that label and its ENS name, in three calls.
Trace where an exchange wallet's money goes
Follow a wallet's money two hops out at three counterparties per hop, then list the heaviest edges with their labels and direction.