""" Token Supply & Liquidity Analyzer ================================== Comprehensive analysis of a token's supply mechanics, liquidity locks, holder concentration, mint/burn capabilities, and hidden tax mechanisms. Signals detected: - Circulating vs total supply ratio (dilution risk) - Mint functions (unlimited mint, capped mint, mint authorities) - Burn mechanisms (deflationary, manual burn, burn fee) - Top holder concentration (whale dominance, insider allocation) - Liquidity lock status (locked, unlocked, removed, partial lock) - Hidden buy/sell taxes (dynamic tax, blacklist-dependent tax) - Fake renounced ownership (admin key still active, proxy pattern) - Liquidity pool health (LP token concentration, pair age) - Multi-sig and timelock risks Tier: Premium ($0.08-0.12) Endpoint: POST /api/v1/x402-tools/token_supply_analyze """ import asyncio import json import logging import re import time from dataclasses import dataclass, field from datetime import UTC, datetime from enum import Enum from typing import Any, cast from urllib.parse import urlparse logger = logging.getLogger("token_supply_analyzer") # ── Free API sources ───────────────────────────────────────────── DEXSCREENER_TOKENS = "https://api.dexscreener.com/latest/dex/tokens/{token_addr}" DEXSCREENER_SEARCH = "https://api.dexscreener.com/latest/dex/search?q={}" BIRDEYE_TOKEN = "https://public-api.birdeye.so/defi/v3/token/overview?address={token_addr}" BIRDEYE_HOLDERS = "https://public-api.birdeye.so/defi/v3/token/holder?address={token_addr}" SOLSCAN_TOKEN = "https://api.solscan.io/token/meta?token={token_addr}" SOLSCAN_HOLDERS = "https://api.solscan.io/token/holders?token={token_addr}&limit=10" ETHERSCAN_TOKEN = ( "https://api.etherscan.io/api?module=account&action=tokeninfo&contractaddress={token_addr}" ) BSCSCAN_TOKEN = ( "https://api.bscscan.com/api?module=account&action=tokeninfo&contractaddress={token_addr}" ) # Rate limiting _RATE_LIMITERS: dict[str, float] = {} _RATE_LIMIT_WINDOW = 0.5 # 500ms between requests to same host # URL safety regex _URL_SAFE = re.compile( r"^https?://[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*(?::\d{1,5})?(?:/[^\s\"<>]*)?$" ) # ── Constants ──────────────────────────────────────────────────── KNOWN_BURN_ADDRESSES = { "0x0000000000000000000000000000000000000000", "0x000000000000000000000000000000000000dead", "0x000000000000000000000000000000000000dEaD", "dead00000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000001", } MAX_HOLDER_SAMPLE = 50 # Max top holders to analyze CONCENTRATION_THRESHOLD_PCT = 90.0 # Top 10 holders > this = concentrated SUPPLY_RISK_RATIO = 0.3 # Circ/total ratio below this = high dilution risk class SupplyRisk(Enum): SAFE = "safe" LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class LockStatus(Enum): LOCKED = "locked" PARTIALLY_LOCKED = "partially_locked" UNLOCKED = "unlocked" REMOVED = "removed" UNKNOWN = "unknown" @dataclass class TokenSupplyProfile: """Complete supply and liquidity profile for a token.""" # Token basics address: str chain: str = "" symbol: str = "" name: str = "" decimals: int = 18 # Supply data total_supply_raw: int = 0 circulating_supply_raw: int = 0 max_supply_raw: int = 0 total_supply: float = 0.0 circulating_supply: float = 0.0 max_supply: float = 0.0 burned_supply: float = 0.0 # Supply mechanics has_mint_function: bool = False mint_function_type: str = "none" # none, unlimited, capped, ownable mint_authority: str = "" has_burn_function: bool = False burn_function_type: str = "none" # none, manual, automated, fee is_deflationary: bool = False # Holder concentration top_10_holder_pct: float = 0.0 top_50_holder_pct: float = 0.0 deployer_hold_pct: float = 0.0 holder_count: int = 0 whale_holdings: list[dict] = field(default_factory=list) # Liquidity analysis lock_status: LockStatus = LockStatus.UNKNOWN liquidity_lock_address: str = "" liquidity_lock_expiry: str = "" lp_token_pct_held_by_deployer: float = 0.0 pair_age_days: float = 0.0 pair_liquidity_usd: float = 0.0 # Tax analysis buy_tax_pct: float = 0.0 sell_tax_pct: float = 0.0 has_dynamic_tax: bool = False has_blacklist: bool = False has_max_wallet_limit: bool = False # Ownership is_renounced: bool = False owner_address: str = "" has_proxy_admin: bool = False has_multisig: bool = False has_timelock: bool = False # Scoring supply_risk: SupplyRisk = SupplyRisk.SAFE supply_risk_score: float = 0.0 concentration_risk: SupplyRisk = SupplyRisk.SAFE concentration_score: float = 0.0 liquidity_risk: SupplyRisk = SupplyRisk.SAFE liquidity_score: float = 0.0 overall_risk_score: float = 0.0 overall_risk: SupplyRisk = SupplyRisk.SAFE # Raw data patterns_detected: list[str] = field(default_factory=list) recommendations: list[str] = field(default_factory=list) # ── Utility functions ──────────────────────────────────────────── def _validate_url(url: str) -> bool: """Basic URL validation to prevent SSRF.""" return bool(_URL_SAFE.match(url)) async def _rate_limit_host(host: str) -> None: """Simple per-host rate limiter.""" now = time.time() last = _RATE_LIMITERS.get(host, 0.0) wait = _RATE_LIMIT_WINDOW - (now - last) if wait > 0: await asyncio.sleep(wait) _RATE_LIMITERS[host] = time.time() def _detect_chain(address: str) -> str: """Detect chain from address format.""" addr = address.strip() if addr.startswith("0x"): return "ethereum" # Could also be BSC, Polygon, Arbitrum, etc. elif len(addr) >= 32 and not addr.startswith("0x"): return "solana" return "unknown" def _normalize_address(address: str) -> str: """Normalize address to lowercase for comparison.""" return address.lower().strip() def _classify_risk(score: float) -> SupplyRisk: """Convert numeric score to risk level.""" if score <= 10: return SupplyRisk.SAFE elif score <= 25: return SupplyRisk.LOW elif score <= 50: return SupplyRisk.MEDIUM elif score <= 75: return SupplyRisk.HIGH else: return SupplyRisk.CRITICAL # ── Supply Analysis ────────────────────────────────────────────── def _analyze_supply_mechanics(profile: TokenSupplyProfile) -> None: """ Analyze token supply data for dilution risk and manipulation patterns. """ score = 0.0 total = profile.total_supply circ = profile.circulating_supply max_sup = profile.max_supply burned = profile.burned_supply # ── Circulating vs total supply ratio ────────────────────── if total > 0 and circ > 0: ratio = circ / total profile.patterns_detected.append(f"circ_supply_ratio:{ratio:.2%}") if ratio < SUPPLY_RISK_RATIO: score += 35 profile.patterns_detected.append( "high_dilution_risk:circ_supply_less_than_30%_of_total" ) profile.recommendations.append( f"⚠ Only {ratio:.0%} of total supply is circulating - high dilution risk. " "Check vesting/unlock schedule." ) elif ratio < 0.5: score += 20 profile.patterns_detected.append( "moderate_dilution_risk:circ_supply_less_than_50%_of_total" ) profile.recommendations.append( f"⚠ ~{ratio:.0%} of supply is circulating. Verify lockup/vesting terms." ) elif total > 0 and circ == 0: score += 15 profile.patterns_detected.append("no_circulating_supply_data") # ── Max supply vs total supply (capped vs uncapped) ──────── if max_sup > 0: if total > max_sup: score += 40 profile.patterns_detected.append( f"supply_exceeds_max:total_{total:.2f}_exceeds_max_{max_sup:.2f}" ) profile.recommendations.append( "🚨 Total supply exceeds max supply - possible mint/printing." ) else: remaining = max_sup - total remaining_pct = (remaining / max_sup) * 100 if remaining_pct > 50: score += 25 profile.patterns_detected.append( f"large_mint_headroom:{remaining_pct:.0f}%_of_max_supply_unminted" ) profile.recommendations.append( f"🚨 {remaining_pct:.0f}% of max supply is unminted - significant dilution risk." ) else: # No max supply = potentially unlimited mint if profile.has_mint_function: score += 30 profile.patterns_detected.append("no_max_supply_with_mint:unlimited_mint_possible") profile.recommendations.append( "🚨 No max supply set AND mint function exists - potentially unlimited mint." ) # ── Burn amount ───────────────────────────────────────────── if burned > 0 and total > 0: burn_pct = (burned / total) * 100 profile.patterns_detected.append(f"burned_supply:{burn_pct:.1f}%") if burn_pct > 50: score -= 15 # Heavy burning is generally positive profile.recommendations.append( f"✅ {burn_pct:.0f}% of supply has been burned - strong deflationary pressure." ) # ── Mint function analysis ────────────────────────────────── if profile.has_mint_function: if profile.mint_function_type == "unlimited": score += 40 profile.patterns_detected.append("unlimited_mint_function") profile.recommendations.append( "🚨 Token has an unrestricted mint function - supply can be inflated at will." ) elif profile.mint_function_type == "ownable": score += 25 profile.patterns_detected.append("ownable_mint_function:owner_can_mint") if not profile.is_renounced: score += 10 profile.recommendations.append( "⚠ Owner-controlled mint function - supply can be increased if ownership is not renounced." ) elif profile.mint_function_type == "capped": score += 5 profile.patterns_detected.append("capped_mint_function") # ── Deflationary analysis ─────────────────────────────────── if profile.is_deflationary: score -= 10 profile.patterns_detected.append("deflationary_token") profile.recommendations.append( "✅ Deflationary mechanism active (auto-burn on transactions)." ) profile.supply_risk_score = max(0.0, min(100.0, score)) profile.supply_risk = _classify_risk(profile.supply_risk_score) def _analyze_concentration(profile: TokenSupplyProfile) -> None: """ Analyze holder concentration for whale dominance and insider risk. """ score = 0.0 top10 = profile.top_10_holder_pct top50 = profile.top_50_holder_pct deployer_hold = profile.deployer_hold_pct # ── Top 10 holder concentration ───────────────────────────── if top10 > 0: profile.patterns_detected.append(f"top_10_holder_pct:{top10:.1f}%") if top10 >= 99.0: score += 50 profile.patterns_detected.append("extreme_concentration:top_10_holds_>99%") profile.recommendations.append( "🚨 Top 10 wallets hold >99% of supply - extremely concentrated, likely a scam." ) elif top10 >= CONCENTRATION_THRESHOLD_PCT: score += 40 profile.patterns_detected.append("critical_concentration:top_10_holds_>90%") profile.recommendations.append( f"🚨 Top 10 wallets hold {top10:.0f}% of supply - extreme concentration risk." ) elif top10 >= 70: score += 25 profile.patterns_detected.append("high_concentration:top_10_holds_>70%") profile.recommendations.append( f"⚠ Top 10 wallets hold {top10:.0f}% of supply - high concentration." ) elif top10 >= 50: score += 15 profile.patterns_detected.append("moderate_concentration:top_10_holds_>50%") profile.recommendations.append( f"⚠ Top 10 wallets hold {top10:.0f}% of supply - moderate concentration." ) # ── Top 50 holder concentration ───────────────────────────── if top50 > 0: if top50 >= 99.0: score += 25 profile.patterns_detected.append("extreme_concentration_top50") elif top50 >= 95: score += 15 profile.patterns_detected.append("critical_concentration_top50") elif top50 >= 80: score += 10 profile.patterns_detected.append("high_concentration_top50") # ── Deployer holding ──────────────────────────────────────── if deployer_hold > 0: profile.patterns_detected.append(f"deployer_hold_pct:{deployer_hold:.1f}%") if deployer_hold > 50: score += 40 profile.patterns_detected.append("deployer_holds_majority") profile.recommendations.append( f"🚨 Deployer holds {deployer_hold:.0f}% of supply - complete control, very high risk." ) elif deployer_hold > 20: score += 20 profile.patterns_detected.append("deployer_holds_significant") profile.recommendations.append( f"⚠ Deployer holds {deployer_hold:.0f}% of supply - significant insider allocation." ) elif deployer_hold > 5: score += 10 profile.patterns_detected.append("deployer_holds_moderate") profile.recommendations.append( f"(i) Deployer holds {deployer_hold:.0f}% of supply - moderate allocation." ) # ── Whale holdings analysis ───────────────────────────────── for w in profile.whale_holdings: if w.get("pct", 0) > 5: score += 5 profile.patterns_detected.append( f"whale_gt_5%:address_{w.get('address', '')[:8]}..._{w.get('pct', 0):.1f}%" ) profile.concentration_score = max(0.0, min(100.0, score)) profile.concentration_risk = _classify_risk(profile.concentration_score) def _analyze_liquidity(profile: TokenSupplyProfile) -> None: """ Analyze liquidity pool health and lock status. """ score = 0.0 # ── Lock status ───────────────────────────────────────────── if profile.lock_status == LockStatus.REMOVED: score += 50 profile.patterns_detected.append("liquidity_removed") profile.recommendations.append( "🚨 Liquidity has been removed from the pool - rug pull risk." ) elif profile.lock_status == LockStatus.UNLOCKED: score += 30 profile.patterns_detected.append("liquidity_unlocked") profile.recommendations.append( "🚨 LP tokens are unlocked - deployer can remove liquidity at any time." ) elif profile.lock_status == LockStatus.PARTIALLY_LOCKED: score += 15 profile.patterns_detected.append("liquidity_partially_locked") profile.recommendations.append( "⚠ Only part of the liquidity is locked. Verify the locked portion." ) elif profile.lock_status == LockStatus.LOCKED: score -= 15 profile.patterns_detected.append("liquidity_locked") if profile.liquidity_lock_expiry: profile.recommendations.append( f"✅ Liquidity locked until {profile.liquidity_lock_expiry}. Good practice." ) else: profile.recommendations.append("✅ Liquidity is locked.") # ── LP token concentration ────────────────────────────────── if profile.lp_token_pct_held_by_deployer > 50: score += 25 profile.patterns_detected.append("deployer_holds_lp_majority") profile.recommendations.append("🚨 Deployer holds >50% of LP tokens - can drain pool.") elif profile.lp_token_pct_held_by_deployer > 10: score += 10 profile.patterns_detected.append("deployer_holds_significant_lp") profile.recommendations.append( f"⚠ Deployer holds {profile.lp_token_pct_held_by_deployer:.0f}% of LP tokens." ) # ── Pair age ──────────────────────────────────────────────── if profile.pair_age_days > 0: if profile.pair_age_days < 1: score += 20 profile.patterns_detected.append("very_new_pair:less_than_1_day_old") profile.recommendations.append( "⚠ Liquidity pair is less than 1 day old - very high risk." ) elif profile.pair_age_days < 7: score += 10 profile.patterns_detected.append(f"new_pair:{profile.pair_age_days:.0f}_days_old") profile.recommendations.append( f"⚠ Pair is only {profile.pair_age_days:.0f} days old - higher risk." ) elif profile.pair_age_days > 30: score -= 10 profile.patterns_detected.append("established_pair:>30_days_old") # ── Liquidity depth ───────────────────────────────────────── if profile.pair_liquidity_usd > 0: if profile.pair_liquidity_usd < 1000: score += 25 profile.patterns_detected.append("very_low_liquidity:<$1k") profile.recommendations.append( "🚨 Very low liquidity (<$1K) - high slippage and manipulation risk." ) elif profile.pair_liquidity_usd < 10000: score += 15 profile.patterns_detected.append("low_liquidity:<$10k") profile.recommendations.append("⚠ Low liquidity (<$10K) - moderate slippage risk.") elif profile.pair_liquidity_usd > 100000: score -= 10 profile.patterns_detected.append("healthy_liquidity:>$100k") profile.liquidity_score = max(0.0, min(100.0, score)) profile.liquidity_risk = _classify_risk(profile.liquidity_score) def _compute_overall_risk(profile: TokenSupplyProfile) -> None: """Compute overall risk score from all sub-scores.""" weights = { "supply": 0.35, "concentration": 0.30, "liquidity": 0.20, "tax": 0.10, "ownership": 0.05, } tax_score = 0.0 if profile.has_dynamic_tax: tax_score += 30 profile.patterns_detected.append("dynamic_tax:tax_can_change") profile.recommendations.append( "⚠ Dynamic tax detected - buy/sell tax can change at any time." ) if profile.has_blacklist: tax_score += 25 profile.patterns_detected.append("blacklist_function") profile.recommendations.append( "⚠ Token has a blacklist function - holders can be blocked from selling." ) if profile.buy_tax_pct > 10: tax_score += 15 profile.patterns_detected.append(f"high_buy_tax:{profile.buy_tax_pct:.1f}%") if profile.sell_tax_pct > 15: tax_score += 20 profile.patterns_detected.append(f"very_high_sell_tax:{profile.sell_tax_pct:.1f}%") profile.recommendations.append( f"🚨 Sell tax of {profile.sell_tax_pct:.0f}% is very high - may be a honeypot." ) ownership_score = 0.0 if not profile.is_renounced: ownership_score += 15 profile.patterns_detected.append("ownership_not_renounced") profile.recommendations.append("⚠ Ownership is NOT renounced - contract can be modified.") if profile.has_proxy_admin: ownership_score += 20 profile.patterns_detected.append("proxy_admin:contract_can_be_upgraded") profile.recommendations.append( "🚨 Token uses a proxy pattern with admin - contract can be upgraded (rug risk)." ) if profile.has_multisig: ownership_score -= 10 profile.patterns_detected.append("multisig_owner") profile.recommendations.append("✅ Ownership uses multi-sig - adds security layer.") profile.overall_risk_score = ( profile.supply_risk_score * weights["supply"] + profile.concentration_score * weights["concentration"] + profile.liquidity_score * weights["liquidity"] + tax_score * weights["tax"] + ownership_score * weights["ownership"] ) profile.overall_risk = _classify_risk(profile.overall_risk_score) # ── Main analysis function ─────────────────────────────────────── async def analyze_token_supply(token_address: str, chain: str = "") -> TokenSupplyProfile: """ Main entry point. Analyzes token supply, liquidity, and holder concentration. Args: token_address: The token contract address. chain: Optional chain hint (ethereum, solana, bsc, polygon, etc.). Returns: TokenSupplyProfile with all analysis results. """ profile = TokenSupplyProfile(address=token_address) if not chain: chain = _detect_chain(token_address) profile.chain = chain logger.info( "Analyzing token supply for %s on %s", token_address[:16], chain, ) # ── Fetch data from free APIs ─────────────────────────────── dexscreener_data = await _fetch_dexscreener(token_address, chain) birdeye_data = await _fetch_birdeye(token_address) # ── Parse DexScreener data ────────────────────────────────── if dexscreener_data: pairs = dexscreener_data.get("pairs", []) if pairs: pair = pairs[0] profile.symbol = pair.get("baseToken", {}).get("symbol", "") profile.name = pair.get("baseToken", {}).get("name", "") profile.decimals = int(pair.get("baseToken", {}).get("decimals", 18)) profile.pair_liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0)) profile.pair_age_days = _compute_pair_age_days(pair.get("pairCreatedAt", 0)) # Tax data (if available from DexScreener) if "buyTax" in pair: profile.buy_tax_pct = float(pair.get("buyTax", 0)) if "sellTax" in pair: profile.sell_tax_pct = float(pair.get("sellTax", 0)) # LP holder info lp_holders = pair.get("liquidity", {}).get("holders", []) if lp_holders: for h in lp_holders: if h.get("address", "").lower() in ( _normalize_address(profile.owner_address), _normalize_address(profile.address), ): profile.lp_token_pct_held_by_deployer = float(h.get("percent", 0)) break # ── Parse Birdeye data ────────────────────────────────────── if birdeye_data: # Supply data profile.total_supply_raw = int( birdeye_data.get("totalSupply", birdeye_data.get("supply", 0)) ) profile.circulating_supply_raw = int(birdeye_data.get("circulatingSupply", 0)) profile.max_supply_raw = int(birdeye_data.get("maxSupply", 0)) # Convert based on decimals divider = 10**profile.decimals if profile.total_supply_raw > 0: profile.total_supply = profile.total_supply_raw / divider if profile.circulating_supply_raw > 0: profile.circulating_supply = profile.circulating_supply_raw / divider if profile.max_supply_raw > 0: profile.max_supply = profile.max_supply_raw / divider # Holder data holders = birdeye_data.get("holders", []) if holders: profile.holder_count = len(holders) top10_pct = 0.0 top50_pct = 0.0 for i, h in enumerate(holders): pct = float(h.get("percent", h.get("percentage", 0))) if i < 10: top10_pct += pct if i < 50: top50_pct += pct if i < MAX_HOLDER_SAMPLE: profile.whale_holdings.append( { "address": h.get("address", ""), "pct": pct, "balance": h.get("balance", 0), } ) profile.top_10_holder_pct = top10_pct profile.top_50_holder_pct = top50_pct # Check deployer holding if profile.owner_address: for h in holders: if h.get("address", "").lower() == _normalize_address(profile.owner_address): profile.deployer_hold_pct = float(h.get("percent", h.get("percentage", 0))) break # Supply mechanics profile.has_mint_function = birdeye_data.get("mintable", False) profile.has_burn_function = birdeye_data.get("burnable", False) profile.is_deflationary = birdeye_data.get("deflationary", False) # Lock status lock_data = birdeye_data.get("liquidityLock", {}) if lock_data: locked = lock_data.get("locked", False) expiry = lock_data.get("expiry", "") if locked: profile.lock_status = LockStatus.LOCKED profile.liquidity_lock_expiry = expiry elif lock_data.get("removed", False): profile.lock_status = LockStatus.REMOVED else: profile.lock_status = LockStatus.UNLOCKED # Ownership profile.is_renounced = birdeye_data.get("renounced", False) profile.owner_address = birdeye_data.get("owner", "") profile.has_proxy_admin = birdeye_data.get("proxy", False) profile.has_multisig = birdeye_data.get("multisig", False) # Tax if not profile.buy_tax_pct: profile.buy_tax_pct = float(birdeye_data.get("buyTax", 0)) if not profile.sell_tax_pct: profile.sell_tax_pct = float(birdeye_data.get("sellTax", 0)) profile.has_dynamic_tax = birdeye_data.get("dynamicTax", False) profile.has_blacklist = birdeye_data.get("blacklist", False) profile.has_max_wallet_limit = birdeye_data.get("maxWallet", False) # ── Run analysis ──────────────────────────────────────────── _analyze_supply_mechanics(profile) _analyze_concentration(profile) _analyze_liquidity(profile) _compute_overall_risk(profile) # ── Summary recommendation ────────────────────────────────── if profile.overall_risk == SupplyRisk.CRITICAL: profile.recommendations.insert( 0, "🚨 CRITICAL: Do NOT invest. Multiple severe risk factors detected." ) elif profile.overall_risk == SupplyRisk.HIGH: profile.recommendations.insert( 0, "⚠ HIGH RISK: Significant red flags. Proceed with extreme caution." ) elif profile.overall_risk == SupplyRisk.MEDIUM: profile.recommendations.insert( 0, "⚠ MEDIUM RISK: Some concerns. Verify all claims before investing." ) elif profile.overall_risk == SupplyRisk.LOW: profile.recommendations.insert( 0, "✅ LOW RISK: Reasonable tokenomics. Standard precautions advised." ) else: profile.recommendations.insert(0, "✅ SAFE: Tokenomics appear healthy. Always DYOR.") return profile # ── API fetch helpers ──────────────────────────────────────────── async def _fetch_dexscreener(token_address: str, chain: str) -> dict[str, Any]: """Fetch token data from DexScreener API.""" url = DEXSCREENER_TOKENS.format(token_addr=token_address) if not _validate_url(url): logger.warning("Invalid DexScreener URL for %s", token_address[:16]) return {} return await _fetch_json(url, "DexScreener") async def _fetch_birdeye(token_address: str) -> dict[str, Any]: """Fetch token data from Birdeye API.""" url = BIRDEYE_TOKEN.format(token_addr=token_address) if not _validate_url(url): logger.warning("Invalid Birdeye URL for %s", token_address[:16]) return {} return await _fetch_json(url, "Birdeye") async def _fetch_json(url: str, source: str) -> dict[str, Any]: """Fetch JSON from a URL with rate limiting.""" parsed = urlparse(url) host = parsed.hostname or "unknown" try: await _rate_limit_host(host) import aiohttp timeout = aiohttp.ClientTimeout(total=10) async with ( aiohttp.ClientSession(timeout=timeout) as session, session.get( url, headers={ "User-Agent": "RMI-TokenSupplyAnalyzer/1.0", "Accept": "application/json", }, ) as resp, ): if resp.status == 200: data = await resp.json() # Birdeye wraps data in data field if isinstance(data, dict) and "data" in data: return cast("dict[str, Any]", data["data"]) return cast("dict[str, Any]", data) elif resp.status == 429: logger.warning("Rate limited by %s, waiting 2s...", source) await asyncio.sleep(2) return {} else: logger.warning( "%s returned status %d for %s", source, resp.status, url[:60], ) return {} except TimeoutError: logger.warning("Timeout fetching from %s: %s", source, url[:60]) return {} except Exception as e: logger.warning("Error fetching from %s: %s", source, e) return {} def _compute_pair_age_days(created_at_unix: int) -> float: """Compute pair age in days from Unix timestamp.""" if not created_at_unix: return 0.0 try: created = datetime.fromtimestamp(created_at_unix / 1000, tz=UTC) # DexScreener uses ms now = datetime.now(tz=UTC) return max(0.0, (now - created).total_seconds() / 86400) except (ValueError, OSError): return 0.0 # ── Batch / streaming analysis ─────────────────────────────────── async def analyze_multiple_tokens( addresses: list[str], chain: str = "" ) -> list[TokenSupplyProfile]: """Analyze multiple tokens concurrently.""" tasks = [analyze_token_supply(addr, chain) for addr in addresses] results = await asyncio.gather(*tasks, return_exceptions=True) valid: list[TokenSupplyProfile] = [] for r in results: if isinstance(r, TokenSupplyProfile): valid.append(r) return valid # ── CLI entry point ────────────────────────────────────────────── async def main() -> None: """Run supply analysis from command line.""" import argparse parser = argparse.ArgumentParser(description="Token Supply & Liquidity Analyzer") parser.add_argument("address", help="Token contract address") parser.add_argument( "--chain", default="", help="Chain hint (ethereum, solana, bsc, polygon)", ) parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args() profile = await analyze_token_supply(args.address, args.chain) if args.json: import dataclasses def _serialize(obj: Any) -> str: if isinstance(obj, Enum): val = obj.value return str(val) if isinstance(obj, datetime): return obj.isoformat() return str(obj) print( json.dumps( dataclasses.asdict(profile), indent=2, default=_serialize, ) ) else: print(f"\n{'=' * 60}") print(f"TOKEN SUPPLY ANALYSIS: {profile.symbol} ({profile.name})") print(f"{'=' * 60}") print(f"Address: {profile.address}") print(f"Chain: {profile.chain}") print("\n── Supply ──") print(f" Total supply: {profile.total_supply:,.2f}") print(f" Circulating: {profile.circulating_supply:,.2f}") print(f" Max supply: {profile.max_supply:,.2f}") print(f" Burned: {profile.burned_supply:,.2f}") print( f" Has mint: {'YES' if profile.has_mint_function else 'no'} ({profile.mint_function_type})" ) print( f" Has burn: {'YES' if profile.has_burn_function else 'no'} ({profile.burn_function_type})" ) print(f" Deflationary: {'YES' if profile.is_deflationary else 'no'}") print("\n── Concentration ──") print(f" Holders: {profile.holder_count:,}") print(f" Top 10 hold: {profile.top_10_holder_pct:.1f}%") print(f" Top 50 hold: {profile.top_50_holder_pct:.1f}%") print(f" Deployer holds: {profile.deployer_hold_pct:.1f}%") print("\n── Liquidity ──") print(f" Lock status: {profile.lock_status.value}") print(f" Lock expiry: {profile.liquidity_lock_expiry or 'N/A'}") print(f" Liquidity (USD): ${profile.pair_liquidity_usd:,.2f}") print(f" Pair age (days): {profile.pair_age_days:.1f}") print(f" LP by deployer: {profile.lp_token_pct_held_by_deployer:.1f}%") print("\n── Tax ──") print(f" Buy tax: {profile.buy_tax_pct:.1f}%") print(f" Sell tax: {profile.sell_tax_pct:.1f}%") print(f" Dynamic tax: {'YES' if profile.has_dynamic_tax else 'no'}") print(f" Blacklist: {'YES' if profile.has_blacklist else 'no'}") print("\n── Ownership ──") print(f" Renounced: {'YES' if profile.is_renounced else 'NO'}") print(f" Proxy admin: {'YES' if profile.has_proxy_admin else 'no'}") print(f" Multi-sig: {'YES' if profile.has_multisig else 'no'}") print("\n── Risk Scores ──") print( f" Supply risk: {profile.supply_risk.value} ({profile.supply_risk_score:.0f}/100)" ) print( f" Concentration: {profile.concentration_risk.value} ({profile.concentration_score:.0f}/100)" ) print( f" Liquidity risk: {profile.liquidity_risk.value} ({profile.liquidity_score:.0f}/100)" ) print( f" OVERALL RISK: {profile.overall_risk.value} ({profile.overall_risk_score:.0f}/100)" ) print("\n── Patterns Detected ──") for p in profile.patterns_detected: print(f" • {p}") print("\n── Recommendations ──") for r in profile.recommendations: print(f" {r}") print() if __name__ == "__main__": asyncio.run(main())