| 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 = [] |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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}%") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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), |
| } |
|
|
| |
|
|
| 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() |
|
|