#!/usr/bin/env python3 """ Free Solscan API Client - Reverse-Engineered Bypasses Cloudflare + $200/mo paywall Based on: github.com/paoloanzn/free-solscan-api Extended with RMI security intelligence """ import base64 import hashlib import logging import time from typing import Any import cloudscraper logger = logging.getLogger(__name__) # ─── Token Generator ────────────────────────────────────────────────────────── def generate_solauth_token() -> str: """Generate sol-aut token for Solscan API authentication.""" timestamp = str(int(time.time())) data = f"solscan_{timestamp}" signature = hashlib.sha256(data.encode()).hexdigest() return base64.b64encode(f"{timestamp}:{signature}".encode()).decode() # ─── Scrapers (separate instances for parallelism) ──────────────────────────── _scraper = cloudscraper.create_scraper(browser={"browser": "chrome", "platform": "windows", "desktop": True}) BASE_URL = "https://api-v2.solscan.io/v2" def _headers() -> dict[str, str]: return { "Accept": "application/json, text/plain, */*", "sol-aut": generate_solauth_token(), "Referer": "https://solscan.io/", "Origin": "https://solscan.io", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", } # ─── Core Request Handler ───────────────────────────────────────────────────── def _request(endpoint: str, params: dict | None = None, retries: int = 3) -> Any: """Make authenticated request to Solscan API with retries.""" url = f"{BASE_URL}{endpoint}" for attempt in range(retries): try: response = _scraper.get(url, headers=_headers(), params=params or {}, timeout=15) if response.status_code == 200: data = response.json() if isinstance(data, dict) and "data" in data: return data["data"] return data elif response.status_code == 429: wait = 2**attempt logger.warning(f"Rate limited, waiting {wait}s...") time.sleep(wait) else: logger.warning(f"HTTP {response.status_code} on {endpoint}") if attempt == retries - 1: return None except Exception as e: logger.error(f"Request failed ({attempt + 1}/{retries}): {e}") if attempt == retries - 1: return None time.sleep(1) return None # ─── API Endpoints ──────────────────────────────────────────────────────────── class FreeSolscanClient: """Reverse-engineered Solscan API client with all security endpoints.""" # ── Account / Wallet ───────────────────────────────────────────────────── @staticmethod def account_info(address: str) -> dict | None: """Get account info (balance, type, owner).""" return _request("/account", {"address": address}) @staticmethod def account_transactions(address: str, page: int = 1, page_size: int = 40) -> list | None: """Get transaction history for an address.""" return _request("/account/transaction", {"address": address, "page": page, "page_size": page_size}) @staticmethod def account_transfers( address: str, page: int = 1, page_size: int = 100, remove_spam: bool = True, exclude_zero: bool = True, activity_type: str | None = None, flow: str | None = None, ) -> list | None: """Get token transfers for an address.""" params = { "address": address, "page": page, "page_size": page_size, "remove_spam": str(remove_spam).lower(), "exclude_amount_zero": str(exclude_zero).lower(), } if activity_type: params["activity_type"] = activity_type if flow: params["flow"] = flow return _request("/account/transfer", params) @staticmethod def account_defi_activities( address: str, page: int = 1, page_size: int = 100, activity_type: str | None = None ) -> list | None: """Get DeFi activities (swaps, liquidity, etc).""" params = { "address": address, "page": page, "page_size": page_size, } if activity_type: params["activity_type"] = activity_type return _request("/account/defi/activities", params) @staticmethod def account_balance_history(address: str) -> list | None: """Get SOL balance history over time.""" return _request("/account/balance_change", {"address": address}) @staticmethod def account_portfolio(address: str, page: int = 1, page_size: int = 100, hide_zero: bool = True) -> dict | None: """Get token portfolio for an address.""" return _request( "/account/tokenaccounts", { "address": address, "page": page, "page_size": page_size, "type": "token", "hide_zero": str(hide_zero).lower(), }, ) @staticmethod def account_token_accounts(address: str, token: str) -> list | None: """Get specific token accounts owned by address.""" return _request( "/account/tokenaccounts", { "address": address, "tokenAddress": token, "type": "token", }, ) # ── Token Data ─────────────────────────────────────────────────────────── @staticmethod def token_data( token_address: str = "So11111111111111111111111111111111111111112", ) -> dict | None: """Get token metadata (name, symbol, supply, decimals, etc).""" return _request("/token", {"address": token_address}) @staticmethod def token_holders(token_address: str, page: int = 1, page_size: int = 100) -> list | None: """Get top token holders.""" return _request( "/token/holder", { "tokenAddress": token_address, "page": page, "page_size": page_size, }, ) @staticmethod def token_holders_total(token_address: str) -> int | None: """Get total number of unique holders.""" result = _request("/token/holder/total", {"tokenAddress": token_address}) if isinstance(result, dict): return result.get("result") return result @staticmethod def token_transfer_summary(token_address: str) -> dict | None: """Get transfer summary (volume, count, etc).""" return _request("/token/transfer/summary", {"tokenAddress": token_address}) @staticmethod def token_defi_activities( token_address: str, page: int = 1, page_size: int = 100, activity_type: str | None = None ) -> list | None: """Get DeFi activities for a token (swaps, liquidity changes).""" params = { "token": token_address, "page": page, "page_size": page_size, } if activity_type: params["activity_type"] = activity_type return _request("/token/defi/activities", params) @staticmethod def token_markets(token_address: str, page: int = 1, page_size: int = 100) -> list | None: """Get DEX markets for a token.""" return _request( "/token/markets", { "token": token_address, "page": page, "page_size": page_size, }, ) # ── Transaction Details ────────────────────────────────────────────────── @staticmethod def transaction_detail(tx_hash: str) -> dict | None: """Get full transaction details including inner instructions.""" return _request("/transaction/detail", {"tx": tx_hash}) # ── Top Transfers (Critical for Bundler Detection) ────────────────────── @staticmethod def top_address_transfers(address: str, range_days: int = 7) -> list | None: """Get top transfers to/from an address - KEY for gas funding traces.""" return _request( "/account/top-transfer", { "address": address, "range_day": range_days, }, ) # ── Batch Operations ───────────────────────────────────────────────────── @staticmethod def batch_account_info(addresses: list[str]) -> dict[str, Any]: """Get info for multiple addresses (for holder analysis).""" results = {} for addr in addresses: results[addr] = FreeSolscanClient.account_info(addr) time.sleep(0.2) # Rate limit respect return results @staticmethod def get_holder_wallets(token_address: str, top_n: int = 50) -> list[dict]: """Get top N holders with their balances.""" holders = [] page = 1 while len(holders) < top_n: batch = FreeSolscanClient.token_holders(token_address, page=page, page_size=100) if not batch: break holders.extend(batch) page += 1 time.sleep(0.3) return holders[:top_n] # ── Security-Specific Helpers ──────────────────────────────────────────── @staticmethod def get_deployer_transactions(deployer: str, pages: int = 5) -> list[dict]: """Get recent transaction history for deployer analysis.""" all_txs = [] for page in range(1, pages + 1): txs = FreeSolscanClient.account_transactions(deployer, page=page, page_size=40) if not txs: break all_txs.extend(txs) time.sleep(0.2) return all_txs @staticmethod def get_wallet_funding_sources(wallet: str, days: int = 30) -> list[dict]: """Trace where a wallet got its initial SOL from - KEY for exchange funding %.""" transfers = FreeSolscanClient.top_address_transfers(wallet, range_days=days) if not transfers: return [] # Look for incoming transfers (funding) funding_sources = [] for tx in transfers: flow = tx.get("flow", "") if flow == "in" or tx.get("change_amount", 0) > 0: funding_sources.append( { "from": tx.get("from_address"), "to": tx.get("to_address"), "amount": tx.get("change_amount", 0), "token": tx.get("token_address"), "symbol": tx.get("token_symbol"), "time": tx.get("block_time"), "tx_hash": tx.get("trans_id"), } ) return funding_sources @staticmethod def get_token_creation_tx(token_address: str) -> dict | None: """Find the transaction that created this token.""" transfers = FreeSolscanClient.account_transfers(token_address, page=1, page_size=10) if transfers and len(transfers) > 0: # The earliest transfer often reveals the creation oldest = min(transfers, key=lambda x: x.get("block_time", float("inf"))) tx_hash = oldest.get("trans_id") if tx_hash: return FreeSolscanClient.transaction_detail(tx_hash) return None @staticmethod def get_lp_token_accounts(token_address: str) -> list[dict]: """Get liquidity pool token accounts for LP analysis.""" markets = FreeSolscanClient.token_markets(token_address, page_size=100) lp_accounts = [] if markets: for market in markets: lp_info = { "market_id": market.get("marketId"), "program": market.get("programId"), "liquidity_a": market.get("liquidityA"), "liquidity_b": market.get("liquidityB"), "volume_24h": market.get("volume24h"), "lp_locked": market.get("lpLocked"), "lp_lock_tx": market.get("lpLockTx"), } lp_accounts.append(lp_info) return lp_accounts # ─── Known Exchange Wallets (for Exchange Funding % calculation) ──────────── KNOWN_EXCHANGE_WALLETS = { # Binance "5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9": "Binance", "AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWJTSc": "Binance", "2ojv9BAiHUrvsm9gxDe7fJSzbNZSJbkEjpJgjHfUJVZW": "Binance", # Coinbase "3X3kC6PTLVPKYxFG3KeiqEA6f3icpJYYrGAGTB2EfUta": "Coinbase", "GJRs4FwHtemZ5ZE9x3SSv2EHfFpQMFt4VqFz9KxM8WGH": "Coinbase", # Kraken "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1": "Kraken", "FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN1": "Kraken", # OKX "7CNAohxBYpFi8zAAfNcRpt7Hjn2FyuKVs2aHXV5Wpump": "OKX", "BHwdGGP9LFLBdPZWuY6mk8mGG9i7WwfUuRG4vxWaFkqA": "OKX", # Bybit "AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWJTSc": "Bybit", # noqa: F601 "6FzX58J2q7Wd1j5GmU5uZJhKLnG9zQZxYGvZk2Jx6WqD": "Bybit", # Jupiter / DEX aggregator "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter DEX", "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGJBb": "Jupiter DEX", # Pump.fun "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "Pump.fun", # Raydium "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium", # Orca "9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP": "Orca", # Phantom "DeJBGdMFa1uynnnKiwrVioatTuHmNLpyADnmSZ5EVXd1": "Phantom", } EXCHANGE_DOMAINS = { "binance.com", "coinbase.com", "kraken.com", "okx.com", "bybit.com", "kucoin.com", "gate.io", "mexc.com", "huobi.com", "bitget.com", "crypto.com", } def is_known_exchange(wallet_address: str) -> str | None: """Check if a wallet belongs to a known exchange.""" return KNOWN_EXCHANGE_WALLETS.get(wallet_address) # ─── Quick Test ─────────────────────────────────────────────────────────────── if __name__ == "__main__": logging.basicConfig(level=logging.INFO) # Test with a known token token = "So11111111111111111111111111111111111111112" # wSOL print("Testing Free Solscan Client...") data = FreeSolscanClient.token_data(token) print(f"Token: {data}") holders = FreeSolscanClient.token_holders_total(token) print(f"Total holders: {holders}")