224 lines
8.7 KiB
Python
224 lines
8.7 KiB
Python
import asyncio
|
|
import os
|
|
from datetime import datetime
|
|
|
|
import httpx
|
|
|
|
BIRDEYE_API_KEY = os.getenv("BIRDEYE_API_KEY", "")
|
|
BASE_URL = "https://public-api.birdeye.so"
|
|
HEADERS = {"X-API-KEY": BIRDEYE_API_KEY, "accept": "application/json"}
|
|
|
|
|
|
class BirdeyeClient:
|
|
def __init__(self):
|
|
self.headers = HEADERS
|
|
self.client = httpx.AsyncClient(timeout=30.0)
|
|
self.last_call = 0
|
|
|
|
async def _call(self, endpoint: str, params: dict | None = None) -> dict:
|
|
import time
|
|
|
|
now = time.time()
|
|
wait = 0.6 - (now - self.last_call)
|
|
if wait > 0:
|
|
await asyncio.sleep(wait)
|
|
self.last_call = time.time()
|
|
try:
|
|
r = await self.client.get(f"{BASE_URL}{endpoint}", headers=self.headers, params=params or {})
|
|
return r.json() if r.status_code == 200 else {"error": f"HTTP {r.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
async def get_price(self, address: str) -> dict:
|
|
return await self._call("/defi/price", {"address": address})
|
|
|
|
async def get_token_overview(self, address: str) -> dict:
|
|
return await self._call("/defi/token_overview", {"address": address})
|
|
|
|
async def get_new_listings(self, limit: int = 20) -> list:
|
|
r = await self._call("/defi/v2/tokens/new_listing", {"limit": limit, "offset": 0})
|
|
return r.get("data", {}).get("items", []) if isinstance(r, dict) else []
|
|
|
|
async def security_scan(self, address: str) -> dict:
|
|
"""Derived security analysis using ALL Birdeye market data"""
|
|
overview = await self.get_token_overview(address)
|
|
await asyncio.sleep(0.6)
|
|
|
|
d = overview.get("data", {}) if isinstance(overview, dict) else {}
|
|
|
|
if not d:
|
|
return {"address": address, "error": "No data", "risk_score": -1}
|
|
|
|
score = 0
|
|
flags = []
|
|
signals = []
|
|
|
|
# 1. LIQUIDITY HEALTH (0-25 pts)
|
|
mcap = d.get("marketCap", 0) or 0
|
|
liq = d.get("liquidity", 0) or 0
|
|
if mcap > 0 and liq > 0:
|
|
ratio = liq / mcap
|
|
if ratio < 0.05:
|
|
score += 25
|
|
flags.append("CRITICAL: Liquidity/MCap < 5% — easy manipulation")
|
|
elif ratio < 0.15:
|
|
score += 15
|
|
flags.append("WARNING: Low liquidity ratio")
|
|
elif ratio > 0.5:
|
|
signals.append("Strong liquidity backing")
|
|
else:
|
|
signals.append("Normal liquidity levels")
|
|
|
|
# 2. PRICE VOLATILITY (0-20 pts)
|
|
changes = [abs(d.get(f"priceChange{t}Percent") or 0) for t in ["1m", "5m", "30m"]]
|
|
avg_chg = sum(changes) / max(len(changes), 1)
|
|
if avg_chg > 20:
|
|
score += 20
|
|
flags.append("EXTREME volatility — pump/dump in progress")
|
|
elif avg_chg > 5:
|
|
score += 10
|
|
flags.append("High volatility — watch for manipulation")
|
|
elif avg_chg < 1:
|
|
signals.append("Stable price action")
|
|
|
|
# 3. HOLDER HEALTH (0-20 pts)
|
|
holders = d.get("holder", 0) or 0
|
|
if holders < 20:
|
|
score += 20
|
|
flags.append(f"Very few holders ({holders}) — high concentration")
|
|
elif holders < 100:
|
|
score += 10
|
|
flags.append(f"Low holder count ({holders})")
|
|
elif holders > 500:
|
|
signals.append(f"Healthy holder base ({holders:,}) wallets")
|
|
|
|
# Check wallet change for suspicious activity
|
|
uw_change = d.get("uniqueWallet30mChangePercent", 0) or 0
|
|
if uw_change > 50:
|
|
flags.append(f"Suspicious +{uw_change:.0f}% wallet growth in 30m — possible bots")
|
|
elif uw_change > 20:
|
|
flags.append(f"Rapid wallet growth +{uw_change:.0f}%")
|
|
|
|
# 4. TRADE ACTIVITY (0-15 pts)
|
|
last_trade = d.get("lastTradeUnixTime", 0) or 0
|
|
if last_trade > 0:
|
|
mins = (datetime.utcnow().timestamp() - last_trade) / 60
|
|
if mins > 60:
|
|
score += 15
|
|
flags.append(f"No trades for {int(mins)} min — possible dead token")
|
|
elif mins > 30:
|
|
score += 5
|
|
flags.append(f"Low activity — last trade {int(mins)} min ago")
|
|
else:
|
|
signals.append("Active trading")
|
|
|
|
# 5. METADATA QUALITY (0-10 pts)
|
|
ext = d.get("extensions", {})
|
|
has_web = bool(ext.get("website"))
|
|
has_social = bool(ext.get("twitter") or ext.get("discord"))
|
|
has_desc = bool(ext.get("description"))
|
|
if not has_web and not has_social:
|
|
score += 10
|
|
flags.append("No website or socials — anonymous project")
|
|
elif not has_web:
|
|
score += 5
|
|
flags.append("No website — transparency concern")
|
|
elif has_desc:
|
|
signals.append("Complete metadata — transparent project")
|
|
|
|
# 6. VOLUME/MARKET CAP RATIO (0-10 pts) — wash trading detection
|
|
v24h = d.get("v24hUSD", 0) or 0
|
|
if mcap > 0 and v24h > 0:
|
|
v_ratio = v24h / mcap
|
|
if v_ratio > 5:
|
|
score += 10
|
|
flags.append(f"Volume {v_ratio:.1f}x MarketCap — WASH TRADING likely")
|
|
elif v_ratio > 2:
|
|
score += 5
|
|
flags.append(f"Volume {v_ratio:.1f}x MarketCap — possible wash trading")
|
|
elif v_ratio > 0.1:
|
|
signals.append("Healthy volume/market cap ratio")
|
|
|
|
# 7. BUY/SELL RATIO ANALYSIS (bonus signal)
|
|
buy24h = d.get("buy24h", 0) or 0
|
|
sell24h = d.get("sell24h", 0) or 0
|
|
if buy24h > 0 and sell24h > 0:
|
|
if sell24h > buy24h * 2:
|
|
flags.append("Heavy sell pressure — 2x more sells than buys")
|
|
elif buy24h > sell24h * 1.5:
|
|
signals.append("Buy pressure dominant — bullish signal")
|
|
|
|
# VERDICT
|
|
if score >= 60:
|
|
verdict = "HIGH RISK"
|
|
elif score >= 35:
|
|
verdict = "MEDIUM RISK"
|
|
elif score >= 15:
|
|
verdict = "LOW-MEDIUM RISK"
|
|
else:
|
|
verdict = "LOW RISK"
|
|
|
|
return {
|
|
"address": address,
|
|
"token_name": d.get("name", "Unknown"),
|
|
"symbol": d.get("symbol", "???"),
|
|
"risk_score": min(score, 100),
|
|
"risk_level": verdict,
|
|
"price": d.get("price", 0),
|
|
"market_cap": mcap,
|
|
"liquidity": liq,
|
|
"fdv": d.get("fdv", 0),
|
|
"holders": holders,
|
|
"number_markets": d.get("numberMarkets", 0),
|
|
"volume_24h": v24h,
|
|
"buy_24h": buy24h,
|
|
"sell_24h": sell24h,
|
|
"price_change_24h": d.get("priceChange24hPercent", 0),
|
|
"wallet_growth_30m": uw_change,
|
|
"last_trade": d.get("lastTradeHumanTime", ""),
|
|
"flags": flags,
|
|
"positive_signals": signals,
|
|
"metadata": {"website": has_web, "socials": has_social, "description": has_desc},
|
|
"analyzed_at": datetime.utcnow().isoformat(),
|
|
"birdeye_powered": True,
|
|
}
|
|
|
|
async def new_token_radar(self, limit: int = 20, min_liquidity: float = 1000) -> dict:
|
|
tokens = await self.get_new_listings(limit)
|
|
scored = []
|
|
for t in tokens:
|
|
liq = t.get("liquidity", 0) or 0
|
|
if liq < min_liquidity:
|
|
continue
|
|
score = 0
|
|
reasons = []
|
|
if liq > 10000:
|
|
score += 25
|
|
reasons.append("Good liquidity")
|
|
uw = t.get("uniqueWallet30m", 0) or 0
|
|
if uw > 50:
|
|
score += min(uw * 0.2, 20)
|
|
reasons.append(f"{uw} recent wallets")
|
|
score += min(t.get("trade24h", 0) or 0 * 0.01, 10)
|
|
scored.append({**t, "opportunity_score": min(score, 50), "score_reasons": reasons})
|
|
return {
|
|
"tokens": sorted(scored, key=lambda x: x.get("opportunity_score", 0), reverse=True),
|
|
"count": len(scored),
|
|
}
|
|
|
|
# ── Wallet Intelligence ──────────────────────────────────────────────────
|
|
|
|
async def get_wallet_networth(self, wallet: str) -> dict:
|
|
"""Get wallet net worth in USD and token breakdown."""
|
|
return await self._call("/v1/wallet/networth", {"wallet": wallet})
|
|
|
|
async def get_wallet_pnl(self, wallet: str, timeframe: str = "7d") -> dict:
|
|
"""Get wallet profit/loss for a given timeframe."""
|
|
return await self._call("/v1/wallet/pnl", {"wallet": wallet, "time_frame": timeframe})
|
|
|
|
async def get_wallet_smart_money_status(self, wallet: str) -> dict:
|
|
"""Check if wallet is tagged as smart money."""
|
|
return await self._call("/v1/wallet/smart_money", {"wallet": wallet})
|
|
|
|
async def close(self):
|
|
await self.client.aclose()
|