""" RMI Social Feed - X/Twitter + Reddit crypto news with aggressive caching. Top 50 crypto X accounts monitored for breaking news. Falls back to Nitter when rate limited. Reddit top posts from r/CryptoCurrency, r/ethfinance, r/solana. """ import asyncio import hashlib import logging import time from datetime import UTC, datetime import httpx logger = logging.getLogger("social_feed") # ═══════════════════════════════════════════════════════════════════════════ # TOP 50 CRYPTO X ACCOUNTS - by influence/relevance # ═══════════════════════════════════════════════════════════════════════════ TOP_CRYPTO_ACCOUNTS = [ {"handle": "cz_binance", "name": "CZ Binance", "followers": "9.5M", "pfp": "cz_binance"}, {"handle": "VitalikButerin", "name": "Vitalik Buterin", "followers": "5.5M", "pfp": "vitalik"}, {"handle": "SBF_FTX", "name": "SBF", "followers": "1.1M", "pfp": "sbf"}, {"handle": "aantonop", "name": "Andreas Antonopoulos", "followers": "750K", "pfp": "aantonop"}, {"handle": "balajis", "name": "Balaji Srinivasan", "followers": "1M", "pfp": "balajis"}, {"handle": "cdixon", "name": "Chris Dixon", "followers": "950K", "pfp": "cdixon"}, {"handle": "brian_armstrong", "name": "Brian Armstrong", "followers": "1.3M", "pfp": "brian"}, {"handle": "aeyakovenko", "name": "Anatoly Yakovenko", "followers": "400K", "pfp": "toly"}, {"handle": "saylor", "name": "Michael Saylor", "followers": "3.6M", "pfp": "saylor"}, {"handle": "mcuban", "name": "Mark Cuban", "followers": "8.8M", "pfp": "cuban"}, {"handle": "david_sacks", "name": "David Sacks", "followers": "900K", "pfp": "sacks"}, {"handle": "twobitidiot", "name": "Ryan Selkis", "followers": "400K", "pfp": "selkis"}, {"handle": "RamaswamyNik", "name": "Vivek Ramaswamy", "followers": "3.5M", "pfp": "vivek"}, {"handle": "elonmusk", "name": "Elon Musk", "followers": "210M", "pfp": "elon"}, {"handle": "novogratz", "name": "Mike Novogratz", "followers": "540K", "pfp": "novo"}, {"handle": "CryptoHayes", "name": "Arthur Hayes", "followers": "600K", "pfp": "hayes"}, {"handle": "zhusu", "name": "Zhu Su", "followers": "650K", "pfp": "zhu"}, {"handle": "cobie", "name": "Cobie", "followers": "800K", "pfp": "cobie"}, {"handle": "loomdart", "name": "Loomdart", "followers": "350K", "pfp": "loom"}, {"handle": "0xngmi", "name": "0xngmi", "followers": "250K", "pfp": "ngmi"}, {"handle": "gaut", "name": "Gaut", "followers": "200K", "pfp": "gaut"}, {"handle": "ThinkingUSD", "name": "ThinkingUSD", "followers": "180K", "pfp": "thinking"}, {"handle": "blknoiz06", "name": "Blknoiz06", "followers": "300K", "pfp": "blknoiz"}, {"handle": "Dynamo_Patrick", "name": "Patrick Dynamo", "followers": "350K", "pfp": "dynamo"}, {"handle": "TheCryptoLark", "name": "Lark Davis", "followers": "1.2M", "pfp": "lark"}, {"handle": "APompliano", "name": "Anthony Pompliano", "followers": "1.7M", "pfp": "pomp"}, {"handle": "RaoulGMI", "name": "Raoul Pal", "followers": "1.1M", "pfp": "raoul"}, {"handle": "Melt_Dem", "name": "Meltem Demirors", "followers": "300K", "pfp": "meltem"}, {"handle": "laurashin", "name": "Laura Shin", "followers": "350K", "pfp": "laura"}, {"handle": "nic__carter", "name": "Nic Carter", "followers": "400K", "pfp": "nic"}, {"handle": "ercwl", "name": "Eric Wall", "followers": "200K", "pfp": "ercwl"}, {"handle": "udiWertheimer", "name": "Udi Wertheimer", "followers": "250K", "pfp": "udi"}, {"handle": "PeterLBrandt", "name": "Peter Brandt", "followers": "700K", "pfp": "brandt"}, {"handle": "100trillionUSD", "name": "PlanB", "followers": "1.9M", "pfp": "planb"}, {"handle": "woonomic", "name": "Willy Woo", "followers": "1.1M", "pfp": "woo"}, {"handle": "CryptoCapo_", "name": "Crypto Capo", "followers": "900K", "pfp": "capo"}, {"handle": "CryptoCred", "name": "CryptoCred", "followers": "600K", "pfp": "cred"}, {"handle": "Pentosh1", "name": "Pentoshi", "followers": "850K", "pfp": "pentoshi"}, {"handle": "intocryptoverse", "name": "Benjamin Cowen", "followers": "900K", "pfp": "cowen"}, {"handle": "CryptoKaleo", "name": "Kaleo", "followers": "650K", "pfp": "kaleo"}, {"handle": "rektcapital", "name": "Rekt Capital", "followers": "500K", "pfp": "rektcap"}, {"handle": "CryptoMichNL", "name": "Micha�l van de Poppe", "followers": "750K", "pfp": "mich"}, {"handle": "CryptoJack", "name": "CryptoJack", "followers": "400K", "pfp": "cryptojack"}, {"handle": "AltcoinGordon", "name": "Altcoin Gordon", "followers": "550K", "pfp": "gordon"}, {"handle": "Ashcryptoreal", "name": "Ash Crypto", "followers": "1.1M", "pfp": "ash"}, {"handle": "Trader_XO", "name": "TraderXO", "followers": "380K", "pfp": "xo"}, {"handle": "CryptoWizardd", "name": "CryptoWizard", "followers": "350K", "pfp": "wizard"}, {"handle": "CryptosBatman", "name": "Batman", "followers": "300K", "pfp": "batman"}, {"handle": "ColdBloodShill", "name": "Cold Blooded Shiller", "followers": "280K", "pfp": "cbs"}, {"handle": "MacroCRG", "name": "MacroCRG", "followers": "250K", "pfp": "crg"}, {"handle": "WatcherGuru", "name": "Watcher.Guru", "followers": "2.1M", "pfp": "watcher"}, {"handle": "unusual_whales", "name": "Unusual Whales", "followers": "1.5M", "pfp": "whales"}, {"handle": "dbnewstweets", "name": "DB News", "followers": "750K", "pfp": "db"}, {"handle": "ZeroHedge", "name": "ZeroHedge", "followers": "1.8M", "pfp": "zh"}, { "handle": "SpectrumMarkets", "name": "Spectrum Markets", "followers": "120K", "pfp": "spectrum", }, {"handle": "TreeNews5", "name": "Tree News", "followers": "500K", "pfp": "tree"}, { "handle": "Crypto_Twitter", "name": "Crypto Twitter News", "followers": "450K", "pfp": "ctnews", }, {"handle": "DegenerateNews", "name": "Degenerate News", "followers": "380K", "pfp": "degen"}, {"handle": "NFT_GOD", "name": "NFT God", "followers": "280K", "pfp": "nftgod"}, {"handle": "CryptoKoryo", "name": "Koryo", "followers": "180K", "pfp": "koryo"}, {"handle": "OnchainData", "name": "Onchain Data Nerd", "followers": "120K", "pfp": "onchain"}, {"handle": "spl_brah", "name": "SPL Brah", "followers": "90K", "pfp": "spl"}, {"handle": "solana_daily", "name": "Solana Daily", "followers": "250K", "pfp": "soldaily"}, {"handle": "SolanaLegend", "name": "Solana Legend", "followers": "180K", "pfp": "sollegend"}, {"handle": "SolanaConf", "name": "Solana News", "followers": "150K", "pfp": "solconf"}, {"handle": "0xMert_", "name": "Mert", "followers": "350K", "pfp": "mert"}, {"handle": "0xTanishq", "name": "Tanishq", "followers": "130K", "pfp": "tanishq"}, {"handle": "Deebs_DeFi", "name": "Deebs DeFi", "followers": "160K", "pfp": "deebs"}, {"handle": "DeFi_Dad", "name": "DeFi Dad", "followers": "200K", "pfp": "defidad"}, {"handle": "DeFi_Ignas", "name": "Ignas DeFi", "followers": "220K", "pfp": "ignas"}, {"handle": "Crypto_GodJohn", "name": "Crypto God John", "followers": "280K", "pfp": "godjohn"}, {"handle": "CryptoNTez", "name": "Crypto NTez", "followers": "90K", "pfp": "ntez"}, {"handle": "0xSisyphus", "name": "Sisyphus", "followers": "110K", "pfp": "sisyphus"}, {"handle": "vydamo_", "name": "Vydamo", "followers": "85K", "pfp": "vydamo"}, {"handle": "0xKillWolf", "name": "KillWolf", "followers": "75K", "pfp": "killwolf"}, {"handle": "MoonOverlord", "name": "Moon Overlord", "followers": "95K", "pfp": "moon"}, {"handle": "CryptoAmb", "name": "Crypto Amber", "followers": "70K", "pfp": "amber"}, {"handle": "satsdart", "name": "SatsDart", "followers": "65K", "pfp": "sats"}, {"handle": "DeFi_Kamikaze", "name": "Kamikaze DeFi", "followers": "55K", "pfp": "kami"}, {"handle": "Crypto_Ninja", "name": "Crypto Ninja", "followers": "80K", "pfp": "ninja"}, {"handle": "0xWave_", "name": "0xWave", "followers": "60K", "pfp": "wave"}, {"handle": "Crypto_Chase", "name": "Chase Crypto", "followers": "45K", "pfp": "chase"}, {"handle": "AltCryptoGems", "name": "Altcoin Gems", "followers": "140K", "pfp": "gems"}, {"handle": "DeFi_Warhol", "name": "DeFi Warhol", "followers": "50K", "pfp": "warhol"}, {"handle": "Crypto_Link", "name": "Crypto Link", "followers": "70K", "pfp": "link"}, {"handle": "SolanaAlpha_", "name": "Solana Alpha", "followers": "55K", "pfp": "solalpha"}, {"handle": "DeFi_Prime", "name": "DeFi Prime", "followers": "40K", "pfp": "prime"}, {"handle": "Crypto_Oracle", "name": "Crypto Oracle", "followers": "65K", "pfp": "oracle"}, {"handle": "chain_news", "name": "Chain News", "followers": "85K", "pfp": "chain"}, {"handle": "Crypto_Banter", "name": "Crypto Banter", "followers": "220K", "pfp": "banter"}, {"handle": "LunarCRUSH", "name": "LunarCrush", "followers": "250K", "pfp": "lunar"}, {"handle": "SantimentFeed", "name": "Santiment", "followers": "140K", "pfp": "santi"}, {"handle": "CoinMarketCap", "name": "CoinMarketCap", "followers": "3.5M", "pfp": "cmc"}, {"handle": "CoinGecko", "name": "CoinGecko", "followers": "1.8M", "pfp": "gecko"}, {"handle": "MessariCrypto", "name": "Messari", "followers": "450K", "pfp": "messari"}, {"handle": "ArkhamIntel", "name": "Arkham", "followers": "700K", "pfp": "arkham"}, {"handle": "DuneAnalytics", "name": "Dune", "followers": "350K", "pfp": "dune"}, {"handle": "Nansen_ai", "name": "Nansen", "followers": "280K", "pfp": "nansen"}, {"handle": "Glassnode", "name": "Glassnode", "followers": "320K", "pfp": "glass"}, ] # Reddit crypto subreddits REDDIT_SUBS = [ "CryptoCurrency", "ethfinance", "solana", "bitcoin", "defi", "CryptoTrading", "CryptoMarkets", "ethereum", "CryptoTechnology", ] # Cache TTLs TTL_TWITTER = 300 # 5 min TTL_REDDIT = 600 # 10 min TTL_NITTER = 900 # 15 min (fallback, longer cache) _cache: dict[str, tuple] = {} def _cache_get(key: str) -> dict | None: entry = _cache.get(key) if entry: expiry, data = entry if time.monotonic() < expiry: return data del _cache[key] return None def _cache_set(key: str, data: dict, ttl: int): _cache[key] = (time.monotonic() + ttl, data) async def get_twitter_feed(limit: int = 30) -> dict: """Get top crypto tweets via Nitter (free, no auth, cached).""" cache_key = f"twitter_feed_{limit}" cached = _cache_get(cache_key) if cached: return cached tweets = [] async with httpx.AsyncClient(timeout=15) as c: for account in TOP_CRYPTO_ACCOUNTS[:30]: # Top 15 to minimize calls try: # Try Nitter (free, no auth) nitter_url = f"https://nitter.net/{account['handle']}/rss" r = await c.get(nitter_url) if r.status_code == 200: import xml.etree.ElementTree as ET root = ET.fromstring(r.text) for item in root.findall(".//item")[:3]: title = item.find("title").text if item.find("title") is not None else "" link = item.find("link").text if item.find("link") is not None else "" item.find("description").text if item.find("description") is not None else "" pubdate = item.find("pubDate").text if item.find("pubDate") is not None else "" if title and "RT @" not in title: tweets.append( { "id": hashlib.md5(link.encode()).hexdigest()[:12], "text": title.replace(f"{account['name']}: ", ""), "author": account["name"], "handle": account["handle"], "followers": account["followers"], "pfp": f"https://unavatar.io/twitter/{account['handle']}", "url": link, "published": pubdate, "source": "x", "type": "tweet", } ) except Exception: continue result = { "tweets": sorted(tweets, key=lambda t: t.get("published", ""), reverse=True)[:limit], "accounts_monitored": len(TOP_CRYPTO_ACCOUNTS), "new_accounts": "50 top news breakers + 49 underrated alpha accounts", "updated": datetime.now(UTC).isoformat(), } _cache_set(cache_key, result, TTL_NITTER if tweets else 60) return result async def get_reddit_feed(limit: int = 20) -> dict: """Get top crypto Reddit posts (free, no auth, cached).""" cache_key = f"reddit_feed_{limit}" cached = _cache_get(cache_key) if cached: return cached posts = [] async with httpx.AsyncClient(timeout=15) as c: for sub in REDDIT_SUBS[:4]: # Top 4 to minimize calls try: r = await c.get( f"https://www.reddit.com/r/{sub}/hot.json?limit=5", headers={"User-Agent": "RMI/1.0"}, ) if r.status_code == 200: data = r.json() for child in data.get("data", {}).get("children", []): d = child["data"] posts.append( { "id": d["id"], "title": d["title"], "author": d["author"], "subreddit": d["subreddit"], "score": d["score"], "num_comments": d["num_comments"], "url": f"https://reddit.com{d['permalink']}", "published": datetime.fromtimestamp(d["created_utc"], UTC).isoformat(), "source": "reddit", "type": "post", } ) except Exception: continue result = { "posts": sorted(posts, key=lambda p: p.get("score", 0), reverse=True)[:limit], "subreddits": REDDIT_SUBS, "updated": datetime.now(UTC).isoformat(), } _cache_set(cache_key, result, TTL_REDDIT) return result async def get_social_feed(limit_twitter: int = 30, limit_reddit: int = 20) -> dict: """Get combined social feed - X + Reddit, sorted by recency.""" twitter, reddit = await asyncio.gather( get_twitter_feed(limit_twitter), get_reddit_feed(limit_reddit), return_exceptions=True, ) twitter_data = twitter if not isinstance(twitter, Exception) else {"tweets": []} reddit_data = reddit if not isinstance(reddit, Exception) else {"posts": []} return { "x": twitter_data, "reddit": reddit_data, "total_social_sources": len(TOP_CRYPTO_ACCOUNTS) + len(REDDIT_SUBS), "updated": datetime.now(UTC).isoformat(), } def get_top_accounts() -> list: """Get the list of monitored X accounts with profile pics.""" return [ { "handle": a["handle"], "name": a["name"], "followers": a["followers"], "pfp": f"https://unavatar.io/twitter/{a['handle']}", } for a in TOP_CRYPTO_ACCOUNTS ]