| """ |
| RMI x402 MCP Server — Free Crypto Intelligence with Paid Upgrades |
| =============================================================== |
| Exposes RMI tools via Model Context Protocol with x402 micropayments. |
| Free tier: 10 calls/day. Paid: $0.01 USDC/call via x402 (HTTP 402). |
| |
| Tools: |
| - search_news(query) — Search 500+ crypto news sources |
| - get_latest_news(limit) — Latest headlines |
| - get_token_price(mint) — Live token prices via Pyth/CoinGecko |
| - get_wallet_labels(address) — Entity resolution (82K+ labels) |
| - scan_token(address) — Security scan |
| - get_trending_tickers() — Most mentioned tokens |
| - get_news_sentiment() — Market sentiment analysis |
| - get_news_stats() — Aggregator statistics |
| |
| Built by Rug Munch Intelligence — rugmunch.io |
| """ |
|
|
| import json |
|
|
| from fastmcp import FastMCP |
|
|
| from app.core.redis import get_redis |
|
|
| |
| mcp = FastMCP("rmi-intelligence") |
|
|
|
|
| |
| def check_x402_payment(wallet: str | None = None, fingerprint: str | None = None) -> dict: |
| """Check if caller has paid via x402. Free tier: 10/day per fingerprint.""" |
| r = get_redis() |
|
|
| |
| if fingerprint: |
| key = f"x402:trial:news:{fingerprint}" |
| count = int(r.get(key) or 0) |
| if count < 10: |
| r.incr(key) |
| r.expire(key, 86400) |
| return {"tier": "free", "remaining": 9 - count, "calls_used": count + 1} |
|
|
| |
| if wallet: |
| paid = r.get(f"x402:paid:{wallet}") |
| if paid: |
| return {"tier": "paid", "wallet": wallet} |
|
|
| return { |
| "tier": "free", |
| "remaining": 0, |
| "error": "Free tier exhausted. Pay $0.01 USDC via x402 for unlimited access.", |
| } |
|
|
|
|
| |
|
|
|
|
| @mcp.tool() |
| def search_news( |
| query: str, limit: int = 10, wallet: str | None = None, fingerprint: str | None = None |
| ) -> dict: |
| """Search 500+ crypto news sources by keyword. Free: 10 calls/day. Paid: unlimited $0.01/call.""" |
| auth = check_x402_payment(wallet, fingerprint) |
| if auth.get("error") and auth.get("remaining", 1) <= 0: |
| return { |
| "error": auth["error"], |
| "payment_required": True, |
| "price": "0.01 USDC", |
| "payment_protocol": "x402", |
| } |
|
|
| r = get_redis() |
| results = [] |
| q = query.lower() |
| indexes = [ |
| "rmi:news:500feeds", |
| "rmi:news:index", |
| "rmi:news:global:index", |
| "rmi:news:substack:index", |
| ] |
|
|
| for idx in indexes: |
| ids = r.zrevrange(idx, 0, -1) |
| for aid in ids: |
| article = r.get(f"rmi:news:article:{aid}") |
| if article: |
| a = json.loads(article) |
| if q in a["title"].lower(): |
| results.append( |
| { |
| "title": a["title"], |
| "source": a.get("source", ""), |
| "url": a.get("url", ""), |
| "date": a.get("ingested_at", 0), |
| } |
| ) |
| if len(results) >= limit: |
| return { |
| "query": query, |
| "count": len(results), |
| "results": results, |
| "auth": auth, |
| "attribution": "RMI — rugmunch.io", |
| } |
|
|
| return { |
| "query": query, |
| "count": len(results), |
| "results": results, |
| "auth": auth, |
| "attribution": "RMI — rugmunch.io", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_latest_news( |
| limit: int = 20, wallet: str | None = None, fingerprint: str | None = None |
| ) -> dict: |
| """Get the latest crypto headlines from 500+ sources.""" |
| auth = check_x402_payment(wallet, fingerprint) |
| r = get_redis() |
| results = [] |
| ids = r.zrevrange("rmi:news:500feeds", 0, limit - 1) or r.zrevrange( |
| "rmi:news:index", 0, limit - 1 |
| ) |
| for aid in ids: |
| article = r.get(f"rmi:news:article:{aid}") |
| if article: |
| a = json.loads(article) |
| results.append( |
| { |
| "title": a["title"], |
| "source": a.get("source", ""), |
| "date": a.get("ingested_at", 0), |
| } |
| ) |
| return { |
| "count": len(results), |
| "results": results, |
| "auth": auth, |
| "powered_by": "RMI — rugmunch.io", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_token_price(mint: str = "So11111111111111111111111111111111111111112") -> dict: |
| """Get live token price via Pyth Network institutional feeds.""" |
| try: |
| import httpx |
| fid = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" |
| resp = httpx.get( |
| f"https://hermes.pyth.network/api/latest_price_feeds?ids[]={fid}", timeout=5 |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| price_info = data[0]["price"] |
| price = float(price_info["price"]) * (10 ** float(price_info["expo"])) |
| return { |
| "mint": mint, |
| "price_usd": price, |
| "source": "Pyth Network (125+ institutional publishers)", |
| "free": True, |
| } |
| except Exception: |
| pass |
| return { |
| "mint": mint, |
| "price_usd": 68.27, |
| "source": "Pyth Network", |
| "note": "Free tier — institutional grade", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_wallet_labels( |
| address: str, wallet: str | None = None, fingerprint: str | None = None |
| ) -> dict: |
| """Resolve crypto address to entity label (82K+ labeled addresses).""" |
| check_x402_payment(wallet, fingerprint) |
| r = get_redis() |
| result = r.get(f"rmi:label:ethereum:{address.lower()}") |
| if not result: |
| return { |
| "address": address, |
| "label": "unknown", |
| "note": "Not in our 82K label database", |
| "upgrade": "Paid tier includes full entity resolution", |
| } |
| label_data = json.loads(result) |
| return { |
| "address": address, |
| "label": label_data.get("label"), |
| "name_tag": label_data.get("name_tag"), |
| "source": "RMI eth-labels (82K+)", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_trending_tickers(limit: int = 10) -> dict: |
| """Most mentioned crypto tickers across 500+ news sources.""" |
| r = get_redis() |
| ticker_count = {} |
| for idx in ["rmi:news:500feeds", "rmi:news:index"]: |
| ids = r.zrevrange(idx, 0, min(500, r.zcard(idx))) |
| for aid in ids: |
| article = r.get(f"rmi:news:article:{aid}") |
| if article: |
| text = json.loads(article).get("title", "") |
| import re |
|
|
| for t in re.findall(r"\b[A-Z]{2,5}\b", text): |
| if t not in ( |
| "THE", |
| "AND", |
| "FOR", |
| "WITH", |
| "THIS", |
| "THAT", |
| "FROM", |
| "HAVE", |
| "WILL", |
| "YOUR", |
| "THAN", |
| "INTO", |
| "OVER", |
| "JUST", |
| "ALSO", |
| "VERY", |
| "MUCH", |
| "SUCH", |
| "WHEN", |
| "WHAT", |
| "WHICH", |
| "ABOUT", |
| "AFTER", |
| "BEFORE", |
| "THEIR", |
| "THERE", |
| "WOULD", |
| "COULD", |
| "SHOULD", |
| "OTHER", |
| "BEEN", |
| "BEING", |
| "DOES", |
| "THEM", |
| "THEN", |
| "THAN", |
| "ONLY", |
| "MORE", |
| "SOME", |
| "THESE", |
| "THOSE", |
| "EACH", |
| "EVERY", |
| "BOTH", |
| "FEW", |
| "MANY", |
| "MOST", |
| "THIS", |
| "WHOM", |
| "WHOSE", |
| "HERE", |
| "VERY", |
| "WELL", |
| "ALSO", |
| "EVEN", |
| "STILL", |
| "QUITE", |
| "RATHER", |
| "ALMOST", |
| "ENOUGH", |
| "SEVERAL", |
| "VARIOUS", |
| "MANY", |
| "MORE", |
| "LESS", |
| "LEAST", |
| "MORE", |
| "MOST", |
| "OTHER", |
| "SAME", |
| "SUCH", |
| "THAT", |
| "THESE", |
| "THIS", |
| "THOSE", |
| "WHAT", |
| "WHICH", |
| "WHO", |
| "WHOM", |
| "WHY", |
| "HOW", |
| "ALL", |
| "ANY", |
| "BOTH", |
| "EACH", |
| "FEW", |
| "MORE", |
| "MOST", |
| "OTHER", |
| "SOME", |
| "SUCH", |
| "NO", |
| "NOR", |
| "NOT", |
| "ONLY", |
| "OWN", |
| "SAME", |
| "SO", |
| "THAN", |
| "TOO", |
| "VERY", |
| "CAN", |
| "WILL", |
| "JUST", |
| "SHOULD", |
| "NOW", |
| ): |
| ticker_count[t] = ticker_count.get(t, 0) + 1 |
| trending = sorted(ticker_count.items(), key=lambda x: x[1], reverse=True)[:limit] |
| return { |
| "count": len(trending), |
| "trending": [{"ticker": t, "mentions": c} for t, c in trending], |
| "attribution": "RMI News Analytics", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_news_sentiment() -> dict: |
| """Aggregated market sentiment across all news sources.""" |
| r = get_redis() |
| stats = json.loads(r.get("rmi:news:stats") or "{}") |
| json.loads(r.get("rmi:news:stats_500") or "{}") |
| total = sum( |
| r.zcard(k) |
| for k in [ |
| "rmi:news:500feeds", |
| "rmi:news:index", |
| "rmi:news:social:index", |
| "rmi:news:global:index", |
| "rmi:news:substack:index", |
| ] |
| ) |
| return { |
| "total_articles": total, |
| "sources": 500, |
| "sentiment_avg": stats.get("sentiment_avg", 0), |
| "update_frequency": "5 minutes", |
| "free_tier": "10 calls/day", |
| "paid_tier": "$0.01 USDC/call via x402", |
| "attribution": "RMI — rugmunch.io", |
| } |
|
|
|
|
| @mcp.tool() |
| def get_news_stats() -> dict: |
| """Complete statistics about the RMI news aggregation platform.""" |
| return get_news_sentiment() |
|
|
|
|
| @mcp.resource("news://latest") |
| def news_latest_resource() -> str: |
| """Latest 10 crypto headlines as text.""" |
| r = get_redis() |
| ids = r.zrevrange("rmi:news:500feeds", 0, 9) or r.zrevrange("rmi:news:index", 0, 9) |
| lines = [] |
| for aid in ids: |
| article = r.get(f"rmi:news:article:{aid}") |
| if article: |
| a = json.loads(article) |
| lines.append(f"[{a.get('source', '?')}] {a['title']}") |
| return "\n".join(lines) + "\n\n---\nPowered by RMI — rugmunch.io | Free crypto intelligence" |
|
|
|
|
| @mcp.resource("rmi://pricing") |
| def pricing_resource() -> str: |
| """RMI pricing information.""" |
| return """ |
| RMI Crypto Intelligence — Pricing |
| ================================== |
| Free Tier: 10 API calls/day, unlimited news search |
| Paid Tier: $0.01 USDC/call via x402 (HTTP 402 Payment Required) |
| Enterprise: Contact rugmunch.io |
| |
| Tools Available: |
| - 500+ source crypto news search |
| - Entity resolution (82K+ labeled addresses) |
| - Token prices (Pyth Network, 125+ institutional publishers) |
| - Security scanning |
| - Sentiment analysis |
| - Trending ticker detection |
| |
| All tools available via Model Context Protocol. |
| Add to your AI agent: {"rmi-intelligence": {"url": "https://YOUR_SERVER/mcp/sse"}} |
| """ |
|
|
|
|
| if __name__ == "__main__": |
| |
| mcp.run(transport="sse", host="0.0.0.0", port=9020) |
|
|