Skip to main content

Smart-money co-buys

A small, self-contained example that ties the REST and WebSocket APIs together into something useful: it watches the tracked wallets with the biggest average buys and flags any token that 2 or more of them buy within 15 minutes — an early sign of smart money converging on a mint.

Everything runs in memory (no database). The result is written to a co_buys.json file that updates live as clusters form.

What you'll use

  • REST GET /wallets to rank tracked wallets by average buy size.
  • WebSocket /ws to stream those wallets' trades in real time.

Prerequisites

pip install websockets certifi

You also need an API key with some credits (see Authentication). certifi is only there so TLS verification works on machines with a stale certificate store (common on Windows); on most systems the standard library is enough.

Set up the shared bits — your key, the endpoints, and a TLS context:

import asyncio, json, random, ssl, time, urllib.request
from pathlib import Path
import websockets

API_KEY = "<your-api-key>"
API_BASE = "https://api.prysmatic-sol.xyz"
WS_URL = "wss://api.prysmatic-sol.xyz/ws"

WINDOW_SECONDS = 15 * 60 # buys must land within this span to count as a cluster
MIN_WALLETS = 2 # how many distinct wallets make it a co-buy
MIN_AVG_BUY_SOL = 2.0 # only watch wallets whose average buy is at least this
MAX_WATCH = 100 # the WebSocket filter accepts up to 100 wallets

try:
import certifi
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
except ImportError:
SSL_CONTEXT = ssl.create_default_context()

Step 1 — Pick the biggest buyers

Page through GET /wallets, read each wallet's behavior.avg_buy_amount, and keep the ones that buy big. The response tells you when to stop with has_more.

def get(path: str) -> dict:
req = urllib.request.Request(
f"{API_BASE}{path}", headers={"Authorization": f"Bearer {API_KEY}"})
with urllib.request.urlopen(req, timeout=30, context=SSL_CONTEXT) as resp:
return json.loads(resp.read())

def top_buyers() -> list[str]:
wallets = []
page = 1
while True:
data = get(f"/wallets?page={page}")
for item in data["items"]:
alias = item["identity"]["wallet"]
avg_buy = item.get("behavior", {}).get("avg_buy_amount") or 0.0
wallets.append((alias, float(avg_buy)))
if not data.get("has_more"):
break
page += 1

wallets.sort(key=lambda w: w[1], reverse=True)
picked = [a for a, avg in wallets if avg >= MIN_AVG_BUY_SOL][:MAX_WATCH]
print(f"Watching {len(picked)} wallets")
return picked

Step 2 — Detect co-buys in a sliding window

For each mint, keep the buys from the last 15 minutes. When two or more distinct wallets show up in that window, you have a cluster.

class CoBuyTracker:
def __init__(self, watched: list[str]):
self.watched = watched
self.buys: dict[str, list[dict]] = {} # mint -> [{wallet, block_time}]
self.clusters: dict[str, dict] = {} # mint -> cluster record

def add_buy(self, mint: str, wallet: str, block_time: int) -> bool:
events = self.buys.setdefault(mint, [])
cutoff = block_time - WINDOW_SECONDS
events[:] = [e for e in events if e["block_time"] >= cutoff] # drop old
if not any(e["wallet"] == wallet for e in events):
events.append({"wallet": wallet, "block_time": block_time})

distinct = {e["wallet"] for e in events}
if len(distinct) < MIN_WALLETS:
return False
self.clusters[mint] = {
"mint": mint,
"wallets": sorted(distinct),
"first_buy": min(e["block_time"] for e in events),
"last_buy": max(e["block_time"] for e in events),
"buys": sorted(events, key=lambda e: e["block_time"]),
}
return True

def dump(self) -> None:
Path("co_buys.json").write_text(json.dumps({
"generated_at": int(time.time()),
"window_minutes": WINDOW_SECONDS // 60,
"watched_wallets": self.watched,
"co_buys": list(self.clusters.values()),
}, indent=2))

Step 3 — Stream the live feed

Open the WebSocket, subscribe to trades filtered to your watchlist, and feed every buy into the tracker. Note the small details that make it production-shaped:

  • Ignore the {"type": "ping"} heartbeat and stop on balance_exhausted.
  • Reconnect with exponential backoff and jitter instead of a tight loop.
  • Re-subscribe on every reconnect — subscriptions do not survive a dropped connection.
async def stream(watched: list[str], tracker: CoBuyTracker):
delay = 1.0
while True:
try:
async with websockets.connect(
WS_URL,
extra_headers={"Authorization": f"Bearer {API_KEY}"},
ssl=SSL_CONTEXT,
) as ws:
await ws.send(json.dumps({
"action": "subscribe", "channels": ["trades"], "wallets": watched,
}))
delay = 1.0
async for raw in ws:
msg = json.loads(raw)
if msg.get("type") == "ping":
continue
if msg.get("type") == "balance_exhausted":
return
if msg.get("channel") != "trades":
continue
t = msg["data"]
if t.get("side") != "buy":
continue
if tracker.add_buy(t["mint"], t["wallet"], int(t["block_time"])):
c = tracker.clusters[t["mint"]]
print(f"CO-BUY {c['mint']} by {c['wallets']}")
tracker.dump()
except websockets.ConnectionClosed:
pass
await asyncio.sleep(delay + random.uniform(0, delay)) # backoff + jitter
delay = min(delay * 2, 30)

Step 4 — Run it

async def main():
watched = top_buyers()
tracker = CoBuyTracker(watched)
tracker.dump()
await stream(watched, tracker)

if __name__ == "__main__":
asyncio.run(main())

Save the snippets above as co_buys.py and run:

python co_buys.py

You'll see the watchlist print, then live CO-BUY lines as clusters form, and a co_buys.json that looks like this:

{
"generated_at": 1781450000,
"window_minutes": 15,
"watched_wallets": ["W145", "W342", "W57"],
"co_buys": [
{
"mint": "4Sm4YsjRrvSRQ4x1P86ncqumV3ZVgXxtFMKjiL8vpump",
"wallets": ["W57", "W145"],
"first_buy": 1781449880,
"last_buy": 1781449990,
"buys": [
{ "wallet": "W145", "block_time": 1781449880 },
{ "wallet": "W57", "block_time": 1781449990 }
]
}
]
}
Credits

Each delivered payload costs 1 credit, but the wallets filter means you only pay for the wallets you actually watch. Co-buys depend on real on-chain activity, so leave it running for a while.

Ideas to extend it

  • Tighten the window or raise MIN_WALLETS to surface only stronger signals.
  • Weight clusters by the wallets' score (from the Wallets API).
  • Push clusters to a webhook or a Telegram bot instead of a JSON file.