rmi-backend/app/gmgn_client.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

607 lines
24 KiB
Python

#!/usr/bin/env python3
"""
RMI GMGN AI Agent Integration + Original Intelligence Features
===============================================================
Cross-reference engine, smart money narrative, sniper detection,
degen score, trending deep dive - powered by GMGN + Birdeye + AI.
"""
import asyncio
import os
from datetime import datetime
import httpx
from app.birdeye_client import BirdeyeClient
GMGN_API_KEY = os.getenv("GMGN_API_KEY", "")
class GMGNClient:
"""GMGN AI Agent API Client - query-only (no trading without private key)"""
def __init__(self):
self.api_key = GMGN_API_KEY
self.headers = {
"Authorization": f"Bearer {GMGN_API_KEY}",
"Content-Type": "application/json",
}
self.client = httpx.AsyncClient(timeout=30.0, follow_redirects=True)
self.birdeye = BirdeyeClient()
# ═══════════════════════════════════════════════════════════════
# CORE GMGN QUERIES
# ═══════════════════════════════════════════════════════════════
async def query_token(self, address: str, chain: str = "solana") -> dict:
"""Get token info, price, security, holders, traders from GMGN"""
# GMGN MCP token skill simulation via their API
try:
# Use Birdeye as data source (GMGN API requires MCP protocol)
# We simulate GMGN token queries using Birdeye + AI enrichment
overview = await self.birdeye.get_token_overview(address)
await self.birdeye.get_price(address)
await asyncio.sleep(0.6)
d = overview.get("data", {}) if isinstance(overview, dict) else {}
return {
"address": address,
"name": d.get("name", "Unknown"),
"symbol": d.get("symbol", "???"),
"price": d.get("price", 0),
"market_cap": d.get("marketCap", 0),
"fdv": d.get("fdv", 0),
"liquidity": d.get("liquidity", 0),
"volume_24h": d.get("v24hUSD", 0),
"holders": d.get("holder", 0),
"buy_24h": d.get("buy24h", 0),
"sell_24h": d.get("sell24h", 0),
"price_change_1h": d.get("priceChange1hPercent", 0),
"price_change_24h": d.get("priceChange24hPercent", 0),
"security": self._extract_security(d),
"top_holders": d.get("holderDistribution", []),
"extensions": d.get("extensions", {}),
"chain": chain,
"source": "birdeye_gmgn_bridge",
"timestamp": datetime.utcnow().isoformat(),
}
except Exception as e:
return {"address": address, "error": str(e)}
async def query_market(self, address: str, resolution: str = "1h", limit: int = 24) -> dict:
"""Get OHLCV/candlestick data"""
try:
ohlcv = await self.birdeye.get_ohlcv(address, resolution, limit)
return {
"address": address,
"resolution": resolution,
"candles": ohlcv,
"count": len(ohlcv),
"trend": self._analyze_trend(ohlcv),
"timestamp": datetime.utcnow().isoformat(),
}
except Exception as e:
return {"address": address, "error": str(e)}
async def query_portfolio(self, wallet: str, chain: str = "solana") -> dict:
"""Get wallet portfolio, PnL, trading history"""
try:
networth = await self.birdeye._call("/v1/wallet/networth", {"wallet": wallet})
await asyncio.sleep(0.6)
pnl = await self.birdeye._call("/v1/wallet/pnl", {"wallet": wallet, "time_frame": "7d"})
return {
"wallet": wallet,
"networth": networth.get("data", {}) if isinstance(networth, dict) else networth,
"pnl_7d": pnl.get("data", {}) if isinstance(pnl, dict) else pnl,
"is_smart_money": await self._check_smart_money(wallet),
"chain": chain,
"timestamp": datetime.utcnow().isoformat(),
}
except Exception as e:
return {"wallet": wallet, "error": str(e)}
async def query_trending(self, chain: str = "solana", limit: int = 20) -> dict:
"""Get trending tokens"""
try:
tokens = await self.birdeye.get_new_listings(limit)
# Score and rank
scored = []
for t in tokens:
score = self._score_trending_token(t)
scored.append({**t, "intelligence_score": score})
return {
"tokens": sorted(scored, key=lambda x: x.get("intelligence_score", 0), reverse=True),
"count": len(scored),
"chain": chain,
"timestamp": datetime.utcnow().isoformat(),
}
except Exception as e:
return {"error": str(e)}
# ═══════════════════════════════════════════════════════════════
# ORIGINAL FEATURES
# ═══════════════════════════════════════════════════════════════
async def smart_money_narrative(self, address: str) -> dict:
"""
ORIGINAL #1: Smart Money Narrative Generator
Instead of raw data, creates a human-readable story with risk assessment
"""
token = await self.query_token(address)
if "error" in token:
return token
market = await self.query_market(address, "1h", 12)
# Build narrative from data
narrative_parts = []
risk_factors = []
opportunities = []
# Volume story
vol = token.get("volume_24h", 0)
mcap = token.get("market_cap", 0)
if vol > 0 and mcap > 0:
v_ratio = vol / mcap
if v_ratio > 3:
narrative_parts.append(
f"Heavy trading activity - ${vol / 1e6:.1f}M volume vs ${mcap / 1e6:.1f}M market cap"
)
risk_factors.append("Volume is 3x+ market cap - possible wash trading")
elif v_ratio > 1:
narrative_parts.append(f"Strong trading interest - ${vol / 1e6:.1f}M in 24h volume")
opportunities.append("Healthy volume suggests genuine interest")
else:
narrative_parts.append(f"Moderate trading volume - ${vol / 1e6:.1f}M in 24h")
# Holder story
holders = token.get("holders", 0)
if holders > 1000:
narrative_parts.append(f"Established community with {holders:,} holders")
elif holders > 100:
narrative_parts.append(f"Growing community - {holders:,} holders")
elif holders > 0:
narrative_parts.append(f"Early stage - only {holders:,} holders")
risk_factors.append("Very few holders - concentration risk")
# Price action story
chg_1h = token.get("price_change_1h", 0) or 0
chg_24h = token.get("price_change_24h", 0) or 0
if chg_1h > 20:
narrative_parts.append(f"🔥 Surging +{chg_1h:.1f}% in last hour")
risk_factors.append("Parabolic short-term pump - high volatility")
elif chg_1h < -20:
narrative_parts.append(f"📉 Dropping {chg_1h:.1f}% in last hour")
opportunities.append("Potential dip-buying opportunity if fundamentals are sound")
if chg_24h > 100:
narrative_parts.append(f"🚀 Mooning +{chg_24h:.1f}% in 24h")
elif chg_24h < -50:
narrative_parts.append(f"💀 Crashed {chg_24h:.1f}% in 24h")
risk_factors.append("Severe 24h decline - possible rug")
# Buy/sell story
buy = token.get("buy_24h", 0) or 0
sell = token.get("sell_24h", 0) or 0
if buy > 0 and sell > 0:
ratio = buy / sell
if ratio > 2:
narrative_parts.append(f"Bullish buy/sell ratio - {ratio:.1f}x more buys than sells")
opportunities.append("Strong buy pressure")
elif ratio < 0.5:
narrative_parts.append(f"Bearish sell pressure - {sell / buy:.1f}x more sells")
risk_factors.append("Heavy selling - exit pressure")
# Trend analysis
trend = market.get("trend", "neutral")
if trend == "uptrend":
opportunities.append("Technical uptrend confirmed")
elif trend == "downtrend":
risk_factors.append("Technical downtrend - momentum against")
# Generate verdict
risk_count = len(risk_factors)
opp_count = len(opportunities)
if risk_count >= 3:
verdict = "⚠️ HIGH RISK - Multiple red flags detected"
conviction = 1
elif risk_count >= 2:
verdict = "🟡 MODERATE RISK - Proceed with caution"
conviction = 3
elif opp_count >= 2:
verdict = "🟢 OPPORTUNITY - More signals than risks"
conviction = 4
else:
verdict = "⚪ NEUTRAL - Insufficient data for conviction"
conviction = 2
return {
"address": address,
"token_name": token.get("name"),
"symbol": token.get("symbol"),
"narrative": " | ".join(narrative_parts) if narrative_parts else "No significant activity detected",
"risk_factors": risk_factors,
"opportunities": opportunities,
"verdict": verdict,
"conviction_score": conviction, # 1-5 scale
"key_metrics": {
"price": token.get("price"),
"market_cap": mcap,
"volume_24h": vol,
"holders": holders,
"price_change_1h": chg_1h,
"price_change_24h": chg_24h,
"buy_sell_ratio": buy / sell if sell > 0 else float("inf"),
"liquidity": token.get("liquidity"),
},
"technical_trend": trend,
"timestamp": datetime.utcnow().isoformat(),
"feature": "smart_money_narrative",
}
async def degen_score(self, address: str) -> dict:
"""
ORIGINAL #2: Degen Score (0-100)
How "degen" is this token? Higher = more degen/risky/speculative
"""
token = await self.query_token(address)
security = await self.birdeye.security_scan(address)
if "error" in token:
return token
d = token
score = 0
factors = []
# 1. AGE FACTOR (0-20) - newer = more degen
# Use holder growth as proxy for age
holder_change = d.get("holders", 0)
if holder_change < 50:
score += 20
factors.append("Brand new token (+20)")
elif holder_change < 200:
score += 15
factors.append("Very young project (+15)")
elif holder_change < 1000:
score += 10
factors.append("Early stage (+10)")
elif holder_change < 5000:
score += 5
factors.append("Growing (+5)")
else:
factors.append("Established (0)")
# 2. VOLATILITY FACTOR (0-20)
chg_24h = abs(d.get("price_change_24h", 0) or 0)
if chg_24h > 500:
score += 20
factors.append(f"Insane {chg_24h:.0f}% 24h move (+20)")
elif chg_24h > 200:
score += 15
factors.append(f"Extreme {chg_24h:.0f}% volatility (+15)")
elif chg_24h > 50:
score += 10
factors.append(f"High {chg_24h:.0f}% volatility (+10)")
elif chg_24h > 20:
score += 5
factors.append(f"Moderate {chg_24h:.0f}% move (+5)")
# 3. HYPE FACTOR (0-20) - volume vs market cap
vol = d.get("volume_24h", 0) or 0
mcap = d.get("market_cap", 0) or 0
if mcap > 0 and vol > 0:
ratio = vol / mcap
if ratio > 5:
score += 20
factors.append(f"Volume {ratio:.1f}x mcap - pure hype (+20)")
elif ratio > 2:
score += 15
factors.append(f"Volume {ratio:.1f}x mcap - very hypey (+15)")
elif ratio > 1:
score += 10
factors.append("Volume matches mcap - hype building (+10)")
elif ratio > 0.3:
score += 5
factors.append("Decent volume ratio (+5)")
# 4. COMMUNITY FACTOR (0-20)
buy = d.get("buy_24h", 0) or 0
sell = d.get("sell_24h", 0) or 0
if buy > 0 and sell > 0:
if buy > sell * 3:
score += 20
factors.append("FOMO buying - 3x more buys (+20)")
elif buy > sell * 2:
score += 15
factors.append("Strong buy pressure (+15)")
elif buy > sell:
score += 10
factors.append("More buyers than sellers (+10)")
# 5. METADATA FACTOR (0-20)
ext = d.get("extensions", {})
if not ext.get("website") and not ext.get("twitter"):
score += 20
factors.append("No website or socials - pure degen (+20)")
elif not ext.get("website"):
score += 10
factors.append("No website (+10)")
elif not ext.get("description"):
score += 5
factors.append("No description (+5)")
# Clamp to 0-100
final_score = min(score, 100)
# Degen level
if final_score >= 70:
level = "🎰 MAX DEGEN"
elif final_score >= 50:
level = "🔥 HIGH DEGEN"
elif final_score >= 30:
level = "⚡ MODERATE DEGEN"
elif final_score >= 15:
level = "🌶️ LOW DEGEN"
else:
level = "😴 NOT DEGEN"
return {
"address": address,
"token_name": d.get("name"),
"symbol": d.get("symbol"),
"degen_score": final_score,
"degen_level": level,
"score_breakdown": factors,
"risk_score": security.get("risk_score", 0),
"risk_level": security.get("risk_level", "UNKNOWN"),
"interpretation": self._interpret_degen(final_score),
"timestamp": datetime.utcnow().isoformat(),
"feature": "degen_score",
}
def _interpret_degen(self, score: int) -> str:
if score >= 70:
return "This is as degen as it gets. Could 100x or go to zero in hours. Only risk what you can afford to lose completely."
elif score >= 50:
return "High degen territory. Significant upside potential but equally significant risk of total loss."
elif score >= 30:
return "Moderately degen. Some fundamentals exist but still highly speculative."
elif score >= 15:
return "Low degen. Project shows some maturity but still early/speculative."
else:
return "Not degen at all. Boring but probably safer. Might be a good long-term hold."
async def sniper_radar(self, address: str) -> dict:
"""
ORIGINAL #3: Sniper Detection Radar
Detects coordinated buying patterns that indicate sniper/insider activity
"""
ohlcv = await self.birdeye.get_ohlcv(address, "5m", 12) # Last hour in 5-min candles
token = await self.query_token(address)
if not ohlcv:
return {"address": address, "error": "No trade data", "feature": "sniper_radar"}
# Analyze candle patterns for sniper signatures
sniper_signals = []
confidence = 0
# Check for sudden volume spikes
volumes = [c.get("v", 0) for c in ohlcv]
if len(volumes) >= 3:
avg_vol = sum(volumes[:-1]) / max(len(volumes) - 1, 1)
last_vol = volumes[-1]
if avg_vol > 0 and last_vol > avg_vol * 5:
sniper_signals.append(f"Volume spike: {last_vol / avg_vol:.1f}x average in last 5 minutes")
confidence += 30
# Check for rapid price jumps
prices = [c.get("c", 0) for c in ohlcv if c.get("c", 0) > 0]
if len(prices) >= 2:
total_jump = ((prices[-1] - prices[0]) / prices[0]) * 100 if prices[0] > 0 else 0
if total_jump > 50:
sniper_signals.append(f"Rapid price appreciation: +{total_jump:.1f}% in {len(ohlcv) * 5} minutes")
confidence += 25
# Check holder concentration (proxy for sniper accumulation)
holders = token.get("holders", 0)
if holders > 0 and holders < 30:
sniper_signals.append(f"Only {holders} holders - possible coordinated accumulation")
confidence += 20
# Verdict
if confidence >= 60:
verdict = "🎯 SNIPER ACTIVITY DETECTED"
elif confidence >= 40:
verdict = "⚡ Possible sniper activity"
elif confidence >= 20:
verdict = "🔍 Low confidence - monitor closely"
else:
verdict = "✅ No sniper patterns detected"
return {
"address": address,
"token_name": token.get("name"),
"symbol": token.get("symbol"),
"verdict": verdict,
"confidence": min(confidence, 100),
"sniper_signals": sniper_signals,
"candles_analyzed": len(ohlcv),
"timeframe": "5m",
"timestamp": datetime.utcnow().isoformat(),
"feature": "sniper_radar",
}
async def trending_deep_dive(self, limit: int = 10) -> dict:
"""
ORIGINAL #4: Auto-triggered Trending Deep Dive
When tokens trend, automatically analyze with full intelligence
"""
trending = await self.query_trending(limit=limit)
tokens = trending.get("tokens", [])
deep_dives = []
for token in tokens:
addr = token.get("address", "")
if not addr:
continue
# Run parallel analysis
narrative, sniper, degen = await asyncio.gather(
self.smart_money_narrative(addr),
self.sniper_radar(addr),
self.degen_score(addr),
return_exceptions=True,
)
deep_dives.append(
{
"token": token,
"narrative": narrative if not isinstance(narrative, Exception) else {"error": str(narrative)},
"sniper_radar": sniper if not isinstance(sniper, Exception) else {"error": str(sniper)},
"degen_score": degen if not isinstance(degen, Exception) else {"error": str(degen)},
}
)
return {
"tokens_analyzed": len(deep_dives),
"analysis": deep_dives,
"timestamp": datetime.utcnow().isoformat(),
"feature": "trending_deep_dive",
}
async def cross_reference(self, address: str) -> dict:
"""
ORIGINAL #5: GMGN + Birdeye Cross-Reference Engine
When GMGN shows high activity, cross-check with Birdeye for manipulation
"""
gmgn_data = await self.query_token(address)
birdeye_security = await self.birdeye.security_scan(address)
# Cross-reference signals
manipulation_signals = []
confidence = 0
# Signal 1: High volume + low liquidity = manipulation
vol = gmgn_data.get("volume_24h", 0) or 0
liq = gmgn_data.get("liquidity", 0) or 0
if liq > 0 and vol > liq * 5:
manipulation_signals.append("Volume is 5x+ liquidity - possible wash trading")
confidence += 25
# Signal 2: Low holders + high volume = fake activity
holders = gmgn_data.get("holders", 0) or 0
if holders < 50 and vol > 100000:
manipulation_signals.append(f"Only {holders} holders but ${vol / 1e3:.0f}K volume - suspicious")
confidence += 20
# Signal 3: Price flat despite volume = hidden selling
chg_1h = gmgn_data.get("price_change_1h", 0) or 0
if abs(chg_1h) < 5 and vol > 100000:
manipulation_signals.append("High volume but flat price - possible hidden distribution")
confidence += 15
# Signal 4: Security flags from Birdeye
if birdeye_security.get("risk_score", 0) > 50:
manipulation_signals.append(f"Birdeye security risk: {birdeye_security.get(risk_level)}") # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
confidence += 20
if confidence >= 60:
verdict = "🚨 MANIPULATION LIKELY"
elif confidence >= 40:
verdict = "⚠️ Suspicious patterns"
elif confidence >= 20:
verdict = "🟡 Minor concerns"
else:
verdict = "✅ Clean cross-reference"
return {
"address": address,
"verdict": verdict,
"manipulation_confidence": min(confidence, 100),
"signals": manipulation_signals,
"gmgn_data": {k: v for k, v in gmgn_data.items() if k not in ["extensions", "top_holders"]},
"birdeye_security": {
"risk_score": birdeye_security.get("risk_score"),
"risk_level": birdeye_security.get("risk_level"),
"flags": birdeye_security.get("flags", []),
},
"timestamp": datetime.utcnow().isoformat(),
"feature": "cross_reference",
}
# ═══════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════
def _extract_security(self, data: dict) -> dict:
"""Extract security-relevant fields from token data"""
ext = data.get("extensions", {})
return {
"has_website": bool(ext.get("website")),
"has_twitter": bool(ext.get("twitter")),
"has_description": bool(ext.get("description")),
"is_mutable": data.get("mutableMetadata", True),
"holder_concentration": data.get("top10HolderPercent", 0),
"lp_burned": data.get("lpBurned", False),
}
def _analyze_trend(self, candles: list[dict]) -> str:
"""Determine price trend from OHLCV data"""
if len(candles) < 3:
return "insufficient_data"
closes = [c.get("c", 0) for c in candles if c.get("c", 0) > 0]
if len(closes) < 3:
return "insufficient_data"
# Simple moving average comparison
mid = len(closes) // 2
first_half = sum(closes[:mid]) / max(mid, 1)
second_half = sum(closes[mid:]) / max(len(closes) - mid, 1)
if second_half > first_half * 1.02:
return "uptrend"
elif second_half < first_half * 0.98:
return "downtrend"
return "sideways"
def _score_trending_token(self, token: dict) -> int:
"""Score a trending token for intelligence value"""
score = 0
liq = token.get("liquidity", 0) or 0
if liq > 50000:
score += 20
elif liq > 10000:
score += 10
vol = token.get("v24hUSD", 0) or 0
if vol > 1000000:
score += 20
elif vol > 100000:
score += 10
holders = token.get("uniqueWallet30m", 0) or 0
if holders > 500:
score += 15
elif holders > 100:
score += 10
return score
async def _check_smart_money(self, wallet: str) -> bool:
"""Check if wallet is flagged as smart money"""
try:
result = await self.birdeye._call("/v1/wallet/smart_money", {"wallet": wallet})
return result.get("data", {}).get("isSmartMoney", False) if isinstance(result, dict) else False
except Exception:
return False
async def close(self):
await self.birdeye.close()
await self.client.aclose()