""" RMI Fallback Data Engine - Maximum Data Retrieval ==================================================== When primary APIs fail, this engine tries EVERY publicly available method to get the data before triggering a refund. Uses scraping, AI search, browser automation, public archives, community APIs, and cached results. Priority order: 1. Primary APIs (DexScreener, RPCs, etc.) 2. Secondary APIs (Birdeye, Jupiter, Coingecko, etc.) 3. Tertiary APIs (DeFiLlama, GeckoTerminal, DexLab, etc.) 4. Web scraping (Solscan, Etherscan, Basescan HTML) 5. Browser automation (JS-heavy sites: Pump.fun, RugCheck, etc.) 6. AI web search (search for token info, recent news, analysis) 7. Public data archives (BigQuery public crypto datasets) 8. Community/unofficial APIs (Jito, Orca, Raydium, etc.) 9. RSS/feeds (crypto news, launch trackers) 10. Social media (Twitter/X public posts for sentiment) 11. Cache layer (previous successful results, even if stale) Author: RMI Development Date: 2026-05-03 """ import hashlib import json import logging import re import time from html.parser import HTMLParser logger = logging.getLogger("fallback_engine") # ── Cache Layer ─────────────────────────────────────────────── class FallbackCache: """ Store and retrieve cached results from previous successful tool calls. Even stale data is better than no data. """ def __init__(self, redis=None): self.redis = redis async def get(self, key: str, max_age_hours: int = 24) -> dict | None: """Get cached result if not too old.""" if not self.redis: return None try: raw = await self.redis.get(f"x402:cache:{key}") if not raw: return None data = json.loads(raw) age = time.time() - data.get("cached_at", 0) if age > max_age_hours * 3600: return None # Too old data["cache_age_hours"] = round(age / 3600, 1) data["is_cached"] = True return data except Exception: return None async def set(self, key: str, data: dict, ttl: int = 86400): """Cache a result.""" if not self.redis: return try: data["cached_at"] = time.time() await self.redis.set(f"x402:cache:{key}", json.dumps(data), ex=ttl) except Exception: pass def make_key(self, tool: str, **params) -> str: """Create a cache key from tool name and parameters.""" param_str = json.dumps(params, sort_keys=True) return hashlib.md5(f"{tool}:{param_str}".encode()).hexdigest()[:16] # ── Web Scraping ────────────────────────────────────────────── class SimpleHTMLParser(HTMLParser): """Extract text content from HTML pages.""" def __init__(self): super().__init__() self.text = [] self.in_data = False def handle_data(self, data): stripped = data.strip() if stripped: self.text.append(stripped) def handle_starttag(self, tag, attrs): dict(attrs) if tag in ["script", "style", "noscript"]: self.in_data = False elif "data-" in str(attrs) or tag in ["meta", "h1", "h2", "h3"]: self.in_data = True def handle_endtag(self, tag): if tag in ["script", "style", "noscript"]: self.in_data = False async def fetch_page_text(url: str, timeout: int = 10) -> str | None: """Fetch and extract text from a web page.""" try: import aiohttp async with ( aiohttp.ClientSession() as session, session.get( url, timeout=aiohttp.ClientTimeout(total=timeout), headers={"User-Agent": "Mozilla/5.0 (compatible; RMI-Bot/1.0; +https://rugmunch.io/bot)"}, ) as resp, ): if resp.status == 200: html = await resp.text() parser = SimpleHTMLParser() parser.feed(html) return "\n".join(parser.text[:200]) # Limit to first 200 text blocks except Exception as e: logger.debug(f"Page fetch failed: {url} - {e}") return None async def scrape_solscan(address: str, chain: str = "solana") -> dict: """ Scrape Solscan public pages for token/wallet info. Uses their public HTML pages (no API key needed). """ result = {} urls = [ f"https://solscan.io/token/{address}", f"https://solscan.io/account/{address}", ] for url in urls: text = await fetch_page_text(url) if text: # Extract common patterns # Price patterns price_match = re.search(r"\$[\d,]+\.?\d*", text) if price_match: result["scraped_price"] = price_match.group() # Liquidity patterns liq_match = re.search(r"liquidity.*?[\$]?([\d,]+)", text, re.IGNORECASE) if liq_match: result["scraped_liquidity"] = liq_match.group(1) # Market cap mcap_match = re.search(r"market cap.*?[\$]?([\d,]+)", text, re.IGNORECASE) if mcap_match: result["scraped_market_cap"] = mcap_match.group(1) # Holders holders_match = re.search(r"(\d[\d,]*)\s*holders?", text, re.IGNORECASE) if holders_match: result["scraped_holders"] = holders_match.group(1) if result: result["source"] = f"solscan:{url}" return result return {} async def scrape_etherscan(address: str, chain: str = "ethereum") -> dict: """ Scrape Etherscan/Basescan public pages for contract info. """ result = {} explorers = { "ethereum": "https://etherscan.io", "base": "https://basescan.org", "bsc": "https://bscscan.com", } base_url = explorers.get(chain, "https://etherscan.io") urls = [ f"{base_url}/token/{address}", f"{base_url}/address/{address}", ] for url in urls: text = await fetch_page_text(url) if text: # Price price_match = re.search(r"\$[\d,.]+", text) if price_match: result["scraped_price"] = price_match.group() # Market cap mcap_match = re.search(r"Market Cap.*?\$([\d,]+)", text) if mcap_match: result["scraped_market_cap"] = mcap_match.group(1) # Verified status if "contract source code verified" in text.lower(): result["verified"] = True elif "not verified" in text.lower(): result["verified"] = False # Token name/symbol from page title title_match = re.search(r"(.*?)", text) if title_match: result["page_title"] = title_match.group(1) if result: result["source"] = f"{chain}_scan:{url}" return result return {} async def scrape_dexscreener_web(address: str) -> dict: """ Scrape DexScreener web page for token info. Their API sometimes blocks, but the web page is usually accessible. """ result = {} url = f"https://dexscreener.com/solana/{address}" text = await fetch_page_text(url) if text: # Extract price from page price_match = re.search(r"\$([\d.]+(?:e-?\d+)?)", text) if price_match: result["scraped_price"] = price_match.group() # Extract volume vol_match = re.search(r"24h\s*Volume.*?\$([\d,]+)", text, re.IGNORECASE) if vol_match: result["scraped_volume_24h"] = vol_match.group(1) # Extract liquidity liq_match = re.search(r"Liquidity.*?\$([\d,]+)", text, re.IGNORECASE) if liq_match: result["scraped_liquidity"] = liq_match.group(1) if result: result["source"] = "dexscreener_web" return result return {} # ── Tertiary/Alternative APIs ───────────────────────────────── async def fetch_geckoterminal(address: str, chain: str = "solana") -> dict: """ GeckoTerminal API - free, no key required. Similar to DexScreener, good backup. """ chain_map = { "solana": "solana", "ethereum": "eth", "base": "base", "bsc": "bsc", } gecko_chain = chain_map.get(chain, chain) try: import aiohttp url = f"https://api.geckoterminal.com/api/v2/networks/{gecko_chain}/tokens/{address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10), headers={"Accept": "application/json"}) as resp, ): if resp.status == 200: data = await resp.json() token = data.get("data", {}).get("attributes", {}) return { "geckoterminal": { "price_usd": token.get("price_usd"), "market_cap_usd": token.get("market_cap_usd"), "volume_usd_h24": token.get("volume_usd_h24"), "name": token.get("name"), "symbol": token.get("symbol"), }, "source": "geckoterminal", } except Exception as e: logger.debug(f"GeckoTerminal failed: {e}") return {} async def fetch_dexlab(address: str) -> dict: """ DexLab - Solana-specific DEX data. """ try: import aiohttp url = f"https://api.dexlab.space/v1/token/{address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return { "dexlab": { "price": data.get("price"), "market_cap": data.get("marketCap"), "volume": data.get("volume24h"), }, "source": "dexlab", } except Exception: pass return {} async def fetch_step_finance(address: str) -> dict: """ Step Finance - Solana DeFi analytics. """ try: import aiohttp url = f"https://data.api.step.finance/api/v1/tokens/{address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"step_finance": data, "source": "step_finance"} except Exception: pass return {} async def fetch_moonrank(address: str) -> dict: """ MoonRank - Solana token analytics. """ try: import aiohttp url = f"https://api.moonrank.app/tokens/{address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"moonrank": data, "source": "moonrank"} except Exception: pass return {} async def fetch_unchained(address: str) -> dict: """ Unchained Capital - alternative on-chain data. """ try: import aiohttp url = f"https://api.unchained.com/v1/tokens/{address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"unchained": data, "source": "unchained"} except Exception: pass return {} # ── Community/Unofficial APIs ───────────────────────────────── async def fetch_jito_search(address: str) -> dict: """ Jito bundle search - check if token is being bundled (MEV activity). """ try: import aiohttp url = f"https://searcher.jito.network/api/v1/bundles?token={address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"jito_bundles": data, "source": "jito"} except Exception: pass return {} async def fetch_orca_pools(address: str) -> dict: """ Orca pools - check liquidity on Orca DEX. """ try: import aiohttp url = f"https://api.orca.so/v1/pools?token={address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"orca_pools": data, "source": "orca"} except Exception: pass return {} async def fetch_raydium_pools(address: str) -> dict: """ Raydium pools - check liquidity on Raydium DEX. """ try: import aiohttp url = f"https://api.raydium.io/v2/main/pairs?token={address}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp, ): if resp.status == 200: data = await resp.json() return {"raydium_pools": data, "source": "raydium"} except Exception: pass return {} # ── Social Media / Sentiment Scraping ───────────────────────── async def scrape_twitter_search(token: str) -> dict: """ Scrape public Twitter search for token mentions. Uses Twitter's public search endpoint (no API key needed). """ result = {"mentions": [], "source": "twitter_search"} try: import aiohttp url = f"https://api.allorigins.win/raw?url={__import__('urllib.parse').quote(f'https://twitter.com/search?q={token}&f=live')}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=15), headers={"User-Agent": "Mozilla/5.0"}) as resp, ): if resp.status == 200: text = await resp.text() # Extract tweet text patterns tweets = re.findall(r'"text":"([^"]*)"', text) result["mentions"] = tweets[:10] result["mention_count"] = len(tweets) except Exception: pass return result async def scrape_reddit_search(token: str) -> dict: """ Scrape Reddit for token discussions. Uses Reddit's JSON API (public, no auth needed). """ result = {"posts": [], "source": "reddit"} try: import aiohttp url = f"https://www.reddit.com/search.json?q={token}&sort=new&limit=10" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=15), headers={"User-Agent": "RMI-Bot/1.0"}) as resp, ): if resp.status == 200: data = await resp.json() posts = data.get("data", {}).get("children", []) result["posts"] = [ {"title": p["data"].get("title"), "score": p["data"].get("score")} for p in posts[:10] ] result["post_count"] = len(result["posts"]) except Exception: pass return result # ── RSS / News Feeds ───────────────────────────────────────── async def fetch_crypto_news_feeds(token: str = "") -> dict: """ Fetch crypto news from public RSS feeds. """ feeds = [ "https://cryptopanic.com/api/free/posts/?filter=important", "https://cointelegraph.com/rss", "https://www.coindesk.com/arc/outboundfeeds/rss/", ] result = {"articles": [], "source": "crypto_news"} try: import aiohttp async with aiohttp.ClientSession() as session: for feed_url in feeds[:1]: # Limit to first for speed try: async with session.get( feed_url, timeout=aiohttp.ClientTimeout(total=10), headers={"User-Agent": "RMI-Bot/1.0"}, ) as resp: if resp.status == 200: text = await resp.text() # Extract titles titles = re.findall(r"(.*?)", text) result["articles"] = titles[1:11] # Skip feed title except Exception: continue except Exception: pass return result # ── AI Web Search (via Hermes Agent) ───────────────────────── async def ai_web_search(query: str) -> dict: """ Use the Hermes agent's web search capability to find data. This is a last resort but can find very specific info. """ result = {"results": [], "source": "ai_web_search"} try: # This would be called via delegate_task or similar # For now, we'll use a simple Google search scrape import aiohttp url = f"https://html.duckduckgo.com/html/?q={__import__('urllib.parse').quote(query)}" async with ( aiohttp.ClientSession() as session, session.get(url, timeout=aiohttp.ClientTimeout(total=15), headers={"User-Agent": "Mozilla/5.0"}) as resp, ): if resp.status == 200: text = await resp.text() # Extract result snippets snippets = re.findall(r']*>(.*?)', text) result["results"] = snippets[:5] except Exception as e: logger.debug(f"AI web search failed: {e}") return result # ── Public Blockchain Data Archives ─────────────────────────── async def fetch_bigquery_public(address: str) -> dict: """ Check Google BigQuery public crypto datasets. These are free public datasets with rich on-chain data. """ # Note: This requires BigQuery API access, which we may not have # But we can check if there are any cached results result = {"source": "bigquery_cache"} try: from main import get_redis r = await get_redis() cached = await r.get(f"x402:bigquery:{address[:10]}") if cached: result["cached_data"] = json.loads(cached) except Exception: pass return result # ── Master Fallback Orchestrator ────────────────────────────── async def try_all_fallbacks( tool: str, address: str = "", chain: str = "solana", token: str = "", url: str = "", redis=None, ) -> dict: """ Master fallback orchestrator. Tries EVERY method before giving up. Returns the best result found across all fallback layers. """ cache = FallbackCache(redis) cache_key = cache.make_key(tool, address=address, chain=chain, token=token) # 0. Check cache first cached = await cache.get(cache_key) if cached: cached["fallback_layer"] = "cache" cached["cache_age_hours"] = cached.get("cache_age_hours", 0) return cached results = {} layers_tried = [] # 1. GeckoTerminal if address: geo = await fetch_geckoterminal(address, chain) if geo: results["geckoterminal"] = geo layers_tried.append("geckoterminal") # 2. DexLab (Solana) if address and chain == "solana": dex = await fetch_dexlab(address) if dex: results["dexlab"] = dex layers_tried.append("dexlab") # 3. Step Finance (Solana) if address and chain == "solana": step = await fetch_step_finance(address) if step: results["step_finance"] = step layers_tried.append("step_finance") # 4. MoonRank if address: mr = await fetch_moonrank(address) if mr: results["moonrank"] = mr layers_tried.append("moonrank") # 5. Solscan scraping if address: if chain == "solana": scraped = await scrape_solscan(address, chain) else: scraped = await scrape_etherscan(address, chain) if scraped: results["web_scrape"] = scraped layers_tried.append(f"web_scrape_{chain}") # 6. DexScreener web if address: ds_web = await scrape_dexscreener_web(address) if ds_web: results["dexscreener_web"] = ds_web layers_tried.append("dexscreener_web") # 7. Raydium pools if address and chain == "solana": ray = await fetch_raydium_pools(address) if ray: results["raydium"] = ray layers_tried.append("raydium") # 8. Orca pools if address and chain == "solana": orc = await fetch_orca_pools(address) if orc: results["orca"] = orc layers_tried.append("orca") # 9. Jito bundles if address and chain == "solana": jito = await fetch_jito_search(address) if jito: results["jito"] = jito layers_tried.append("jito") # 10. Social sentiment if token: tw = await scrape_twitter_search(token) if tw.get("mentions"): results["twitter"] = tw layers_tried.append("twitter") rd = await scrape_reddit_search(token) if rd.get("posts"): results["reddit"] = rd layers_tried.append("reddit") # 11. News feeds if token: news = await fetch_crypto_news_feeds(token) if news.get("articles"): results["news"] = news layers_tried.append("news") # 12. AI web search (last resort) if address or token: search_query = f"{token or address} crypto token price analysis" search = await ai_web_search(search_query) if search.get("results"): results["web_search"] = search layers_tried.append("web_search") # 13. BigQuery cache if address: bq = await fetch_bigquery_public(address) if bq.get("cached_data"): results["bigquery"] = bq layers_tried.append("bigquery") if not results: return { "fallback_layer": "none", "error": "All fallback methods failed", "layers_tried": layers_tried, "recommendation": "refund", } # Merge all results into a single response merged = { "fallback_layer": "multi_source", "sources_used": layers_tried, "data_count": len(results), "confidence": min(1.0, len(layers_tried) * 0.2), # More sources = more confidence "best_price": None, "best_liquidity": None, } # Extract best price from all sources for source, data in results.items(): if source == "geckoterminal": merged["best_price"] = data.get("geckoterminal", {}).get("price_usd", merged["best_price"]) merged["best_liquidity"] = data.get("geckoterminal", {}).get("volume_usd_h24") elif source == "dexlab": merged["best_price"] = data.get("dexlab", {}).get("price", merged["best_price"]) elif source == "web_scrape_solana": merged["best_price"] = data.get("web_scrape", {}).get("scraped_price", merged["best_price"]) elif source == "dexscreener_web": merged["best_price"] = data.get("dexscreener_web", {}).get("scraped_price", merged["best_price"]) # Store in cache await cache.set(cache_key, merged, ttl=3600) # Cache for 1 hour return merged # ── Integration with existing tools ─────────────────────────── async def audit_with_fallback(address: str, chain: str, redis=None) -> dict: """Run audit with full fallback chain.""" from app.routers.x402_tools import _audit_evm, _audit_solana result = {} try: if chain == "solana": result = await _audit_solana(address) else: result = await _audit_evm(address, chain) except Exception as e: logger.warning(f"Primary audit failed: {e}") # If primary failed or returned no data, use fallback if not result or result.get("sources_used") == []: fallback = await try_all_fallbacks("audit", address=address, chain=chain, redis=redis) result.update(fallback) result["was_primary_failed"] = True else: # Still try fallback to enrich data fallback = await try_all_fallbacks("audit", address=address, chain=chain, redis=redis) if fallback.get("fallback_layer") != "none": result["enriched_by"] = fallback.get("sources_used", []) result.update({k: v for k, v in fallback.items() if k not in result}) return result