- 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>
640 lines
24 KiB
Python
640 lines
24 KiB
Python
"""
|
|
Exit Scam Forecaster (Rug Pull Predictor)
|
|
==========================================
|
|
Predictive model identifying tokens at high risk of rug pull.
|
|
Multi-signal analysis combining liquidity patterns, holder behavior,
|
|
deployer history, and social signals for early warning.
|
|
|
|
The model evaluates 6 risk dimensions, each weighted by predictive power,
|
|
to produce a single 0-100 rug score with explainable per-signal breakdown.
|
|
|
|
TOOL : rug_pull_predictor
|
|
TIER : premium / intelligence
|
|
PRICE : $0.10 (100000 atoms)
|
|
TRIAL : 1 free check
|
|
|
|
Signals Analyzed:
|
|
- LIQUIDITY: LP lock status, liquidity depth vs market cap, sudden changes
|
|
- HOLDER_CONCENTRATION: Top-10 concentration, single-entity cluster ratio
|
|
- DEPLOYER: Previous project history, rug count, anonymous deployer
|
|
- LAUNCH_PATTERN: Bundled distribution, sniper activity, presale structure
|
|
- TRADING_BEHAVIOR: Seller concentration, wash trading, suspicious transfers
|
|
- SOCIAL_SIGNALS: Low engagement, anonymous team, no community presence
|
|
|
|
Data Sources (all free):
|
|
- DexScreener - token pairs, liquidity, price
|
|
- Solscan (public) - SPL token holder data
|
|
- Etherscan/BscScan (public free) - EVM token info
|
|
- Birdeye public - basic token metrics
|
|
- GoPlus (free) - token security info (if available)
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import re
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
__all__ = ["RiskLevel", "RugPullAnalysis", "SignalResult", "analyze", "predict_rug_pull"]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Constants ────────────────────────────────────────────────────
|
|
|
|
FREE_APIS = {
|
|
"dexscreener": "https://api.dexscreener.com/latest/dex",
|
|
"birdeye_public": "https://public-api.birdeye.so",
|
|
"goplus": "https://api.gopluslabs.io/api/v1",
|
|
}
|
|
|
|
EVM_ADDR_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
|
|
SOLANA_ADDR_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
|
|
|
|
# Default risk thresholds
|
|
DEFAULT_WEIGHTS = {
|
|
"liquidity": 0.25,
|
|
"holder_concentration": 0.20,
|
|
"deployer": 0.15,
|
|
"launch_pattern": 0.18,
|
|
"trading_behavior": 0.12,
|
|
"social_signals": 0.10,
|
|
}
|
|
|
|
CHAIN_MAP: dict[str, str] = {
|
|
"solana": "solana",
|
|
"ethereum": "ethereum",
|
|
"bsc": "bsc",
|
|
"polygon": "polygon",
|
|
"arbitrum": "arbitrum",
|
|
"optimism": "optimism",
|
|
"base": "base",
|
|
"avalanche": "avalanche",
|
|
}
|
|
|
|
# ── Data Models ──────────────────────────────────────────────────
|
|
|
|
|
|
class RiskLevel(Enum):
|
|
SAFE = "safe"
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
CRITICAL = "critical"
|
|
|
|
|
|
@dataclass
|
|
class SignalResult:
|
|
"""Individual signal analysis result."""
|
|
|
|
signal: str
|
|
score: float # 0 (safe) - 100 (risky)
|
|
weight: float
|
|
findings: list[str] = field(default_factory=list)
|
|
details: dict[str, Any] = field(default_factory=dict)
|
|
|
|
@property
|
|
def weighted_score(self) -> float:
|
|
return self.score * self.weight
|
|
|
|
@property
|
|
def level(self) -> RiskLevel:
|
|
if self.score < 20:
|
|
return RiskLevel.SAFE
|
|
elif self.score < 40:
|
|
return RiskLevel.LOW
|
|
elif self.score < 60:
|
|
return RiskLevel.MEDIUM
|
|
elif self.score < 80:
|
|
return RiskLevel.HIGH
|
|
return RiskLevel.CRITICAL
|
|
|
|
|
|
@dataclass
|
|
class RugPullAnalysis:
|
|
"""Complete rug pull risk analysis result."""
|
|
|
|
token_address: str
|
|
chain: str
|
|
token_symbol: str
|
|
token_name: str
|
|
|
|
overall_score: float # 0 (safe) - 100 (critical risk)
|
|
overall_level: RiskLevel
|
|
signals: list[SignalResult] = field(default_factory=list)
|
|
summary: str = ""
|
|
recommendations: list[str] = field(default_factory=list)
|
|
analysis_timestamp: str = ""
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"token_address": self.token_address,
|
|
"chain": self.chain,
|
|
"token_symbol": self.token_symbol,
|
|
"token_name": self.token_name,
|
|
"rug_score": round(self.overall_score, 1),
|
|
"risk_level": self.overall_level.value,
|
|
"signals": [
|
|
{
|
|
"name": s.signal,
|
|
"score": round(s.score, 1),
|
|
"level": s.level.value,
|
|
"findings": s.findings,
|
|
}
|
|
for s in self.signals
|
|
],
|
|
"summary": self.summary,
|
|
"recommendations": self.recommendations,
|
|
"analyzed_at": self.analysis_timestamp,
|
|
}
|
|
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────
|
|
|
|
|
|
def is_valid_address(addr: str) -> bool:
|
|
addr = addr.strip()
|
|
return bool(EVM_ADDR_RE.match(addr) or SOLANA_ADDR_RE.match(addr))
|
|
|
|
|
|
def _normalize_chain(chain: str) -> str:
|
|
chain = chain.strip().lower()
|
|
return CHAIN_MAP.get(chain, chain)
|
|
|
|
|
|
def _level_from_score(score: float) -> RiskLevel:
|
|
if score < 20:
|
|
return RiskLevel.SAFE
|
|
elif score < 40:
|
|
return RiskLevel.LOW
|
|
elif score < 60:
|
|
return RiskLevel.MEDIUM
|
|
elif score < 80:
|
|
return RiskLevel.HIGH
|
|
return RiskLevel.CRITICAL
|
|
|
|
|
|
# ── Signal Analyzers ────────────────────────────────────────────
|
|
|
|
|
|
async def _analyze_liquidity(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
|
|
"""Analyze liquidity risk: depth, lock status, sudden changes."""
|
|
signal = SignalResult(
|
|
signal="liquidity",
|
|
score=0.0,
|
|
weight=DEFAULT_WEIGHTS["liquidity"],
|
|
findings=[],
|
|
)
|
|
|
|
try:
|
|
# Try DexScreener for pair data
|
|
url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
|
|
resp = await client.get(url, timeout=10)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
pair = pairs[0]
|
|
liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
|
|
fdv = float(pair.get("fdv", 0) or 0)
|
|
volume_24h = float(pair.get("volume", {}).get("h24", 0) or 0)
|
|
|
|
signal.details["liquidity_usd"] = liquidity_usd
|
|
signal.details["fdv"] = fdv
|
|
signal.details["volume_24h"] = volume_24h
|
|
|
|
# Score: low liquidity = higher risk
|
|
if liquidity_usd < 1000:
|
|
signal.score += 40
|
|
signal.findings.append("Extremely low liquidity (<$1K)")
|
|
elif liquidity_usd < 10000:
|
|
signal.score += 25
|
|
signal.findings.append("Low liquidity (<$10K)")
|
|
elif liquidity_usd < 100000:
|
|
signal.score += 15
|
|
else:
|
|
signal.score += 5
|
|
|
|
# Score: low liquidity-to-FDV ratio = manipulation risk
|
|
if fdv > 0 and liquidity_usd > 0:
|
|
liq_ratio = liquidity_usd / fdv
|
|
if liq_ratio < 0.01:
|
|
signal.score += 20
|
|
signal.findings.append("Critical: Liquidity-to-FDV ratio <1% - easy to dump")
|
|
elif liq_ratio < 0.05:
|
|
signal.score += 10
|
|
signal.findings.append("Low liquidity-to-FDV ratio")
|
|
|
|
# Score: zero volume or very low = suspicious
|
|
if volume_24h < 100:
|
|
signal.score += 15
|
|
signal.findings.append("Near-zero 24h trading volume")
|
|
except Exception as e:
|
|
logger.warning("Liquidity analysis failed: %s", e)
|
|
signal.findings.append("Liquidity data unavailable - scoring conservative")
|
|
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
|
|
async def _analyze_holder_concentration(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
|
|
"""Analyze token holder distribution for concentration risk."""
|
|
signal = SignalResult(
|
|
signal="holder_concentration",
|
|
score=0.0,
|
|
weight=DEFAULT_WEIGHTS["holder_concentration"],
|
|
findings=[],
|
|
)
|
|
|
|
if chain == "solana":
|
|
try:
|
|
# Try Birdeye public API for holder info
|
|
resp = await client.get(
|
|
f"{FREE_APIS['birdeye_public']}/public/token/token_owner?address={token_address}",
|
|
headers={"accept": "application/json"},
|
|
timeout=10,
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
holders = data.get("data", {}).get("tokenOwner", []) or data.get("data", {}).get("list", []) or []
|
|
if holders:
|
|
total_supply_analyzed = sum(float(h.get("uiAmount", 0) or 0) for h in holders)
|
|
top10 = sorted(holders, key=lambda h: float(h.get("uiAmount", 0) or 0), reverse=True)[:10]
|
|
top10_pct = (
|
|
sum(float(h.get("uiAmount", 0) or 0) for h in top10) / total_supply_analyzed
|
|
if total_supply_analyzed > 0
|
|
else 0
|
|
)
|
|
|
|
signal.details["top_10_concentration_pct"] = round(top10_pct * 100, 1)
|
|
|
|
if top10_pct > 0.8:
|
|
signal.score += 45
|
|
signal.findings.append(
|
|
f"Top 10 holders control {top10_pct * 100:.0f}% of supply - extreme concentration"
|
|
)
|
|
elif top10_pct > 0.5:
|
|
signal.score += 30
|
|
signal.findings.append(f"Top 10 holders control {top10_pct * 100:.0f}% - high concentration")
|
|
elif top10_pct > 0.3:
|
|
signal.score += 15
|
|
else:
|
|
signal.score += 5
|
|
except Exception as e:
|
|
logger.warning("Holder concentration analysis failed: %s", e)
|
|
signal.findings.append("Holder data unavailable")
|
|
|
|
# Conservative scoring if we lack data
|
|
if not signal.findings:
|
|
signal.score = 30 # moderate default
|
|
signal.findings.append("Limited holder data - scored conservatively")
|
|
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
|
|
async def _analyze_deployer(client: httpx.AsyncClient, deployer: str | None, chain: str) -> SignalResult:
|
|
"""Analyze deployer risk based on available data."""
|
|
signal = SignalResult(
|
|
signal="deployer",
|
|
score=0.0,
|
|
weight=DEFAULT_WEIGHTS["deployer"],
|
|
findings=[],
|
|
)
|
|
|
|
if not deployer:
|
|
signal.score = 50
|
|
signal.findings.append("Deployer address unknown - cannot verify history")
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
try:
|
|
if chain == "solana":
|
|
# Try to check if deployer has multiple tokens
|
|
resp = await client.get(
|
|
f"{FREE_APIS['birdeye_public']}/public/token/list?creator={deployer}",
|
|
headers={"accept": "application/json"},
|
|
timeout=10,
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
tokens_deployed = data.get("data", {}).get("items", [])
|
|
token_count = len(tokens_deployed)
|
|
|
|
signal.details["deployer_token_count"] = token_count
|
|
|
|
if token_count > 20:
|
|
signal.score += 40
|
|
signal.findings.append(f"Serial deployer: {token_count} tokens created - potential rug factory")
|
|
elif token_count > 10:
|
|
signal.score += 25
|
|
signal.findings.append(f"Active deployer: {token_count} tokens created")
|
|
elif token_count > 5:
|
|
signal.score += 10
|
|
else:
|
|
signal.score += 5
|
|
else:
|
|
# EVM - check if deployer is a known contract factory
|
|
signal.score += 10
|
|
signal.findings.append("EVM deployer - limited public history available")
|
|
except Exception as e:
|
|
logger.warning("Deployer analysis failed: %s", e)
|
|
signal.findings.append("Deployer data unavailable")
|
|
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
|
|
async def _analyze_launch_pattern(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
|
|
"""Analyze launch pattern for manipulation signs."""
|
|
signal = SignalResult(
|
|
signal="launch_pattern",
|
|
score=20.0, # Start at moderate risk as baseline
|
|
weight=DEFAULT_WEIGHTS["launch_pattern"],
|
|
findings=[],
|
|
)
|
|
|
|
try:
|
|
# Use DexScreener for trading data
|
|
url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
|
|
resp = await client.get(url, timeout=10)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
pair = pairs[0]
|
|
pair_age_hours = None
|
|
if pair.get("pairCreatedAt"):
|
|
created = pair["pairCreatedAt"] / 1000 # ms to s
|
|
pair_age_hours = (time.time() - created) / 3600
|
|
signal.details["age_hours"] = round(pair_age_hours, 1)
|
|
|
|
txns = pair.get("txns", {})
|
|
buys_h24 = int(txns.get("h24", {}).get("buys", 0))
|
|
sells_h24 = int(txns.get("h24", {}).get("sells", 0))
|
|
|
|
signal.details["buys_24h"] = buys_h24
|
|
signal.details["sells_24h"] = sells_h24
|
|
|
|
# Very young pair = higher risk
|
|
if pair_age_hours is not None:
|
|
if pair_age_hours < 1:
|
|
signal.score += 25
|
|
signal.findings.append(f"Pair is {pair_age_hours:.1f}h old - extremely young, high risk")
|
|
elif pair_age_hours < 24:
|
|
signal.score += 15
|
|
signal.findings.append(f"Pair is {pair_age_hours:.1f}h old - recent launch")
|
|
|
|
# Abnormal buy/sell ratio
|
|
total_txns = buys_h24 + sells_h24
|
|
if total_txns > 0:
|
|
sell_ratio = sells_h24 / total_txns
|
|
if sell_ratio > 0.8:
|
|
signal.score += 20
|
|
signal.findings.append(f"Sell ratio {sell_ratio * 100:.0f}% - heavy selling pressure")
|
|
elif sell_ratio > 0.6:
|
|
signal.score += 10
|
|
elif sell_ratio < 0.1 and buys_h24 > 50:
|
|
signal.score += 15
|
|
signal.findings.append("Abnormally low sell ratio - possible bot wash trading")
|
|
except Exception as e:
|
|
logger.warning("Launch pattern analysis failed: %s", e)
|
|
signal.findings.append("Launch data partially available")
|
|
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
|
|
async def _analyze_trading_behavior(client: httpx.AsyncClient, token_address: str, chain: str) -> SignalResult:
|
|
"""Analyze trading behavior for manipulation signs."""
|
|
signal = SignalResult(
|
|
signal="trading_behavior",
|
|
score=10.0,
|
|
weight=DEFAULT_WEIGHTS["trading_behavior"],
|
|
findings=[],
|
|
)
|
|
|
|
try:
|
|
url = f"{FREE_APIS['dexscreener']}/search?q={token_address}"
|
|
resp = await client.get(url, timeout=10)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
pairs = data.get("pairs", [])
|
|
if pairs:
|
|
pair = pairs[0]
|
|
liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
|
|
volume_24h = float(pair.get("volume", {}).get("h24", 0) or 0)
|
|
|
|
# Suspicious volume-to-liquidity ratio (wash trading indicator)
|
|
if liquidity_usd > 0 and volume_24h > 0:
|
|
vol_liq_ratio = volume_24h / liquidity_usd
|
|
if vol_liq_ratio > 20:
|
|
signal.score += 25
|
|
signal.findings.append(
|
|
f"Volume/liquidity ratio of {vol_liq_ratio:.0f}x - possible wash trading"
|
|
)
|
|
elif vol_liq_ratio > 10:
|
|
signal.score += 10
|
|
elif liquidity_usd == 0 and volume_24h > 0:
|
|
signal.score += 20
|
|
signal.findings.append("Volume with zero liquidity - suspicious")
|
|
|
|
# Price change indicators
|
|
price_change_h24 = pair.get("priceChange", {}).get("h24", 0)
|
|
if isinstance(price_change_h24, int | float):
|
|
if price_change_h24 < -50:
|
|
signal.score += 20
|
|
signal.findings.append(f"Price dropped {abs(price_change_h24):.0f}% in 24h")
|
|
elif price_change_h24 > 500:
|
|
signal.score += 10
|
|
signal.findings.append(f"Price surged {price_change_h24:.0f}% in 24h - potential pump")
|
|
except Exception as e:
|
|
logger.warning("Trading behavior analysis failed: %s", e)
|
|
|
|
signal.score = min(signal.score, 100)
|
|
return signal
|
|
|
|
|
|
async def _analyze_social_signals(token_address: str, chain: str) -> SignalResult:
|
|
"""Analyze social signals (placeholder - relies on chain metadata)."""
|
|
signal = SignalResult(
|
|
signal="social_signals",
|
|
score=20.0, # Default conservative
|
|
weight=DEFAULT_WEIGHTS["social_signals"],
|
|
findings=["Social signal analysis limited with free data sources"],
|
|
)
|
|
|
|
# Conservative scoring: no social data = moderate risk
|
|
signal.score = 30
|
|
return signal
|
|
|
|
|
|
# ── Main Analysis ───────────────────────────────────────────────
|
|
|
|
|
|
async def predict_rug_pull(
|
|
token_address: str,
|
|
chain: str = "solana",
|
|
deployer: str | None = None,
|
|
weights: dict[str, float] | None = None,
|
|
) -> RugPullAnalysis:
|
|
"""
|
|
Predict rug pull risk for a given token.
|
|
|
|
Args:
|
|
token_address: The token contract/mint address
|
|
chain: Blockchain (solana, ethereum, bsc, etc.)
|
|
deployer: Optional deployer/creator address for history check
|
|
weights: Optional custom signal weights (must sum to 1.0)
|
|
|
|
Returns:
|
|
RugPullAnalysis with overall score and per-signal breakdown
|
|
"""
|
|
if not is_valid_address(token_address):
|
|
raise ValueError(f"Invalid token address: {token_address}")
|
|
|
|
chain = _normalize_chain(chain)
|
|
effective_weights = weights or DEFAULT_WEIGHTS
|
|
|
|
# Validate weights
|
|
if abs(sum(effective_weights.values()) - 1.0) > 0.01:
|
|
raise ValueError("Signal weights must sum to 1.0")
|
|
|
|
token_symbol = ""
|
|
token_name = ""
|
|
|
|
# Run all signal analyzers in parallel (single client for efficiency)
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
# Quick metadata fetch for display
|
|
try:
|
|
search_resp = await client.get(f"{FREE_APIS['dexscreener']}/search?q={token_address}", timeout=10)
|
|
if search_resp.status_code == 200:
|
|
pairs = search_resp.json().get("pairs", [])
|
|
if pairs:
|
|
token_symbol = pairs[0].get("baseToken", {}).get("symbol", "")
|
|
token_name = pairs[0].get("baseToken", {}).get("name", "")
|
|
except Exception:
|
|
pass
|
|
results = await asyncio.gather(
|
|
_analyze_liquidity(client, token_address, chain),
|
|
_analyze_holder_concentration(client, token_address, chain),
|
|
_analyze_deployer(client, deployer, chain),
|
|
_analyze_launch_pattern(client, token_address, chain),
|
|
_analyze_trading_behavior(client, token_address, chain),
|
|
_analyze_social_signals(token_address, chain),
|
|
return_exceptions=True,
|
|
)
|
|
|
|
signals: list[SignalResult] = []
|
|
for r in results:
|
|
if isinstance(r, SignalResult):
|
|
signals.append(r)
|
|
elif isinstance(r, Exception):
|
|
logger.warning("Signal analysis exception: %s", r)
|
|
signals.append(
|
|
SignalResult(
|
|
signal="unknown",
|
|
score=50.0,
|
|
weight=0.05,
|
|
findings=[f"Analysis error: {r}"],
|
|
)
|
|
)
|
|
|
|
# Calculate weighted overall score using ternary
|
|
total_weight = sum(s.weight for s in signals)
|
|
overall_score = sum(s.score * s.weight for s in signals) / total_weight if total_weight > 0 else 50.0
|
|
|
|
overall_score = min(max(overall_score, 0), 100)
|
|
overall_level = _level_from_score(overall_score)
|
|
|
|
# Generate summary
|
|
if overall_score >= 70:
|
|
summary = (
|
|
f"CRITICAL RISK: This token shows strong rug pull indicators "
|
|
f"(score: {overall_score:.0f}/100). Multiple red flags detected "
|
|
f"across {sum(1 for s in signals if s.score > 50)} risk dimensions."
|
|
)
|
|
elif overall_score >= 50:
|
|
summary = (
|
|
f"HIGH RISK: This token has concerning signals "
|
|
f"(score: {overall_score:.0f}/100). Proceed with extreme caution."
|
|
)
|
|
elif overall_score >= 30:
|
|
summary = (
|
|
f"MODERATE RISK: Some concerning patterns detected "
|
|
f"(score: {overall_score:.0f}/100). Standard due diligence recommended."
|
|
)
|
|
else:
|
|
summary = (
|
|
f"LOW RISK: No major rug pull indicators detected "
|
|
f"(score: {overall_score:.0f}/100). Continue standard safety checks."
|
|
)
|
|
|
|
# Generate recommendations
|
|
recommendations = []
|
|
if overall_score >= 50:
|
|
recommendations.append("AVOID trading this token - high rug pull probability")
|
|
if any(s.score > 60 for s in signals if s.signal == "liquidity"):
|
|
recommendations.append("Liquidity is critically low or manipulable")
|
|
if any(s.score > 50 for s in signals if s.signal == "holder_concentration"):
|
|
recommendations.append("Top holders control excessive supply - potential dump risk")
|
|
if any(s.score > 50 for s in signals if s.signal == "deployer"):
|
|
recommendations.append("Deployer has concerning history - check their other projects")
|
|
if recommendations:
|
|
recommendations.append("Always verify with RugShield or SENTINEL deep scan before trading")
|
|
|
|
return RugPullAnalysis(
|
|
token_address=token_address,
|
|
chain=chain,
|
|
token_symbol=token_symbol or "UNKNOWN",
|
|
token_name=token_name or "Unknown Token",
|
|
overall_score=overall_score,
|
|
overall_level=overall_level,
|
|
signals=signals,
|
|
summary=summary,
|
|
recommendations=recommendations or ["No specific concerns detected"],
|
|
analysis_timestamp=datetime.now(UTC).isoformat(),
|
|
)
|
|
|
|
|
|
# ── Synchronous Wrapper ─────────────────────────────────────────
|
|
|
|
|
|
def analyze(token_address: str, chain: str = "solana", **kwargs: Any) -> dict[str, Any]:
|
|
"""
|
|
Synchronous entry point for rug pull prediction.
|
|
|
|
Args:
|
|
token_address: Token contract/mint address
|
|
chain: Blockchain name
|
|
**kwargs: Additional options (deployer, weights)
|
|
|
|
Returns:
|
|
Dict with full analysis results
|
|
"""
|
|
result = asyncio.run(predict_rug_pull(token_address, chain, **kwargs))
|
|
return result.to_dict()
|
|
|
|
|
|
# ── CLI Entry Point ─────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 rug_pull_predictor.py <TOKEN_ADDRESS> [chain] [deployer]")
|
|
sys.exit(1)
|
|
|
|
addr = sys.argv[1]
|
|
chain = sys.argv[2] if len(sys.argv) > 2 else "solana"
|
|
deployer = sys.argv[3] if len(sys.argv) > 3 else None
|
|
|
|
result = analyze(addr, chain, deployer=deployer)
|
|
print(json.dumps(result, indent=2))
|