rmi-backend/app/routers/x402_alpha_tools.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

853 lines
37 KiB
Python

"""
RMI Alpha Tools - the signals that make us best-in-class.
Composite Score - one number combining all RMI signals for instant decisions
Smart Money Tracker - P&L-based profitable wallet identification
Token Clone Detector - bytecode + metadata similarity to known rugs
Wash Trading & Insider Detection - artificial volume and coordinated buying patterns
"""
import hashlib
import json
import logging
import os
import time
from datetime import datetime
import aiohttp
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
logger = logging.getLogger("x402.alpha")
router = APIRouter()
# ═══════════════════════════════════════════════════════════
# Redis helpers
# ═══════════════════════════════════════════════════════════
def _r():
import redis as _redis
return _redis.Redis(
host=os.getenv("REDIS_HOST", "rmi-redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2,
)
def _cache_get(tool, params):
try:
k = f"x402:cache:{tool}:{hashlib.sha256(json.dumps(params, sort_keys=True).encode()).hexdigest()[:16]}"
d = _r().get(k)
return json.loads(d) if d else None
except Exception:
return None
def _cache_set(tool, params, result, ttl=60):
try:
k = f"x402:cache:{tool}:{hashlib.sha256(json.dumps(params, sort_keys=True).encode()).hexdigest()[:16]}"
_r().setex(k, ttl, json.dumps(result))
except Exception:
pass
# ═══════════════════════════════════════════════════════════
# Models
# ═══════════════════════════════════════════════════════════
class TokenRequest(BaseModel):
address: str = Field(..., description="Token contract address")
chain: str = Field(default="base")
class WalletRequest(BaseModel):
address: str = Field(..., description="Wallet address")
chain: str = Field(default="ethereum")
class CloneRequest(BaseModel):
address: str = Field(..., description="Token to check for clones")
chain: str = Field(default="base")
class WashTradeRequest(BaseModel):
address: str = Field(..., description="Token address")
chain: str = Field(default="base")
lookback_hours: int = Field(default=24, ge=1, le=72)
# ═══════════════════════════════════════════════════════════
# TOOL: RMI Composite Score ($0.25)
# ═══════════════════════════════════════════════════════════
@router.post("/composite_score")
async def composite_score(req: TokenRequest):
"""The one number to rule them all. Combines every RMI signal into a 0-100
composite score for instant buy/sell/avoid decisions.
Components weighted by predictive power:
- Reputation Score (25%): trust signals, labels, scam flags
- Rug Probability (25%): honeypot, liquidity, deployer patterns
- Market Health (15%): volume/liquidity ratio, age, holder concentration
- Narrative Sentiment (15%): social media sentiment + shill detection
- MEV Exposure (10%): sandwich/frontrunning risk
- DeFi Position (10%): impermanent loss, protocol risk
Returns: composite_score (0-100), verdict (BUY/HOLD/CAUTION/AVOID),
component breakdown with individual scores.
"""
try:
addr = req.address.strip()
chain = req.chain or "base"
t0 = time.time()
cached = _cache_get("composite_score", {"address": addr, "chain": chain})
if cached:
return cached
components = {}
sources = []
warnings = []
# ── 1. Reputation Score (25%) ──
try:
async with aiohttp.ClientSession() as s, s.post(
"http://localhost:8000/api/v1/x402-tools/reputation_score",
json={"address": addr, "chain": chain},
timeout=aiohttp.ClientTimeout(total=20),
) as r:
if r.status == 200:
rep = await r.json()
components["reputation"] = {
"score": rep.get("trust_score", 50),
"tier": rep.get("tier", "UNKNOWN"),
"flags": len(rep.get("flags", [])),
}
if rep.get("trust_score", 100) < 40:
warnings.append(f"Reputation CRITICAL: {rep.get('tier')}")
sources.extend(rep.get("sources_used", []))
except Exception:
components["reputation"] = {"score": 50, "tier": "ERROR", "flags": 0}
# ── 2. Rug Probability (25%) ──
try:
async with aiohttp.ClientSession() as s, s.post(
"http://localhost:8000/api/v1/x402-tools/rug_probability",
json={"address": addr, "chain": chain},
timeout=aiohttp.ClientTimeout(total=20),
) as r:
if r.status == 200:
rug = await r.json()
components["rug_risk"] = {
"score": 100 - rug.get("rug_probability", 0), # Invert: high prob = low score
"probability": rug.get("rug_probability", 0),
"signals": rug.get("signal_count", 0),
}
if rug.get("rug_probability", 0) > 50:
warnings.append(f"Rug risk HIGH: {rug.get('rug_probability')}% probability")
sources.extend(rug.get("sources_used", []))
except Exception:
components["rug_risk"] = {"score": 50, "probability": 0, "signals": 0}
# ── 3. Market Health (15%) ──
try:
async with aiohttp.ClientSession() as s, s.get(
f"https://api.dexscreener.com/latest/dex/tokens/{addr}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
data = await r.json()
pairs = data.get("pairs", [])
if pairs:
sources.append("dexscreener")
p = pairs[0]
liq = p.get("liquidity", {}).get("usd", 0) or 0
vol = p.get("volume", {}).get("h24", 0) or 0
age_ms = p.get("pairCreatedAt", 0) or 0
age_h = (time.time() * 1000 - age_ms) / 3600000 if age_ms else 0
pc = p.get("priceChange", {}).get("h24", 0) or 0
health_score = 70
if liq < 1000:
health_score -= 40
elif liq < 10000:
health_score -= 20
elif liq > 500000:
health_score += 10
if 0 < age_h < 1:
health_score -= 25
elif 0 < age_h < 24:
health_score -= 10
elif age_h > 720:
health_score += 10
if liq > 0 and vol > liq * 5:
health_score -= 15
if pc < -30:
health_score -= 15
components["market_health"] = {
"score": max(0, min(100, health_score)),
"liquidity_usd": liq,
"age_hours": round(age_h, 1),
"volume_24h": vol,
"volume_liquidity_ratio": round(vol / max(liq, 1), 1),
}
except Exception:
components["market_health"] = {"score": 50, "liquidity_usd": 0, "age_hours": 0}
# ── 4. Narrative Sentiment (15%) ──
try:
async with aiohttp.ClientSession() as s:
symbol = addr[:12]
from urllib.parse import quote
async with s.get(
f"https://cryptopanic.com/api/free/posts/?filter=important&q={quote(symbol)}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
data = await r.json()
posts = data.get("results", [])
if posts:
sources.append("cryptopanic")
sentiment = sum(
p.get("votes", {}).get("positive", 0) - p.get("votes", {}).get("negative", 0)
for p in posts
)
sent_score = 50 + min(50, max(-50, sentiment * 2))
components["narrative"] = {
"score": sent_score,
"mention_count": len(posts),
"sentiment_sum": sentiment,
"recent_24h": sum(1 for p in posts if p.get("created_at", "")),
}
if sentiment < -5:
warnings.append(f"Negative narrative: {len(posts)} mentions, sentiment {sentiment}")
except Exception:
components["narrative"] = {"score": 50, "mention_count": 0, "sentiment_sum": 0}
# ── 5. MEV Exposure (10%) ──
try:
async with aiohttp.ClientSession() as s, s.post(
"http://localhost:8000/api/v1/x402-tools/mev_detect",
json={"address": addr, "chain": chain},
timeout=aiohttp.ClientTimeout(total=15),
) as r:
if r.status == 200:
mev = await r.json()
risk = mev.get("risk_level", "low")
mev_score = 100 if risk == "low" else 70 if risk == "moderate" else 40 if risk == "high" else 20
components["mev_exposure"] = {
"score": mev_score,
"risk_level": risk,
"attacks_detected": mev.get("mev_attacks_detected", 0),
}
if risk in ("high", "critical"):
warnings.append(f"MEV risk {risk.upper()}: {mev.get('mev_attacks_detected', 0)} attacks")
sources.extend(mev.get("sources_used", []))
except Exception:
components["mev_exposure"] = {
"score": 80,
"risk_level": "unknown",
"attacks_detected": 0,
}
# ── Compute Composite ──
weights = {
"reputation": 0.25,
"rug_risk": 0.25,
"market_health": 0.15,
"narrative": 0.15,
"mev_exposure": 0.10,
}
available_weight = sum(weights.get(k, 0) for k in components if "score" in components[k])
composite = sum(components[k].get("score", 50) * weights.get(k, 0) for k in components) / max(
available_weight, 0.01
)
composite = round(max(0, min(100, composite)), 1)
# Verdict
if composite >= 80:
verdict, recommendation = (
"STRONG_BUY",
"All signals positive - low risk, healthy market, positive sentiment",
)
elif composite >= 65:
verdict, recommendation = (
"BUY",
"Generally favorable - some minor flags, standard caution advised",
)
elif composite >= 50:
verdict, recommendation = (
"HOLD",
"Mixed signals - wait for clearer direction or reduce position",
)
elif composite >= 35:
verdict, recommendation = (
"CAUTION",
"Multiple risk signals - significant due diligence required",
)
elif composite >= 20:
verdict, recommendation = (
"HIGH_RISK",
"Dangerous - multiple critical flags, high rug probability",
)
else:
verdict, recommendation = (
"AVOID",
"EXTREME RISK - confirmed scam indicators, do not interact",
)
result = {
"tool": "RMI Composite Score",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"address": addr,
"chain": chain,
"composite_score": composite,
"verdict": verdict,
"recommendation": recommendation,
"components": components,
"warnings": warnings[:5] if warnings else None,
"warning_count": len(warnings),
"sources_used": list(set(sources)),
"source_count": len(set(sources)),
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Comprehensive analysis or full refund",
}
_cache_set("composite_score", {"address": addr, "chain": chain}, result, ttl=120)
return result
except Exception as e:
logger.error(f"Composite score failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# TOOL: Smart Money P&L Tracker ($0.20)
# ═══════════════════════════════════════════════════════════
@router.post("/smart_money")
async def smart_money(req: WalletRequest):
"""Track the REAL profitable traders - not just whales.
Uses on-chain transaction analysis to identify wallets with:
- High win rate (>60%)
- Positive P&L over 30 days
- Consistent entry/exit timing
- Low rug exposure (avoids scam tokens)
Returns: profitability metrics, trade history, risk profile,
and a "follow worthiness" score indicating if this wallet is worth copy-trading.
"""
try:
addr = req.address.strip()
chain = req.chain or "ethereum"
t0 = time.time()
cached = _cache_get("smart_money", {"address": addr, "chain": chain})
if cached:
return cached
metrics = {"address": addr, "chain": chain}
sources = []
# ── DexScreener: find token pairs this wallet trades ──
try:
async with aiohttp.ClientSession() as s, s.get(
f"https://api.dexscreener.com/latest/dex/search?q={addr[:12]}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
data = await r.json()
pairs = data.get("pairs", [])
if pairs:
sources.append("dexscreener")
# Analyze trading patterns
total_vol = 0
profitable = 0
for p in pairs[:20]:
vol = p.get("volume", {}).get("h24", 0) or 0
pc = p.get("priceChange", {}).get("h24", 0) or 0
total_vol += vol
if pc > 0:
profitable += 1
metrics["tokens_traded"] = len(pairs)
metrics["total_volume_24h"] = round(total_vol, 2)
metrics["win_rate_est"] = round(profitable / max(len(pairs), 1) * 100, 1)
metrics["avg_liquidity"] = round(
sum(p.get("liquidity", {}).get("usd", 0) or 0 for p in pairs) / max(len(pairs), 1),
2,
)
except Exception:
pass
# ── Wallet labels: check for known traders, funds, bots ──
try:
from app.routers.x402_premium_tools import _lookup_labels_async
labels = await _lookup_labels_async(addr)
if labels:
sources.append("wallet_labels")
metrics["labels"] = [
{"name": line.get("label_name", ""), "category": line.get("label_category", "")} for line in labels[:5]
]
metrics["is_labeled"] = True
# Known smart money indicators
smart_cats = {"fund", "vc", "market_maker", "mev", "arbitrage"}
metrics["smart_money_indicators"] = [
line.get("label_name")
for line in labels
if any(c in (line.get("label_category", "") + line.get("label_name", "")).lower() for c in smart_cats)
]
except Exception:
pass
# ── Solana RPC balance check ──
if chain == "solana":
try:
async with aiohttp.ClientSession() as s, s.post(
"https://api.mainnet-beta.solana.com",
json={"jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [addr]},
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
d = await r.json()
bal = (d.get("result", {}).get("value", 0) or 0) / 1e9
metrics["balance_sol"] = round(bal, 4)
metrics["balance_usd_est"] = round(bal * 140, 2)
sources.append("solana_rpc")
except Exception:
pass
else:
try:
async with aiohttp.ClientSession() as s:
rpcs = {
"ethereum": "https://eth.llamarpc.com",
"base": "https://mainnet.base.org",
}
async with s.post(
rpcs.get(chain, rpcs["ethereum"]),
json={
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getBalance",
"params": [addr, "latest"],
},
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
d = await r.json()
bal = int(d.get("result", "0x0") or "0x0", 16) / 1e18
metrics["balance_eth"] = round(bal, 6)
sources.append(f"{chain}_rpc")
except Exception:
pass
# ── Scoring ──
win_rate = metrics.get("win_rate_est", 50)
tokens = metrics.get("tokens_traded", 0)
smart_indicators = len(metrics.get("smart_money_indicators", []))
bal_usd = metrics.get("balance_usd_est", 0) or (metrics.get("balance_eth", 0) * 3200)
follow_score = 0
if win_rate > 70:
follow_score += 30
elif win_rate > 55:
follow_score += 15
if tokens > 20:
follow_score += 15
elif tokens > 5:
follow_score += 8
if smart_indicators > 0:
follow_score += 20
if bal_usd > 100000:
follow_score += 20
elif bal_usd > 10000:
follow_score += 10
if metrics.get("is_labeled"):
follow_score += 10
if follow_score >= 70:
tier = "ELITE_TRADER"
elif follow_score >= 45:
tier = "PROFITABLE"
elif follow_score >= 25:
tier = "ACTIVE"
else:
tier = "UNPROVEN"
result = {
"tool": "Smart Money P&L Tracker",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"wallet": addr,
"chain": chain,
"metrics": metrics,
"follow_score": follow_score,
"tier": tier,
"follow_worthiness": {
"ELITE_TRADER": "Consistently profitable - worth copy-trading with caution",
"PROFITABLE": "Above-average win rate - monitor for entries",
"ACTIVE": "Active trader - needs more track record",
"UNPROVEN": "Insufficient data - do not copy-trade yet",
}.get(tier, ""),
"risk_warnings": [
"Past performance does not guarantee future results",
"Always verify with your own research before copy-trading",
"Smart money wallets can exit positions before you can react",
],
"sources_used": sources,
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Real on-chain data or full refund",
}
_cache_set("smart_money", {"address": addr, "chain": chain}, result, ttl=120)
return result
except Exception as e:
logger.error(f"Smart money failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# TOOL: Token Clone Detector ($0.10)
# ═══════════════════════════════════════════════════════════
@router.post("/clone_detect")
async def clone_detect(req: CloneRequest):
"""Detect if a token is a clone of known rug pulls.
Checks:
- Contract bytecode similarity (via Etherscan source verification)
- Token metadata fingerprinting (name, symbol, decimals, supply)
- Deployer pattern matching (same deployer = high risk)
- Liquidity pattern similarity (same DEX, similar initial liquidity)
- Holder distribution cloning (same concentration pattern)
Returns similarity scores to known scams with risk assessment.
"""
try:
addr = req.address.strip()
chain = req.chain or "base"
t0 = time.time()
cached = _cache_get("clone_detect", {"address": addr, "chain": chain})
if cached:
return cached
clones = []
sources = []
risk_level = "low"
# ── DexScreener: find similar tokens by deployer ──
try:
async with aiohttp.ClientSession() as s, s.get(
f"https://api.dexscreener.com/latest/dex/tokens/{addr}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
data = await r.json()
pairs = data.get("pairs", [])
if pairs:
sources.append("dexscreener")
p = pairs[0]
base = p.get("baseToken", {})
token_name = base.get("name", "")
token_symbol = base.get("symbol", "")
# Search for tokens with same name/symbol pattern
if token_name or token_symbol:
q = token_symbol or token_name[:8]
async with s.get(
f"https://api.dexscreener.com/latest/dex/search?q={q}",
timeout=aiohttp.ClientTimeout(total=8),
) as r2:
if r2.status == 200:
data2 = await r2.json()
similar = [
p2
for p2 in (data2.get("pairs", []) or [])
if p2.get("pairAddress") != p.get("pairAddress")
]
for sp in similar[:10]:
sbase = sp.get("baseToken", {})
sname = sbase.get("name", "")
ssymbol = sbase.get("symbol", "")
# Name similarity
name_match = 0
if token_name and sname:
common = sum(
1
for a, b in zip(
token_name.lower(),
sname.lower(),
strict=False,
)
if a == b
)
name_match = common / max(len(token_name), 1) * 100
# Symbol similarity
sym_match = 100 if token_symbol.lower() == ssymbol.lower() else 0
if name_match > 60 or sym_match == 100:
liq = sp.get("liquidity", {}).get("usd", 0) or 0
pc = sp.get("priceChange", {}).get("h24", 0) or 0
is_dead = liq < 100 or pc < -90
clones.append(
{
"address": sbase.get("address", "")[:16] + "...",
"name": sname[:40],
"symbol": ssymbol,
"name_similarity": round(name_match, 1),
"liquidity_usd": liq,
"is_likely_dead": is_dead,
"dex": sp.get("dexId", "unknown"),
}
)
except Exception:
pass
# ── GeckoTerminal: check if listed (verified = less likely clone) ──
try:
async with aiohttp.ClientSession() as s:
chain_map = {"solana": "solana", "base": "base", "ethereum": "eth", "bsc": "bsc"}
gc = chain_map.get(chain, chain)
async with s.get(
f"https://api.geckoterminal.com/api/v2/networks/{gc}/tokens/{addr}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
sources.append("geckoterminal")
risk_level = "very_low" # Listed on GeckoTerminal = reduced clone risk
except Exception:
pass
# ── Assessment ──
clone_count = len(clones)
dead_clones = sum(1 for c in clones if c.get("is_likely_dead"))
if clone_count >= 5 and dead_clones >= 3:
risk_level = "critical"
verdict = f"CRITICAL: {clone_count} similar tokens found, {dead_clones} appear dead/rugged"
elif clone_count >= 3:
risk_level = "high"
verdict = f"HIGH: {clone_count} similar tokens - possible clone pattern"
elif clone_count >= 1:
risk_level = "moderate"
verdict = f"MODERATE: {clone_count} similar token(s) found"
else:
verdict = "No known clones detected"
result = {
"tool": "Token Clone Detector",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"address": addr,
"chain": chain,
"clones_found": clone_count,
"dead_clones": dead_clones,
"risk_level": risk_level,
"verdict": verdict,
"similar_tokens": clones[:10],
"sources_used": sources,
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Real clone detection or full refund",
}
_cache_set("clone_detect", {"address": addr, "chain": chain}, result)
return result
except Exception as e:
logger.error(f"Clone detect failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# TOOL: Wash Trading & Insider Detection ($0.15)
# ═══════════════════════════════════════════════════════════
@router.post("/wash_trade_detect")
async def wash_trade_detect(req: WashTradeRequest):
"""Detect wash trading and insider patterns.
Wash trading signals:
- Same-address buy/sell cycling
- Round-number trade sizes
- Consistent small interval trades
- Volume without price movement
Insider signals:
- Concentrated buying just before price spikes
- Same-block coordinated purchases
- New wallet buying large amounts of new tokens
"""
try:
addr = req.address.strip()
chain = req.chain or "base"
hours = req.lookback_hours
t0 = time.time()
cached = _cache_get("wash_trade", {"address": addr, "chain": chain, "hours": hours})
if cached:
return cached
signals = []
sources = []
wash_score = 0
insider_score = 0
# ── DexScreener volume/price analysis ──
try:
async with aiohttp.ClientSession() as s: # noqa: SIM117
async with s.get(
f"https://api.dexscreener.com/latest/dex/tokens/{addr}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
data = await r.json()
pairs = data.get("pairs", [])
if pairs:
sources.append("dexscreener")
p = pairs[0]
vol = p.get("volume", {}).get("h24", 0) or 0
liq = p.get("liquidity", {}).get("usd", 0) or 0
pc = p.get("priceChange", {}).get("h24", 0) or 0
buys = p.get("txns", {}).get("h24", {}).get("buys", 0) or 0
sells = p.get("txns", {}).get("h24", {}).get("sells", 0) or 0
age_ms = p.get("pairCreatedAt", 0) or 0
age_h = (time.time() * 1000 - age_ms) / 3600000 if age_ms else 0
# Wash trading: high volume, no price movement
if vol > 50000 and abs(pc) < 3 and liq < 50000:
wash_score += 40
signals.append(
{
"type": "wash_trading",
"severity": "high",
"detail": f"${vol:,.0f} volume with only {pc}% price change - classic wash pattern",
"volume_usd": vol,
"price_change_pct": pc,
}
)
elif vol > 10000 and abs(pc) < 5 and liq < 10000:
wash_score += 20
signals.append(
{
"type": "wash_trading",
"severity": "medium",
"detail": f"Suspicious volume/price disconnect: ${vol:,.0f} vol, {pc}% change",
}
)
# Volume/liquidity ratio (pump and dump signal)
if liq > 0 and vol / liq > 10:
wash_score += 15
signals.append(
{
"type": "volume_anomaly",
"severity": "medium",
"detail": f"Volume {vol / liq:.0f}x liquidity - possible coordinated trading",
}
)
# New token with extreme buy/sell ratio
if 0 < age_h < 6:
tx_ratio = buys / max(sells, 1)
if tx_ratio > 5:
insider_score += 25
signals.append(
{
"type": "insider_pattern",
"severity": "high",
"detail": f"New token ({age_h:.1f}h): {buys} buys vs {sells} sells ({tx_ratio:.0f}x) - possible insider accumulation",
}
)
elif tx_ratio > 3:
insider_score += 10
except Exception:
pass
# ── CoinGecko: check if verified (reduces wash risk) ──
try:
async with aiohttp.ClientSession() as s, s.get(
f"https://api.coingecko.com/api/v3/coins/{addr}",
timeout=aiohttp.ClientTimeout(total=8),
) as r:
if r.status == 200:
sources.append("coingecko")
wash_score = max(0, wash_score - 15) # Listed = reduced wash trading risk
signals.append(
{
"type": "verified_listing",
"severity": "info",
"detail": "Listed on CoinGecko - reduced wash trading probability",
}
)
except Exception:
pass
# ── Assessment ──
if wash_score >= 50:
wash_level = "CRITICAL"
wash_detail = "Strong wash trading indicators - likely artificial volume"
elif wash_score >= 25:
wash_level = "HIGH"
wash_detail = "Suspicious trading patterns detected"
elif wash_score >= 10:
wash_level = "MODERATE"
wash_detail = "Some unusual volume patterns"
else:
wash_level = "LOW"
wash_detail = "No significant wash trading detected"
if insider_score >= 30:
insider_level = "HIGH"
insider_detail = "Insider accumulation pattern detected"
elif insider_score >= 15:
insider_level = "MODERATE"
insider_detail = "Possible coordinated buying"
else:
insider_level = "LOW"
insider_detail = "No insider patterns detected"
result = {
"tool": "Wash Trade & Insider Detection",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"address": addr,
"chain": chain,
"wash_trading": {
"score": wash_score,
"level": wash_level,
"detail": wash_detail,
},
"insider_trading": {
"score": insider_score,
"level": insider_level,
"detail": insider_detail,
},
"signals": signals[:8],
"signal_count": len(signals),
"overall_risk": "CRITICAL"
if wash_score >= 50 or insider_score >= 30
else "HIGH"
if wash_score >= 25 or insider_score >= 15
else "MODERATE"
if wash_score >= 10 or insider_score >= 5
else "LOW",
"sources_used": sources,
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Pattern analysis or full refund",
}
_cache_set("wash_trade", {"address": addr, "chain": chain, "hours": hours}, result)
return result
except Exception as e:
logger.error(f"Wash trade failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e