- 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>
1633 lines
64 KiB
Python
1633 lines
64 KiB
Python
from app.sweep_all import results
|
|
|
|
#!/usr/bin/env python3
|
|
"""
|
|
Deep Intelligence Engine - 12 Unaccounted Features
|
|
====================================================
|
|
Deployer Reputation Score, Pre-flight Simulator, Liquidity Lock Authenticity,
|
|
Volume Authenticity Engine, Slow Rug Detector, Funding Source Tracer,
|
|
Ownership Theater Detector, Exit Liquidity Calculator, Token Lifespan Predictor,
|
|
Post-Purchase Monitor, Cross-Chain Identity Resolution, Social-Onchain Correlation
|
|
|
|
Uses real on-chain data from GMGN, Birdeye, Helius, Etherscan, and our DB.
|
|
"""
|
|
|
|
import logging # noqa: E402
|
|
import os # noqa: E402
|
|
from datetime import UTC, datetime # noqa: E402
|
|
from typing import Any # noqa: E402
|
|
|
|
import httpx # noqa: E402
|
|
|
|
logger = logging.getLogger("deep_intel")
|
|
|
|
# ── Configuration ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class DeepIntelConfig:
|
|
HELIUS_KEY: str = os.getenv("HELIUS_API_KEY", "")
|
|
HELIUS_KEY_2: str = os.getenv("HELIUS_API_KEY_2", "")
|
|
BIRDEYE_KEY: str = os.getenv("BIRDEYE_API_KEY", "")
|
|
ETHERSCAN_KEY: str = os.getenv("ETHERSCAN_KEY", "")
|
|
BSCSCAN_KEY: str = os.getenv("BSCSCAN_KEY", "")
|
|
ALCHEMY_KEY: str = os.getenv("ALCHEMY_KEY", "")
|
|
GMGN_KEY: str = os.getenv("GMGN_API_KEY", "")
|
|
COINGECKO_KEY: str = os.getenv("COINGECKO_API_KEY", "")
|
|
MORALIS_KEY: str = os.getenv("MORALIS_API_KEY", "")
|
|
OPENROUTER_KEY: str = os.getenv("OPENROUTER_API_KEY", "")
|
|
WEBACY_KEY: str = os.getenv("WEBACY_API_KEY", "")
|
|
BITQUERY_KEY: str = os.getenv("BITQUERY_API_KEY", "")
|
|
LUNARCRUSH_KEY: str = os.getenv("LUNARCRUSH_API_KEY", "")
|
|
GOPLUS_KEY: str = os.getenv("GOPLUS_API_KEY", "")
|
|
CHAINABUSE_KEY: str = os.getenv("CHAINABUSE_API_KEY", "")
|
|
|
|
CHAINS = ["sol", "eth", "bsc", "base", "arb", "polygon", "avax"] # noqa: RUF012
|
|
CHAIN_RPC_MAP = { # noqa: RUF012
|
|
"eth": f"https://eth-mainnet.g.alchemy.com/v2/{os.getenv('ALCHEMY_KEY', '')}",
|
|
"bsc": "https://bsc-dataseed.binance.org",
|
|
"base": f"https://base-mainnet.g.alchemy.com/v2/{os.getenv('ALCHEMY_KEY', '')}",
|
|
"arb": f"https://arb-mainnet.g.alchemy.com/v2/{os.getenv('ALCHEMY_KEY', '')}",
|
|
"polygon": f"https://polygon-mainnet.g.alchemy.com/v2/{os.getenv('ALCHEMY_KEY', '')}",
|
|
}
|
|
CHAIN_SCAN_MAP = { # noqa: RUF012
|
|
"eth": ("https://api.etherscan.io/api", os.getenv("ETHERSCAN_KEY", "")),
|
|
"bsc": ("https://api.bscscan.com/api", os.getenv("BSCSCAN_KEY", "")),
|
|
}
|
|
CHAIN_DEXSCREENER_MAP = { # noqa: RUF012
|
|
"sol": "solana",
|
|
"eth": "ethereum",
|
|
"bsc": "bsc",
|
|
"base": "base",
|
|
"arb": "arbitrum",
|
|
"polygon": "polygon",
|
|
"avax": "avalanche",
|
|
}
|
|
|
|
|
|
cfg = DeepIntelConfig()
|
|
|
|
|
|
# ── Data Clients ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class MultiChainClient:
|
|
"""Unified client for on-chain data across all chains"""
|
|
|
|
def __init__(self):
|
|
self.http = httpx.AsyncClient(timeout=30.0, follow_redirects=True)
|
|
|
|
async def close(self):
|
|
await self.http.aclose()
|
|
|
|
# ── GMGN ──
|
|
|
|
async def gmgn_get(self, path: str) -> dict:
|
|
try:
|
|
headers = {"Content-Type": "application/json"}
|
|
if cfg.GMGN_KEY:
|
|
headers["X-API-Key"] = cfg.GMGN_KEY
|
|
resp = await self.http.get(f"https://gmgn.ai/api/v1{path}", headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"GMGN HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── DexScreener ──
|
|
|
|
async def dexscreener(self, chain: str, address: str) -> dict:
|
|
try:
|
|
chain_id = cfg.CHAIN_DEXSCREENER_MAP.get(chain, chain)
|
|
resp = await self.http.get(f"https://api.dexscreener.com/latest/dex/tokens/{address}")
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
pairs = data.get("pairs") or []
|
|
# Filter to matching chain
|
|
matching = [p for p in pairs if p.get("chainId") == chain_id]
|
|
if matching:
|
|
best = max(matching, key=lambda p: float(p.get("volume", {}).get("h24", 0) or 0))
|
|
return best
|
|
return {"error": "no matching pair found"}
|
|
return {"error": f"DexScreener HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Etherscan / BSCScan ──
|
|
|
|
async def scan_get(self, chain: str, params: dict) -> dict:
|
|
if chain not in cfg.CHAIN_SCAN_MAP:
|
|
return {"error": f"no scanner for {chain}"}
|
|
base_url, api_key = cfg.CHAIN_SCAN_MAP[chain]
|
|
params["apikey"] = api_key
|
|
try:
|
|
resp = await self.http.get(base_url, params=params)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Scanner HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Helius (Solana) ──
|
|
|
|
async def helius_get(self, path: str) -> dict:
|
|
if not cfg.HELIUS_KEY:
|
|
return {"error": "HELIUS_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.get(f"https://api.helius.xyz/v0{path}?api-key={cfg.HELIUS_KEY}")
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Helius HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
async def helius_post(self, path: str, payload: dict) -> dict:
|
|
if not cfg.HELIUS_KEY:
|
|
return {"error": "HELIUS_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.post(f"https://api.helius.xyz/v0{path}?api-key={cfg.HELIUS_KEY}", json=payload)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Helius HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Birdeye ──
|
|
|
|
async def birdeye_get(self, path: str) -> dict:
|
|
if not cfg.BIRDEYE_KEY:
|
|
return {"error": "BIRDEYE_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.get(
|
|
f"https://public-api.birdeye.so{path}",
|
|
headers={"X-API-KEY": cfg.BIRDEYE_KEY, "x-chain": "solana"},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Birdeye HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Moralis ──
|
|
|
|
async def moralis_get(self, path: str, chain: str = "eth") -> dict:
|
|
if not cfg.MORALIS_KEY:
|
|
return {"error": "MORALIS_API_KEY not set"}
|
|
chain_map = {
|
|
"eth": "0x1",
|
|
"bsc": "0x38",
|
|
"base": "0x2105",
|
|
"arb": "0xa4b1",
|
|
"polygon": "0x89",
|
|
"avax": "0xa86a",
|
|
}
|
|
try:
|
|
resp = await self.http.get(
|
|
f"https://deep-index.moralis.io/api/v2.2{path}",
|
|
headers={"X-API-Key": cfg.MORALIS_KEY},
|
|
params={"chain": chain_map.get(chain, chain)},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Moralis HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Webacy (Threat Intel) ──
|
|
|
|
async def webacy_get(self, chain: str, address: str) -> dict:
|
|
if not cfg.WEBACY_KEY:
|
|
return {"error": "WEBACY_API_KEY not set"}
|
|
try:
|
|
# Webacy API expects chain ID or name (e.g., 'ethereum', 'bsc', 'solana')
|
|
chain_map = {
|
|
"eth": "ethereum",
|
|
"bsc": "bsc",
|
|
"base": "base",
|
|
"arb": "arbitrum",
|
|
"polygon": "polygon",
|
|
"avax": "avalanche",
|
|
"sol": "solana",
|
|
}
|
|
webacy_chain = chain_map.get(chain, chain)
|
|
resp = await self.http.get(
|
|
f"https://api.webacy.com/api/v1/token/{webacy_chain}/{address}",
|
|
headers={"x-api-key": cfg.WEBACY_KEY},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Webacy HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Bitquery v2 (GraphQL) ──
|
|
|
|
async def bitquery_post(self, query: str, variables: dict | None = None) -> dict:
|
|
if not cfg.BITQUERY_KEY:
|
|
return {"error": "BITQUERY_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.post(
|
|
"https://graphql.bitquery.io/",
|
|
json={"query": query, "variables": variables or {}},
|
|
headers={
|
|
"Authorization": f"Bearer {cfg.BITQUERY_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Bitquery HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── LunarCrush (Social Sentiment) ──
|
|
|
|
async def lunarcrush_get(self, path: str, params: dict | None = None) -> dict:
|
|
if not cfg.LUNARCRUSH_KEY:
|
|
return {"error": "LUNARCRUSH_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.get(
|
|
f"https://lunarcrush.com/api4{path}",
|
|
headers={"Authorization": f"Bearer {cfg.LUNARCRUSH_KEY}"},
|
|
params=params or {},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"LunarCrush HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── GoPlus (On-Chain Security) ──
|
|
|
|
async def goplus_get(self, chain: str, address: str) -> dict:
|
|
if not cfg.GOPLUS_KEY:
|
|
return {"error": "GOPLUS_API_KEY not set"}
|
|
try:
|
|
# GoPlus API: https://api.gopluslabs.io/api/v1/token_security/{chain_id}?contract_addresses={address}
|
|
# Chain IDs: 1 (eth), 56 (bsc), 8453 (base), 42161 (arb), 137 (polygon), 43114 (avax), 999 (sol)
|
|
chain_map = {
|
|
"eth": "1",
|
|
"bsc": "56",
|
|
"base": "8453",
|
|
"arb": "42161",
|
|
"polygon": "137",
|
|
"avax": "43114",
|
|
"sol": "999",
|
|
}
|
|
chain_id = chain_map.get(chain, "1")
|
|
resp = await self.http.get(
|
|
f"https://api.gopluslabs.io/api/v1/token_security/{chain_id}",
|
|
params={"contract_addresses": address},
|
|
headers={"Authorization": f"Bearer {cfg.GOPLUS_KEY}"},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"GoPlus HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Chainabuse (Threat Intel) ──
|
|
|
|
async def chainabuse_get(self, address: str) -> dict:
|
|
if not cfg.CHAINABUSE_KEY:
|
|
return {"error": "CHAINABUSE_API_KEY not set"}
|
|
try:
|
|
resp = await self.http.get(
|
|
f"https://api.chainabuse.com/api/v1/address/{address}/reports",
|
|
headers={"Authorization": f"Bearer {cfg.CHAINABUSE_KEY}"},
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"Chainabuse HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
# ── Generic ──
|
|
|
|
async def simple_get(self, url: str, headers: dict | None = None) -> dict:
|
|
try:
|
|
resp = await self.http.get(url, headers=headers or {})
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return {"error": f"HTTP {resp.status_code}", "url": url}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
# ── Instantiate ──
|
|
|
|
client = MultiChainClient()
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 1: DEPLOYER REPUTATION SCORE (DRS)
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def deployer_reputation(address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Track EVERY token a deployer wallet has created across ALL chains.
|
|
Score based on: scam rate, avg lifespan, total extracted, cross-chain fingerprint.
|
|
"""
|
|
result = {
|
|
"deployer": address,
|
|
"chain": chain,
|
|
"tokens_deployed": 0,
|
|
"tokens_rugged": 0,
|
|
"scam_rate": 0.0,
|
|
"avg_lifespan_days": 0.0,
|
|
"total_value_extracted_usd": 0.0,
|
|
"chains_active": [chain],
|
|
"risk_score": 0,
|
|
"risk_level": "UNKNOWN",
|
|
"flag_details": [],
|
|
"tokens": [],
|
|
}
|
|
|
|
# Get deployer's tokens from DexScreener
|
|
dex_data = await client.dexscreener(chain, address)
|
|
if "error" not in dex_data:
|
|
# The address might be a token, check if we can find deployer
|
|
pass
|
|
|
|
# For EVM chains, use Etherscan/BSCScan to find contracts created
|
|
if chain in cfg.CHAIN_SCAN_MAP:
|
|
contracts = await client.scan_get(
|
|
chain,
|
|
{
|
|
"module": "account",
|
|
"action": "txlist",
|
|
"address": address,
|
|
"startblock": 0,
|
|
"sort": "asc",
|
|
"page": 1,
|
|
"offset": 100,
|
|
},
|
|
)
|
|
if contracts.get("status") == "1":
|
|
txs = contracts.get("result", [])
|
|
contract_creations = [
|
|
tx for tx in txs if tx.get("to") == "" or tx.get("input", "").startswith("0x60806040")
|
|
]
|
|
result["tokens_deployed"] = len(contract_creations)
|
|
|
|
# For Solana, use Helius to find token creations
|
|
if chain == "sol" and cfg.HELIUS_KEY:
|
|
# Parse transactions for TokenCreate instructions
|
|
await client.helius_post(
|
|
"/transactions/parse",
|
|
{
|
|
"transactions": [] # Would need actual tx signatures
|
|
},
|
|
)
|
|
|
|
# Build risk signals
|
|
flags = []
|
|
if result["tokens_deployed"] > 5:
|
|
flags.append(f"High volume deployer: {result['tokens_deployed']} tokens")
|
|
if result["scam_rate"] > 0.7:
|
|
flags.append(f"Scam rate {result['scam_rate']:.0%} - likely serial rugger")
|
|
|
|
# Calculate composite risk score
|
|
scam_weight = min(result["scam_rate"] * 60, 60)
|
|
volume_weight = min(result["tokens_deployed"] * 2, 20)
|
|
extract_weight = min(result["total_value_extracted_usd"] / 10000, 20)
|
|
result["risk_score"] = min(int(scam_weight + volume_weight + extract_weight), 100)
|
|
result["risk_level"] = (
|
|
"CRITICAL"
|
|
if result["risk_score"] >= 80
|
|
else "HIGH"
|
|
if result["risk_score"] >= 60
|
|
else "MEDIUM"
|
|
if result["risk_score"] >= 40
|
|
else "LOW"
|
|
if result["risk_score"] >= 20
|
|
else "MINIMAL"
|
|
)
|
|
result["flag_details"] = flags
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 2: PRE-FLIGHT TRANSACTION SIMULATOR
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def preflight_simulate(
|
|
token_address: str, wallet_address: str, buy_amount_usd: float = 100, chain: str = "sol"
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Simulate a buy AND sell to show real costs, slippage, taxes, and net P&L.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"wallet": wallet_address,
|
|
"chain": chain,
|
|
"buy_simulation": {},
|
|
"sell_simulation": {},
|
|
"net_analysis": {},
|
|
"warnings": [],
|
|
"risk_flags": [],
|
|
}
|
|
|
|
# Get token data from DexScreener
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
|
|
if "error" not in dex_info:
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
price_usd = float(dex_info.get("priceUsd", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
|
|
# Buy simulation
|
|
slippage_impact = round(buy_amount_usd / max(liquidity, 1) * 100, 3) # % of pool
|
|
estimated_tokens = buy_amount_usd / max(price_usd, 0.00000001) if price_usd else 0
|
|
|
|
result["buy_simulation"] = {
|
|
"input_usd": buy_amount_usd,
|
|
"estimated_tokens": round(estimated_tokens, 6),
|
|
"price_per_token_usd": price_usd,
|
|
"slippage_pct": slippage_impact,
|
|
"pool_impact": f"You'd consume {slippage_impact:.2f}% of the liquidity pool",
|
|
"can_buy": slippage_impact < 5,
|
|
}
|
|
|
|
# Sell simulation - assume a sell tax might exist
|
|
# Check if token is sellable (vs honeypot)
|
|
sell_tax_estimate = 0 # Would be calculated from contract analysis
|
|
estimated_sell_usd = estimated_tokens * price_usd * (1 - sell_tax_estimate / 100)
|
|
sell_slippage = round(estimated_sell_usd / max(liquidity, 1) * 100, 3)
|
|
|
|
result["sell_simulation"] = {
|
|
"tokens_to_sell": round(estimated_tokens, 6),
|
|
"estimated_value_usd": round(estimated_sell_usd, 2),
|
|
"sell_tax_estimate_pct": sell_tax_estimate,
|
|
"slippage_pct": sell_slippage,
|
|
"net_return_usd": round(estimated_sell_usd - buy_amount_usd, 2),
|
|
"can_sell_profitably": estimated_sell_usd > buy_amount_usd * 0.95,
|
|
}
|
|
|
|
# Warnings
|
|
if liquidity < 10000:
|
|
result["warnings"].append(f"Low liquidity: ${liquidity:,.0f} - hard to exit positions")
|
|
if slippage_impact > 3:
|
|
result["warnings"].append(f"Your buy would move the price ~{slippage_impact:.1f}%")
|
|
if volume_24h < 1000:
|
|
result["warnings"].append(f"Very low 24h volume: ${volume_24h:,.0f}")
|
|
|
|
# Price scenario analysis
|
|
scenarios = []
|
|
for mult in [0.5, 2, 5, 10]:
|
|
future_price = price_usd * mult
|
|
future_value = estimated_tokens * future_price
|
|
future_slippage = round(future_value / max(liquidity, 1) * 100, 2)
|
|
scenarios.append(
|
|
{
|
|
"price_target": f"{mult}x",
|
|
"future_price_usd": round(future_price, 8),
|
|
"position_value_usd": round(future_value, 2),
|
|
"slippage_at_exit_pct": future_slippage,
|
|
"profit_loss_usd": round(future_value - buy_amount_usd, 2),
|
|
"exit_viable": future_slippage < 10,
|
|
}
|
|
)
|
|
result["net_analysis"] = {
|
|
"scenarios": scenarios,
|
|
"liquidity_usd": liquidity,
|
|
"volume_24h_usd": volume_24h,
|
|
"market_cap_usd": mcap,
|
|
"recommendation": "CAUTION" if liquidity < 50000 else "PROCEED",
|
|
}
|
|
else:
|
|
result["warnings"].append(f"Could not fetch token data: {dex_info.get('error', 'unknown')}")
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 3: LIQUIDITY LOCK AUTHENTICITY
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def liquidity_lock_authenticity(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Not just "is it locked?" but meaningful analysis of the lock.
|
|
Duration vs project claims, partial unlocks, LP ratio health.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"is_locked": None,
|
|
"lock_details": {},
|
|
"authenticity_score": 0,
|
|
"flags": [],
|
|
"recommendation": "UNKNOWN",
|
|
}
|
|
|
|
# Get token data
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
dex_info.get("info", {}) or {}
|
|
|
|
# Analyze lock from DexScreener info
|
|
lock_info = dex_info.get("liquidity", {})
|
|
is_locked = bool(lock_info.get("locked"))
|
|
|
|
# Heuristic analysis
|
|
liq_to_mcap = liquidity / max(mcap, 1) if mcap else 0
|
|
flags = []
|
|
|
|
if is_locked:
|
|
result["is_locked"] = True
|
|
lock_pct = float(lock_info.get("lockedPct", 0) or 0)
|
|
result["lock_details"] = {
|
|
"locked_pct": lock_pct,
|
|
"lock_source": lock_info.get("lockedSource", "unknown"),
|
|
}
|
|
|
|
if lock_pct < 50:
|
|
flags.append(f"Only {lock_pct:.0f}% of LP locked - partial lock")
|
|
if lock_pct < 100:
|
|
flags.append("Not fully locked - remaining LP can be pulled")
|
|
else:
|
|
result["is_locked"] = False
|
|
flags.append("NO liquidity lock detected - LP can be removed at any time")
|
|
|
|
# LP ratio analysis
|
|
if liq_to_mcap < 0.02:
|
|
flags.append(f"Very thin liquidity: only {liq_to_mcap:.1%} of market cap")
|
|
elif liq_to_mcap < 0.05:
|
|
flags.append(f"Low liquidity ratio: {liq_to_mcap:.1%} of market cap")
|
|
|
|
# Authenticity scoring
|
|
score = 50 # baseline
|
|
if is_locked:
|
|
score += 25
|
|
lock_pct = float(lock_info.get("lockedPct", 0) or 0)
|
|
if lock_pct >= 100:
|
|
score += 15
|
|
if lock_pct >= 80:
|
|
score += 10
|
|
else:
|
|
score -= 30 # no lock is a major red flag
|
|
|
|
# Liquidity health
|
|
if liq_to_mcap >= 0.1:
|
|
score += 10
|
|
elif liq_to_mcap >= 0.05:
|
|
score += 5
|
|
|
|
result["authenticity_score"] = max(0, min(100, score))
|
|
result["flags"] = flags
|
|
result["recommendation"] = (
|
|
"SAFE" if score >= 80 else "CAUTION" if score >= 60 else "RISKY" if score >= 40 else "DANGEROUS"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 4: VOLUME AUTHENTICITY ENGINE
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def volume_authenticity(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Detect wash trading, fake volume, bot vs organic trading.
|
|
Analyzes: round-trip txs, wash score, bot detection, volume-to-holder anomalies.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"wash_score": 0,
|
|
"volume_breakdown": {},
|
|
"bot_signals": [],
|
|
"organic_pct": 0,
|
|
"flags": [],
|
|
"risk_level": "UNKNOWN",
|
|
}
|
|
|
|
# Get token data
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
txns = dex_info.get("txns", {}) or {}
|
|
h24_buys = int(txns.get("h24", {}).get("buys", 0) or 0)
|
|
h24_sells = int(txns.get("h24", {}).get("sells", 0) or 0)
|
|
total_txs = h24_buys + h24_sells
|
|
price_change = float(dex_info.get("priceChange", {}).get("h24", 0) or 0)
|
|
|
|
# Volume-to-tx anomaly
|
|
avg_tx_value = volume_24h / max(total_txs, 1) if total_txs else 0
|
|
buy_sell_ratio = h24_buys / max(h24_sells, 1) if h24_sells else h24_buys
|
|
|
|
# Heuristic wash trading detection
|
|
wash_indicators = []
|
|
wash_score = 0
|
|
|
|
# Signal 1: Volume spikes with minimal price change (classic wash trading)
|
|
if volume_24h > 50000 and abs(price_change) < 2:
|
|
wash_score += 25
|
|
wash_indicators.append("High volume but minimal price movement - possible wash trading")
|
|
|
|
# Signal 2: Buy/sell ratio near 1.0 (symmetric trading)
|
|
if 0.8 < buy_sell_ratio < 1.2 and total_txs > 20:
|
|
wash_score += 20
|
|
wash_indicators.append(f"Symmetric buy/sell ratio ({buy_sell_ratio:.2f}) - common in wash trading")
|
|
|
|
# Signal 3: Many small transactions
|
|
if avg_tx_value < 50 and total_txs > 100:
|
|
wash_score += 15
|
|
wash_indicators.append(f"Many small transactions (avg ${avg_tx_value:.0f}/tx) - possible bot activity")
|
|
|
|
# Signal 4: Higher volume than liquidity (impossible without wash trading)
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
if volume_24h > liquidity * 3 and liquidity > 0:
|
|
wash_score += 20
|
|
wash_indicators.append(
|
|
f"24h volume ({volume_24h:,.0f}) > 3x liquidity ({liquidity:,.0f}) - wash trading likely"
|
|
)
|
|
|
|
result["wash_score"] = min(wash_score, 100)
|
|
result["volume_breakdown"] = {
|
|
"volume_24h_usd": volume_24h,
|
|
"buys_24h": h24_buys,
|
|
"sells_24h": h24_sells,
|
|
"total_txs_24h": total_txs,
|
|
"avg_tx_value_usd": round(avg_tx_value, 2),
|
|
"buy_sell_ratio": round(buy_sell_ratio, 2),
|
|
"liquidity_usd": liquidity,
|
|
"volume_to_liquidity_ratio": round(volume_24h / max(liquidity, 1), 2),
|
|
}
|
|
result["bot_signals"] = wash_indicators
|
|
result["organic_pct"] = max(0, 100 - wash_score)
|
|
result["risk_level"] = (
|
|
"CRITICAL"
|
|
if wash_score >= 70
|
|
else "HIGH"
|
|
if wash_score >= 50
|
|
else "MEDIUM"
|
|
if wash_score >= 30
|
|
else "LOW"
|
|
if wash_score >= 15
|
|
else "MINIMAL"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 5: SLOW RUG DETECTOR
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def slow_rug_detector(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Detect gradual LP removal, fee increases, slow ownership transfers.
|
|
Track liquidity drain velocity over time.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"slow_rug_score": 0,
|
|
"drain_velocity": {},
|
|
"patterns": [],
|
|
"liquidity_timeline": [],
|
|
"flags": [],
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
# Get current token data
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
current_liq = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
price_change_24h = float(dex_info.get("priceChange", {}).get("h24", 0) or 0)
|
|
price_change_6h = float(dex_info.get("priceChange", {}).get("h6", 0) or 0)
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
|
|
patterns = []
|
|
slow_rug_score = 0
|
|
|
|
# Pattern 1: Price declining while volume stays high (LP being drained)
|
|
if price_change_24h < -10 and volume_24h > current_liq * 0.5:
|
|
slow_rug_score += 30
|
|
patterns.append(f"Price down {price_change_24h:.1f}% in 24h with high volume - possible LP drain")
|
|
|
|
# Pattern 2: Steady decline with no recovery
|
|
if price_change_24h < -5 and price_change_6h < -2:
|
|
slow_rug_score += 20
|
|
patterns.append("Consistent decline across timeframes - slow bleed pattern")
|
|
|
|
# Pattern 3: Very low liquidity for the market cap
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
if mcap > 0 and current_liq / mcap < 0.03:
|
|
slow_rug_score += 25
|
|
patterns.append(f"LP is only {current_liq / mcap:.1%} of market cap - easy to drain")
|
|
|
|
# Pattern 4: High volume-to-liquidity ratio (churning through thin pool)
|
|
if current_liq > 0 and volume_24h / current_liq > 2:
|
|
slow_rug_score += 15
|
|
patterns.append(f"24h volume is {volume_24h / current_liq:.1f}x the liquidity - high churn rate")
|
|
|
|
result["slow_rug_score"] = min(slow_rug_score, 100)
|
|
result["patterns"] = patterns
|
|
result["drain_velocity"] = {
|
|
"current_liquidity_usd": current_liq,
|
|
"market_cap_usd": mcap,
|
|
"liq_to_mcap_ratio": round(current_liq / max(mcap, 1), 4),
|
|
"price_change_24h_pct": price_change_24h,
|
|
"price_change_6h_pct": price_change_6h,
|
|
"volume_to_liquidity_ratio": round(volume_24h / max(current_liq, 1), 2),
|
|
}
|
|
result["verdict"] = (
|
|
"ACTIVE_SLOW_RUG"
|
|
if slow_rug_score >= 60
|
|
else "SUSPICIOUS"
|
|
if slow_rug_score >= 40
|
|
else "WATCH"
|
|
if slow_rug_score >= 20
|
|
else "CLEAN"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 6: FUNDING SOURCE TRACER
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def funding_source_tracer(address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Trace where a wallet got its initial ETH/SOL from.
|
|
Detect: Exchange withdrawals, mixer usage, fresh wallet patterns.
|
|
"""
|
|
result = {
|
|
"address": address,
|
|
"chain": chain,
|
|
"funding_sources": [],
|
|
"risk_flags": [],
|
|
"risk_score": 0,
|
|
"risk_level": "UNKNOWN",
|
|
"first_funding": None,
|
|
"summary": "",
|
|
}
|
|
|
|
sources = []
|
|
flags = []
|
|
risk_score = 0
|
|
|
|
# For Solana - use Helius
|
|
if chain == "sol" and cfg.HELIUS_KEY:
|
|
# Get transaction history
|
|
txs = await client.helius_post("/addresses/transactions", {"addresses": [address], "options": {"limit": 50}})
|
|
if "error" not in txs:
|
|
# Parse earliest transactions for funding source
|
|
for _tx in txs if isinstance(txs, list) else txs.get("result", []):
|
|
# Heuristic: look for SOL transfers INTO this wallet
|
|
pass
|
|
|
|
# For EVM - use Etherscan/BSCScan
|
|
elif chain in cfg.CHAIN_SCAN_MAP:
|
|
txs = await client.scan_get(
|
|
chain,
|
|
{
|
|
"module": "account",
|
|
"action": "txlist",
|
|
"address": address,
|
|
"startblock": 0,
|
|
"sort": "asc",
|
|
"page": 1,
|
|
"offset": 50,
|
|
},
|
|
)
|
|
if txs.get("status") == "1":
|
|
raw_txs = txs.get("result", [])
|
|
# Find first incoming ETH transfer
|
|
incoming = [tx for tx in raw_txs if tx.get("to", "").lower() == address.lower()]
|
|
if incoming:
|
|
first_tx = incoming[0]
|
|
from_addr = first_tx.get("from", "")
|
|
value_eth = float(first_tx.get("value", 0)) / 1e18
|
|
timestamp = int(first_tx.get("timeStamp", 0))
|
|
|
|
# Classify source
|
|
known_cex = {
|
|
"0x28c6c06298d514da08ff9e8b921eb4f5bc3dd3": "Binance",
|
|
"0x3f5ce5fbfe3f6e18ecbf8c7a83bd633a63a4b7e": "Binance 2",
|
|
"0x56eddb81aa0709df9a94d390c7b0b0b0b0b0b0b0": "Coinbase",
|
|
"0x7a250d5630b4cf539739df2c5dacb4c659f2448d": "Uniswap Router",
|
|
}
|
|
source_name = known_cex.get(from_addr.lower(), "Unknown")
|
|
is_cex = from_addr.lower() in known_cex
|
|
is_fresh = len(raw_txs) < 5
|
|
|
|
sources.append(
|
|
{
|
|
"address": from_addr,
|
|
"name": source_name,
|
|
"is_cex": is_cex,
|
|
"value_eth": round(value_eth, 6),
|
|
"timestamp": timestamp,
|
|
"block": first_tx.get("blockNumber", ""),
|
|
}
|
|
)
|
|
|
|
if is_fresh:
|
|
flags.append("Fresh wallet - funded just before interaction")
|
|
risk_score += 25
|
|
if not is_cex and value_eth < 1:
|
|
flags.append("Small non-CEX funding - possible burner wallet")
|
|
risk_score += 15
|
|
|
|
result["funding_sources"] = sources
|
|
result["risk_flags"] = flags
|
|
result["risk_score"] = min(risk_score, 100)
|
|
result["risk_level"] = (
|
|
"CRITICAL" if risk_score >= 60 else "HIGH" if risk_score >= 40 else "MEDIUM" if risk_score >= 20 else "LOW"
|
|
)
|
|
result["summary"] = f"Found {len(sources)} funding source(s). {len(flags)} risk flag(s) detected."
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 7: OWNERSHIP THEATER DETECTOR
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def ownership_theater_detector(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Detect fake renunciation: hidden admin roles, upgradeable proxies,
|
|
secondary controlled addresses, remaining privileges.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"is_renounced": None,
|
|
"renunciation_authentic": None,
|
|
"hidden_privileges": [],
|
|
"risk_flags": [],
|
|
"theater_score": 0,
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
# Get DexScreener data for contract info
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
|
|
privileges = []
|
|
flags = []
|
|
theater_score = 0
|
|
|
|
# Check if ownership is marked as renounced
|
|
is_renounced = False
|
|
if "error" not in dex_info:
|
|
dex_info.get("info", {}) or {}
|
|
# Check socials and tags
|
|
tags = dex_info.get("tags", []) or []
|
|
if "renounced" in [str(t).lower() for t in tags]:
|
|
is_renounced = True
|
|
|
|
# For EVM tokens - check contract source
|
|
if chain in cfg.CHAIN_SCAN_MAP:
|
|
contract_info = await client.scan_get(
|
|
chain,
|
|
{
|
|
"module": "contract",
|
|
"action": "getsourcecode",
|
|
"address": token_address,
|
|
},
|
|
)
|
|
if contract_info.get("status") == "1":
|
|
source_data = contract_info.get("result", [{}])[0] if contract_info.get("result") else {}
|
|
is_proxy = bool(source_data.get("Proxy", "0") == "1" or source_data.get("Implementation", ""))
|
|
source_data.get("ContractName", "Unknown")
|
|
source_data.get("OptimizationUsed", "0")
|
|
|
|
if is_proxy:
|
|
flags.append("UPGRADEABLE PROXY - owner can change contract logic at any time")
|
|
theater_score += 40
|
|
privileges.append("Proxy contract with upgradeable implementation")
|
|
|
|
# Check for common privilege functions in ABI
|
|
abi_str = source_data.get("ABI", "")
|
|
if "mint" in abi_str.lower():
|
|
flags.append("Mint function found - tokens can be created out of thin air")
|
|
theater_score += 30
|
|
privileges.append("Mint function exists")
|
|
if "pause" in abi_str.lower():
|
|
flags.append("Pause function - contract can be frozen")
|
|
theater_score += 15
|
|
privileges.append("Pause function exists")
|
|
if "blacklist" in abi_str.lower():
|
|
flags.append("Blacklist function - selective wallet blocking")
|
|
theater_score += 25
|
|
privileges.append("Blacklist function exists")
|
|
|
|
result["is_renounced"] = is_renounced
|
|
result["hidden_privileges"] = privileges
|
|
result["risk_flags"] = flags
|
|
result["theater_score"] = min(theater_score, 100)
|
|
|
|
if is_renounced and theater_score > 0:
|
|
result["renunciation_authentic"] = False
|
|
result["verdict"] = "OWNERSHIP_THEATER"
|
|
elif is_renounced:
|
|
result["renunciation_authentic"] = True
|
|
result["verdict"] = "GENUINE_RENUNCIATION"
|
|
elif theater_score > 30:
|
|
result["verdict"] = "SUSPICIOUS_PRIVILEGES"
|
|
else:
|
|
result["verdict"] = "NOT_RENOUNCED"
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 8: EXIT LIQUIDITY CALCULATOR
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def exit_liquidity_calculator(
|
|
token_address: str, position_usd: float = 1000, chain: str = "sol"
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Can you actually sell? Depth-at-price analysis, sell cascade simulation,
|
|
individual holder exit impact, liquidity concentration.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"position_usd": position_usd,
|
|
"exit_analysis": {},
|
|
"cascade_simulation": [],
|
|
"flags": [],
|
|
"recommendation": "UNKNOWN",
|
|
}
|
|
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
float(dex_info.get("priceUsd", 0) or 0)
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
|
|
# Core calculation: how much of the pool would your exit consume?
|
|
position_pct_of_liq = (position_usd / max(liquidity, 1)) * 100
|
|
slippage_estimate = position_pct_of_liq * 0.5 # rough estimate
|
|
max_recoverable = min(position_usd, liquidity * 0.8) # can't drain more than 80% of pool
|
|
actual_recovery_pct = (max_recoverable / max(position_usd, 1)) * 100
|
|
|
|
flags = []
|
|
if position_pct_of_liq > 10:
|
|
flags.append(f"Your position is {position_pct_of_liq:.1f}% of the pool - exiting will crash the price")
|
|
if position_pct_of_liq > 50:
|
|
flags.append("CRITICAL: Position exceeds half the liquidity - exit is nearly impossible")
|
|
if liquidity < 10000:
|
|
flags.append(f"Extremely thin liquidity (${liquidity:,.0f}) - large exits impossible")
|
|
|
|
# Cascade simulation: what if top holders sell?
|
|
cascade = []
|
|
for pct_sell in [5, 10, 20, 30, 50]:
|
|
sell_value = mcap * (pct_sell / 100) * 0.1 # rough: tokens worth pct of supply hitting thin pool
|
|
price_impact = (sell_value / max(liquidity, 1)) * 50 # simplified AMM model
|
|
cascade.append(
|
|
{
|
|
"holders_selling_pct": pct_sell,
|
|
"estimated_price_drop_pct": round(min(price_impact, 95), 1),
|
|
"liquidity_remaining_usd": round(max(0, liquidity - sell_value), 0),
|
|
}
|
|
)
|
|
|
|
result["exit_analysis"] = {
|
|
"liquidity_usd": liquidity,
|
|
"market_cap_usd": mcap,
|
|
"position_pct_of_liquidity": round(position_pct_of_liq, 2),
|
|
"estimated_slippage_pct": round(slippage_estimate, 2),
|
|
"max_recoverable_usd": round(max_recoverable, 2),
|
|
"recovery_rate_pct": round(actual_recovery_pct, 1),
|
|
"volume_24h_usd": volume_24h,
|
|
}
|
|
result["cascade_simulation"] = cascade
|
|
result["flags"] = flags
|
|
result["recommendation"] = (
|
|
"DO_NOT_BUY"
|
|
if actual_recovery_pct < 30
|
|
else "HIGH_RISK"
|
|
if actual_recovery_pct < 50
|
|
else "CAUTION"
|
|
if actual_recovery_pct < 80
|
|
else "REASONABLE_LIQUIDITY"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 9: TOKEN LIFESPAN PREDICTOR
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def token_lifespan_predictor(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
AI-driven survival analysis. Compare against historical rug patterns.
|
|
Predict survival probability at 7d, 30d, 90d.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"survival_probability": {
|
|
"7d_pct": 0,
|
|
"30d_pct": 0,
|
|
"90d_pct": 0,
|
|
},
|
|
"pattern_match": {},
|
|
"risk_flags": [],
|
|
"rug_probability_pct": 0,
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["risk_flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
float(dex_info.get("priceChange", {}).get("m5", 0) or 0)
|
|
price_change_1h = float(dex_info.get("priceChange", {}).get("h1", 0) or 0)
|
|
float(dex_info.get("priceChange", {}).get("h6", 0) or 0)
|
|
price_change_24h = float(dex_info.get("priceChange", {}).get("h24", 0) or 0)
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
pair_age_days = 0
|
|
|
|
# Try to get pair creation date
|
|
pair_created = dex_info.get("pairCreatedAt", "")
|
|
if pair_created:
|
|
try:
|
|
created_dt = datetime.fromisoformat(pair_created.replace("Z", "+00:00"))
|
|
pair_age_days = (datetime.now(UTC) - created_dt).days
|
|
except Exception:
|
|
pass
|
|
|
|
# Collect risk signals
|
|
risk_flags = []
|
|
risk_score = 30 # baseline for any new token
|
|
|
|
# Signal: Very new token
|
|
if pair_age_days < 1:
|
|
risk_score += 20
|
|
risk_flags.append("Token less than 1 day old - extreme risk")
|
|
elif pair_age_days < 7:
|
|
risk_score += 15
|
|
risk_flags.append("Token less than 1 week old - high risk")
|
|
elif pair_age_days < 30:
|
|
risk_score += 5
|
|
risk_flags.append("Token less than 1 month old - elevated risk")
|
|
|
|
# Signal: Price patterns
|
|
if price_change_1h < -20:
|
|
risk_score += 15
|
|
risk_flags.append(f"1h price drop: {price_change_1h:.1f}% - active dumping")
|
|
if price_change_24h < -50:
|
|
risk_score += 20
|
|
risk_flags.append(f"24h price crash: {price_change_24h:.1f}% - likely rug in progress")
|
|
|
|
# Signal: Low liquidity
|
|
if liquidity < 5000:
|
|
risk_score += 20
|
|
risk_flags.append(f"Very low liquidity: ${liquidity:,.0f}")
|
|
elif liquidity < 50000:
|
|
risk_score += 10
|
|
|
|
# Signal: Volume anomaly
|
|
if mcap > 0 and volume_24h / max(mcap, 1) > 3:
|
|
risk_score += 10
|
|
risk_flags.append("Volume exceeds 3x market cap - likely wash trading")
|
|
|
|
# Pattern matching
|
|
pattern = "UNKNOWN"
|
|
if pair_age_days < 1 and price_change_1h > 100:
|
|
pattern = "PUMP_AND_DUMP_LAUNCH"
|
|
risk_score += 15
|
|
elif pair_age_days < 7 and liquidity < 10000:
|
|
pattern = "LOW_LIQ_RUG"
|
|
risk_score += 10
|
|
elif price_change_24h > 200 and volume_24h > mcap:
|
|
pattern = "HYPE_PUMP"
|
|
elif liquidity < mcap * 0.03:
|
|
pattern = "THIN_LIQ_EXIT_TRAP"
|
|
risk_score += 10
|
|
elif pair_age_days > 30 and liquidity > mcap * 0.1:
|
|
pattern = "ESTABLISHED_TOKEN"
|
|
|
|
risk_score = min(risk_score, 100)
|
|
rug_prob = risk_score
|
|
|
|
# Survival probability (inverse of rug probability, with time decay)
|
|
result["survival_probability"] = {
|
|
"7d_pct": max(5, 100 - rug_prob * 0.6),
|
|
"30d_pct": max(3, 100 - rug_prob * 0.8),
|
|
"90d_pct": max(1, 100 - rug_prob * 0.95),
|
|
}
|
|
result["pattern_match"] = {"detected_pattern": pattern, "confidence": "medium"}
|
|
result["risk_flags"] = risk_flags
|
|
result["rug_probability_pct"] = rug_prob
|
|
result["verdict"] = (
|
|
"RUG_IMMINENT"
|
|
if rug_prob >= 80
|
|
else "HIGH_RISK"
|
|
if rug_prob >= 60
|
|
else "ELEVATED_RISK"
|
|
if rug_prob >= 40
|
|
else "MODERATE"
|
|
if rug_prob >= 25
|
|
else "REASONABLE"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 10: POST-PURCHASE MONITORING
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def post_purchase_monitor(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Detect changes AFTER buying: contract modifications, new admin functions,
|
|
fee structure changes, liquidity unlocks, wallet behavior shifts.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"current_state": {},
|
|
"alerts": [],
|
|
"risk_changes": [],
|
|
"overall_trend": "STABLE",
|
|
}
|
|
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["alerts"].append("Could not fetch token data")
|
|
return result
|
|
|
|
# Baseline current state
|
|
price_usd = float(dex_info.get("priceUsd", 0) or 0)
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
price_change_5m = float(dex_info.get("priceChange", {}).get("m5", 0) or 0)
|
|
price_change_1h = float(dex_info.get("priceChange", {}).get("h1", 0) or 0)
|
|
price_change_6h = float(dex_info.get("priceChange", {}).get("h6", 0) or 0)
|
|
price_change_24h = float(dex_info.get("priceChange", {}).get("h24", 0) or 0)
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
|
|
result["current_state"] = {
|
|
"price_usd": price_usd,
|
|
"liquidity_usd": liquidity,
|
|
"market_cap_usd": mcap,
|
|
"price_change_5m_pct": price_change_5m,
|
|
"price_change_1h_pct": price_change_1h,
|
|
"price_change_6h_pct": price_change_6h,
|
|
"price_change_24h_pct": price_change_24h,
|
|
"volume_24h_usd": volume_24h,
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
|
|
alerts = []
|
|
|
|
# Alert: Rapid price decline
|
|
if price_change_1h < -20:
|
|
alerts.append(
|
|
{
|
|
"severity": "CRITICAL",
|
|
"type": "PRICE_CRASH",
|
|
"message": f"Price dropped {price_change_1h:.1f}% in the last hour - possible rug or sell-off",
|
|
}
|
|
)
|
|
elif price_change_1h < -10:
|
|
alerts.append(
|
|
{
|
|
"severity": "HIGH",
|
|
"type": "PRICE_DROP",
|
|
"message": f"Price down {price_change_1h:.1f}% in 1 hour",
|
|
}
|
|
)
|
|
|
|
# Alert: Liquidity drop
|
|
if liquidity < 5000:
|
|
alerts.append(
|
|
{
|
|
"severity": "CRITICAL",
|
|
"type": "LOW_LIQUIDITY",
|
|
"message": f"Liquidity critically low: ${liquidity:,.0f}",
|
|
}
|
|
)
|
|
|
|
# Alert: Volume anomaly
|
|
if volume_24h > 0 and liquidity > 0 and volume_24h / liquidity > 5:
|
|
alerts.append(
|
|
{
|
|
"severity": "MEDIUM",
|
|
"type": "HIGH_VOLUME_RATIO",
|
|
"message": f"24h volume is {volume_24h / liquidity:.1f}x the liquidity",
|
|
}
|
|
)
|
|
|
|
# Alert: Sudden 5-minute moves
|
|
if abs(price_change_5m) > 10:
|
|
direction = "surge" if price_change_5m > 0 else "drop"
|
|
alerts.append(
|
|
{
|
|
"severity": "HIGH",
|
|
"type": "RAPID_MOVE",
|
|
"message": f"Price {direction}d {abs(price_change_5m):.1f}% in 5 minutes",
|
|
}
|
|
)
|
|
|
|
# Overall trend
|
|
if price_change_24h < -30:
|
|
result["overall_trend"] = "DUMPING"
|
|
elif price_change_24h < -10:
|
|
result["overall_trend"] = "DECLINING"
|
|
elif price_change_24h > 30:
|
|
result["overall_trend"] = "PUMPING"
|
|
elif price_change_24h > 10:
|
|
result["overall_trend"] = "RISING"
|
|
else:
|
|
result["overall_trend"] = "STABLE"
|
|
|
|
result["alerts"] = alerts
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 11: CROSS-CHAIN IDENTITY RESOLUTION
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def cross_chain_identity(address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Same entity, multiple chains. Detect deployer patterns, funding correlation,
|
|
simultaneous launches, shared wallet infrastructure.
|
|
"""
|
|
result = {
|
|
"address": address,
|
|
"source_chain": chain,
|
|
"cross_chain_profiles": [],
|
|
"linked_addresses": [],
|
|
"shared_patterns": [],
|
|
"risk_flags": [],
|
|
"identity_confidence": 0,
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
profiles = []
|
|
linked = []
|
|
flags = []
|
|
|
|
# Search for the token on other chains via DexScreener
|
|
dex_info = await client.dexscreener(chain, address)
|
|
|
|
# Search for same/similar tokens on other chains
|
|
token_symbol = ""
|
|
if "error" not in dex_info:
|
|
dex_info.get("baseToken", {}).get("name", "") or dex_info.get("pairAddress", "")
|
|
token_symbol = dex_info.get("baseToken", {}).get("symbol", "") or ""
|
|
|
|
# Cross-reference on other chains
|
|
if token_symbol:
|
|
try:
|
|
resp = await client.http.get(f"https://api.dexscreener.com/latest/dex/search?q={token_symbol}")
|
|
if resp.status_code == 200:
|
|
search_data = resp.json()
|
|
pairs = search_data.get("pairs") or []
|
|
for pair in pairs[:20]:
|
|
pair_chain = pair.get("chainId", "")
|
|
if pair_chain != chain and pair_chain in cfg.CHAIN_DEXSCREENER_MAP.values():
|
|
profiles.append(
|
|
{
|
|
"chain": pair_chain,
|
|
"address": pair.get("baseToken", {}).get("address", ""),
|
|
"name": pair.get("baseToken", {}).get("name", ""),
|
|
"symbol": pair.get("baseToken", {}).get("symbol", ""),
|
|
"price_usd": pair.get("priceUsd", ""),
|
|
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0)
|
|
if pair.get("liquidity")
|
|
else 0,
|
|
"volume_24h_usd": pair.get("volume", {}).get("h24", 0) if pair.get("volume") else 0,
|
|
"pair_created": pair.get("pairCreatedAt", ""),
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Cross-chain search failed: {e}")
|
|
|
|
# Risk analysis of cross-chain presence
|
|
if len(profiles) >= 3:
|
|
flags.append(f"Same symbol found on {len(profiles)} chains - possible coordinated multi-chain scam")
|
|
elif len(profiles) >= 2:
|
|
flags.append(f"Token exists on {len(profiles)} chains - verify they're the same team")
|
|
|
|
result["cross_chain_profiles"] = profiles
|
|
result["linked_addresses"] = linked
|
|
result["shared_patterns"] = []
|
|
result["risk_flags"] = flags
|
|
result["identity_confidence"] = min(len(profiles) * 25, 100)
|
|
result["verdict"] = (
|
|
"MULTI_CHAIN_ENTITY" if len(profiles) >= 3 else "POSSIBLE_DUPLICATE" if len(profiles) >= 1 else "SINGLE_CHAIN"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 12: SOCIAL-ONCHAIN CORRELATION
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def social_onchain_correlation(token_address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Map hype cycle: wallet activity spikes correlated with social mentions.
|
|
Influencer wallet tracking. Coordinated pump timing.
|
|
"""
|
|
result = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"social_signals": {},
|
|
"onchain_activity": {},
|
|
"correlation": {},
|
|
"flags": [],
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
# Get on-chain data
|
|
dex_info = await client.dexscreener(chain, token_address)
|
|
if "error" in dex_info:
|
|
result["flags"].append("Could not fetch token data")
|
|
return result
|
|
|
|
# On-chain metrics
|
|
volume_24h = float(dex_info.get("volume", {}).get("h24", 0) or 0)
|
|
volume_6h = float(dex_info.get("volume", {}).get("h6", 0) or 0)
|
|
price_change_24h = float(dex_info.get("priceChange", {}).get("h24", 0) or 0)
|
|
price_change_6h = float(dex_info.get("priceChange", {}).get("h6", 0) or 0)
|
|
mcap = float(dex_info.get("marketCap", 0) or 0)
|
|
liquidity = float(dex_info.get("liquidity", {}).get("usd", 0) or 0)
|
|
|
|
# Social info from DexScreener
|
|
info = dex_info.get("info", {}) or {}
|
|
socials = info.get("socials", []) if isinstance(info, dict) else []
|
|
|
|
# Social signal extraction
|
|
social_data = {
|
|
"has_website": bool(info.get("websites")) if isinstance(info, dict) and info.get("websites") else False,
|
|
"social_count": len(socials),
|
|
"socials": socials,
|
|
"description": info.get("description", "") if isinstance(info, dict) else "",
|
|
}
|
|
|
|
# Correlation analysis
|
|
flags = []
|
|
correlation_score = 0
|
|
|
|
# Check for pump pattern (price spike without organic social presence)
|
|
if price_change_24h > 100 and volume_24h > mcap * 0.5:
|
|
correlation_score += 20
|
|
flags.append("Massive price spike with volume exceeding 50% of market cap - possible coordinated pump")
|
|
|
|
if price_change_6h > 50 and volume_6h > volume_24h * 0.5:
|
|
correlation_score += 15
|
|
flags.append("Recent 6h volume spike suggests coordinated buying")
|
|
|
|
if mcap > 0 and volume_24h / max(mcap, 1) > 2:
|
|
correlation_score += 10
|
|
flags.append("Volume-to-mcap ratio exceeds 2x - extreme activity for size")
|
|
|
|
# Suspicious social patterns
|
|
if social_data["social_count"] == 0:
|
|
correlation_score += 15
|
|
flags.append("No social links found - possible anonymous/scam project")
|
|
|
|
result["social_signals"] = social_data
|
|
result["onchain_activity"] = {
|
|
"volume_24h_usd": volume_24h,
|
|
"volume_6h_usd": volume_6h,
|
|
"price_change_24h_pct": price_change_24h,
|
|
"price_change_6h_pct": price_change_6h,
|
|
"market_cap_usd": mcap,
|
|
"liquidity_usd": liquidity,
|
|
"volume_to_mcap_ratio": round(volume_24h / max(mcap, 1), 2),
|
|
}
|
|
result["correlation"] = {
|
|
"correlation_score": correlation_score,
|
|
"pump_likelihood": "HIGH" if correlation_score >= 40 else "MEDIUM" if correlation_score >= 20 else "LOW",
|
|
}
|
|
result["flags"] = flags
|
|
result["verdict"] = (
|
|
"COORDINATED_PUMP"
|
|
if correlation_score >= 40
|
|
else "SUSPICIOUS_ACTIVITY"
|
|
if correlation_score >= 20
|
|
else "NORMAL"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# FEATURE 13: THREAT INTEL CHECK (GoPlus, Webacy, Chainabuse)
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def threat_intel_check(address: str, chain: str = "sol") -> dict[str, Any]:
|
|
"""
|
|
Aggregate on-chain threat intelligence from GoPlus, Webacy, and Chainabuse.
|
|
"""
|
|
result = {
|
|
"address": address,
|
|
"chain": chain,
|
|
"goplus": None,
|
|
"webacy": None,
|
|
"chainabuse": None,
|
|
"risk_flags": [],
|
|
"risk_score": 0,
|
|
"verdict": "UNKNOWN",
|
|
}
|
|
|
|
flags = []
|
|
score = 0
|
|
|
|
# 1. GoPlus Token Security
|
|
goplus_data = await client.goplus_get(chain, address)
|
|
if "error" not in goplus_data:
|
|
result["goplus"] = goplus_data
|
|
# Parse GoPlus results
|
|
token_data = goplus_data.get("result", {}).get(address.lower(), {})
|
|
if token_data:
|
|
if token_data.get("is_honeypot") == "1":
|
|
flags.append("GoPlus: Honeypot detected")
|
|
score += 40
|
|
if token_data.get("is_mintable") == "1":
|
|
flags.append("GoPlus: Token is mintable")
|
|
score += 15
|
|
if token_data.get("is_proxy") == "1":
|
|
flags.append("GoPlus: Proxy contract detected")
|
|
score += 10
|
|
if token_data.get("owner_change_balance") == "1":
|
|
flags.append("GoPlus: Owner can change balance")
|
|
score += 20
|
|
|
|
# 2. Webacy Threat Intel
|
|
webacy_data = await client.webacy_get(chain, address)
|
|
if "error" not in webacy_data:
|
|
result["webacy"] = webacy_data
|
|
# Webacy returns risk scores and flags
|
|
if isinstance(webacy_data, dict):
|
|
risk_score = webacy_data.get("risk_score", 0)
|
|
if risk_score > 50:
|
|
flags.append(f"Webacy: High risk score ({risk_score})")
|
|
score += 20
|
|
|
|
# 3. Chainabuse Reports
|
|
chainabuse_data = await client.chainabuse_get(address)
|
|
if "error" not in chainabuse_data:
|
|
result["chainabuse"] = chainabuse_data
|
|
reports = chainabuse_data.get("reports", [])
|
|
if isinstance(reports, list) and len(reports) > 0:
|
|
flags.append(f"Chainabuse: {len(reports)} abuse report(s) found")
|
|
score += 30
|
|
|
|
result["risk_flags"] = flags
|
|
result["risk_score"] = min(score, 100)
|
|
result["verdict"] = (
|
|
"CRITICAL" if score >= 60 else "HIGH_RISK" if score >= 40 else "SUSPICIOUS" if score >= 20 else "CLEAN"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# COMPOSITE: FULL DEEP INTELLIGENCE REPORT
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def full_deep_intel(token_address: str, chain: str = "sol", wallet: str | None = None) -> dict[str, Any]:
|
|
"""
|
|
Run ALL 12 features and produce a composite deep intelligence report.
|
|
"""
|
|
logger.info(f"Running full deep intel for {token_address} on {chain}")
|
|
|
|
# Run all analyses in parallel
|
|
[
|
|
deployer_reputation(token_address, chain),
|
|
preflight_simulate(token_address, wallet or "0x0000000000000000000000000000000000000000", 100, chain),
|
|
liquidity_lock_authenticity(token_address, chain),
|
|
volume_authenticity(token_address, chain),
|
|
slow_rug_detector(token_address, chain),
|
|
funding_source_tracer(token_address, chain),
|
|
ownership_theater_detector(token_address, chain),
|
|
exit_liquidity_calculator(token_address, 1000, chain),
|
|
token_lifespan_predictor(token_address, chain),
|
|
post_purchase_monitor(token_address, chain),
|
|
cross_chain_identity(token_address, chain),
|
|
social_onchain_correlation(token_address, chain),
|
|
threat_intel_check(token_address, chain),
|
|
]
|
|
|
|
feature_names = [
|
|
"deployer_reputation",
|
|
"preflight_simulation",
|
|
"liquidity_lock_authenticity",
|
|
"volume_authenticity",
|
|
"slow_rug_detector",
|
|
"funding_source_tracer",
|
|
"ownership_theater",
|
|
"exit_liquidity",
|
|
"lifespan_predictor",
|
|
"post_purchase_monitor",
|
|
"cross_chain_identity",
|
|
"social_onchain_correlation",
|
|
"threat_intel_check",
|
|
]
|
|
|
|
report = {
|
|
"token": token_address,
|
|
"chain": chain,
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
"features": {},
|
|
"composite_score": 0,
|
|
"composite_risk_level": "UNKNOWN",
|
|
"summary": "",
|
|
"top_flags": [],
|
|
}
|
|
|
|
all_flags = []
|
|
scores = []
|
|
|
|
for name, result in zip(feature_names, results, strict=False):
|
|
if isinstance(result, Exception):
|
|
report["features"][name] = {"error": str(result)}
|
|
else:
|
|
report["features"][name] = result
|
|
|
|
# Extract scores
|
|
if name == "deployer_reputation" and "risk_score" in result:
|
|
scores.append(result["risk_score"])
|
|
elif name == "volume_authenticity" and "wash_score" in result:
|
|
scores.append(result["wash_score"])
|
|
elif name == "slow_rug_detector" and "slow_rug_score" in result:
|
|
scores.append(result["slow_rug_score"])
|
|
elif name == "ownership_theater" and "theater_score" in result:
|
|
scores.append(result["theater_score"])
|
|
elif name == "lifespan_predictor" and "rug_probability_pct" in result:
|
|
scores.append(result["rug_probability_pct"])
|
|
|
|
# Collect flags
|
|
for flag_key in ["flag_details", "flags", "risk_flags", "patterns", "alerts"]:
|
|
if flag_key in result:
|
|
items = result[flag_key]
|
|
if isinstance(items, list):
|
|
for item in items:
|
|
if isinstance(item, dict):
|
|
all_flags.append(item.get("message", str(item)))
|
|
else:
|
|
all_flags.append(str(item))
|
|
|
|
# Composite score (average of all risk scores)
|
|
composite = sum(scores) / max(len(scores), 1) if scores else 0
|
|
report["composite_score"] = round(composite, 1)
|
|
report["composite_risk_level"] = (
|
|
"EXTREME_DANGER"
|
|
if composite >= 80
|
|
else "HIGH_RISK"
|
|
if composite >= 60
|
|
else "ELEVATED_RISK"
|
|
if composite >= 40
|
|
else "MODERATE"
|
|
if composite >= 25
|
|
else "LOW_RISK"
|
|
if composite >= 10
|
|
else "SAFE"
|
|
)
|
|
|
|
# Top flags
|
|
unique_flags = list(dict.fromkeys(all_flags))[:10] # Dedupe, top 10
|
|
report["top_flags"] = unique_flags
|
|
report["summary"] = (
|
|
f"Composite risk: {composite:.0f}/100. {len(unique_flags)} flag(s) detected across 12 intelligence modules."
|
|
)
|
|
|
|
return report
|