5781 lines
213 KiB
Python
Executable file
5781 lines
213 KiB
Python
Executable file
"""
|
||
RMI x402 Security Tools — Production Implementation
|
||
=====================================================
|
||
10 paid x402-powered security APIs with multi-layer fallback chains.
|
||
Every tool delivers quality results no matter what — fallback into fallback.
|
||
|
||
Built on existing bot infrastructure:
|
||
risk_engine.py, wallet_persona.py, smart_money.py, scanner_wrapper.py
|
||
fallback_system.py, cluster_monitor.py, launch_detector.py, token_statistics.py
|
||
url_scam_detector.py, sentiment_radar.py, enrichment_engine.py, api_sourcer.py
|
||
|
||
Payment: x402 USDC on Base + Solana via Cloudflare Workers.
|
||
Guarantee: Every paid scan returns data or auto-refund.
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import os
|
||
from datetime import datetime
|
||
from typing import ClassVar, Any
|
||
|
||
import aiohttp
|
||
from fastapi import APIRouter, HTTPException, Request
|
||
from pydantic import BaseModel, Field
|
||
|
||
logger = logging.getLogger("x402_tools")
|
||
|
||
router = APIRouter(prefix="/api/v1/x402-tools", tags=["x402-security-tools"])
|
||
|
||
# Caching shield — all data calls route through cache → rate limit → provider chain
|
||
from app.caching_shield.service_mcp import get_service_mcp
|
||
from app.caching_shield.tool_data import td
|
||
|
||
_svc_mcp = get_service_mcp()
|
||
|
||
# ── Data Sources (multi-layer fallback) ─────────────────────────
|
||
|
||
# Free public RPCs for blockchain queries
|
||
FREE_RPCS = {
|
||
"solana": [
|
||
"https://api.mainnet-beta.solana.com",
|
||
"https://solana.publicnode.com",
|
||
"https://api.devnet.solana.com",
|
||
],
|
||
"base": [
|
||
"https://base.llamarpc.com",
|
||
"https://base.publicnode.com",
|
||
"https://developer-access-mainnet.base.org",
|
||
],
|
||
"ethereum": [
|
||
"https://eth.llamarpc.com",
|
||
"https://ethereum.publicnode.com",
|
||
"https://rpc.ankr.com/eth",
|
||
],
|
||
"bsc": [
|
||
"https://bsc-dataseed.binance.org",
|
||
"https://bsc.publicnode.com",
|
||
],
|
||
}
|
||
|
||
# Free API endpoints (no key required)
|
||
FREE_APIS = {
|
||
"dexscreener": "https://api.dexscreener.com/latest/dex",
|
||
"coingecko": "https://api.coingecko.com/api/v3",
|
||
"birdeye_public": "https://public-api.birdeye.so",
|
||
"jupiter": "https://api.jup.ag",
|
||
"defillama": "https://api.llama.fi",
|
||
"pumpfun": "https://frontend-api.pump.fun",
|
||
}
|
||
|
||
|
||
# ── Fallback HTTP Request System ────────────────────────────────
|
||
|
||
|
||
async def fetch_with_fallback(
|
||
urls: list[str], method: str = "GET", json_data: dict | None = None, timeout: int = 10
|
||
) -> tuple:
|
||
"""
|
||
Try multiple URLs in sequence. Returns (data, source_url) on first success.
|
||
Falls back from primary to secondary to tertiary sources.
|
||
"""
|
||
async with aiohttp.ClientSession() as session:
|
||
for url in urls:
|
||
try:
|
||
if method == "GET":
|
||
async with session.get(
|
||
url, timeout=aiohttp.ClientTimeout(total=timeout)
|
||
) as resp:
|
||
if resp.status == 200:
|
||
data = await resp.json()
|
||
return data, url
|
||
elif method == "POST" and json_data:
|
||
async with session.post(
|
||
url, json=json_data, timeout=aiohttp.ClientTimeout(total=timeout)
|
||
) as resp:
|
||
if resp.status == 200:
|
||
data = await resp.json()
|
||
return data, url
|
||
except Exception as e:
|
||
logger.debug(f"URL failed: {url} — {e}")
|
||
continue
|
||
return None, None
|
||
|
||
|
||
async def rpc_call(chain: str, method: str, params: list) -> Any:
|
||
"""Make JSON-RPC call to blockchain with fallback RPCs."""
|
||
rpcs = FREE_RPCS.get(chain, FREE_RPCS.get("solana"))
|
||
urls = [f"{rpc}" for rpc in rpcs]
|
||
payloads = [{"jsonrpc": "2.0", "id": 1, "method": method, "params": params} for _ in urls]
|
||
|
||
async with aiohttp.ClientSession() as session:
|
||
for url, payload in zip(urls, payloads, strict=False):
|
||
try:
|
||
async with session.post(
|
||
url, json=payload, timeout=aiohttp.ClientTimeout(total=10)
|
||
) as resp:
|
||
if resp.status == 200:
|
||
result = await resp.json()
|
||
return result.get("result")
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
|
||
# ── Request Models ──────────────────────────────────────────────
|
||
|
||
|
||
class TokenRequest(BaseModel):
|
||
address: str
|
||
chain: str = "solana"
|
||
|
||
|
||
class WalletRequest(BaseModel):
|
||
address: str
|
||
chain: str = "solana"
|
||
|
||
|
||
class SmartMoneyRequest(BaseModel):
|
||
chain: str = "solana"
|
||
threshold: float = 10000.0
|
||
limit: int = 20
|
||
|
||
|
||
class URLRequest(BaseModel):
|
||
url: str
|
||
|
||
|
||
class SentimentRequest(BaseModel):
|
||
token: str
|
||
chain: str = "solana"
|
||
|
||
|
||
class ClusterRequest(BaseModel):
|
||
address: str
|
||
chain: str = "solana"
|
||
depth: int = 3
|
||
|
||
|
||
class InsiderRequest(BaseModel):
|
||
creator_address: str
|
||
chain: str = "solana"
|
||
|
||
|
||
class TwitterRequest(BaseModel):
|
||
query: str
|
||
user: str | None = None
|
||
handle: str | None = None
|
||
tweet_id: str | None = None
|
||
|
||
|
||
class MultiTokenRequest(BaseModel):
|
||
addresses: list[str]
|
||
chain: str = "solana"
|
||
|
||
|
||
class WalletListRequest(BaseModel):
|
||
addresses: list[str]
|
||
chain: str = "solana"
|
||
|
||
|
||
class MarketRequest(BaseModel):
|
||
chain: str = "all"
|
||
hours: int = 24
|
||
|
||
|
||
class GenericRequest(BaseModel):
|
||
address: str | None = None
|
||
chain: str = "solana"
|
||
token: str | None = None
|
||
query: str | None = None
|
||
hours: int = 24
|
||
threshold: float = 10000.0
|
||
limit: int = 20
|
||
|
||
|
||
# ── Helper: Record x402 Payment ────────────────────────────────
|
||
|
||
|
||
# TOOL 1: Deep Contract Audit ($0.50)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _audit_solana(address: str) -> dict:
|
||
"""Full audit for Solana tokens with 5-layer fallback."""
|
||
result = {"chain": "solana", "address": address, "sources_used": []}
|
||
|
||
# Layer 1: DexScreener (free, no key)
|
||
try:
|
||
data, _src = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
result["dexscreener"] = {
|
||
"price_usd": pair.get("priceUsd", 0),
|
||
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
||
"volume_24h": pair.get("volume", {}).get("h24", 0),
|
||
"price_change_24h": pair.get("priceChange", {}).get("h24", 0),
|
||
"buyers_24h": pair.get("txns", {}).get("h24", {}).get("buys", 0),
|
||
"sellers_24h": pair.get("txns", {}).get("h24", {}).get("sells", 0),
|
||
}
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: Solana RPC — get account info
|
||
try:
|
||
account = await rpc_call("solana", "getAccountInfo", [address, {"encoding": "jsonParsed"}])
|
||
if account and account.get("value"):
|
||
result["account_exists"] = True
|
||
result["lamports"] = account["value"].get("lamports", 0)
|
||
result["sources_used"].append("solana_rpc")
|
||
else:
|
||
result["account_exists"] = False
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: Birdeye public API
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://public-api.birdeye.so/defi/token_meta?address={address}"],
|
||
headers={"X-API-KEY": os.getenv("BIRDEYE_KEY", "")},
|
||
)
|
||
if data and data.get("data"):
|
||
result["birdeye"] = data["data"]
|
||
result["sources_used"].append("birdeye")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 4: Jupiter token list
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://token.jup.ag/all"])
|
||
if data:
|
||
token = next((t for t in data if t.get("address") == address), None)
|
||
if token:
|
||
result["jupiter"] = {
|
||
"name": token.get("name"),
|
||
"symbol": token.get("symbol"),
|
||
"decimals": token.get("decimals"),
|
||
"logo": token.get("logoURI"),
|
||
}
|
||
result["sources_used"].append("jupiter")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 5: Coingecko
|
||
try:
|
||
data, _ = await fetch_with_fallback([f"https://api.coingecko.com/api/v3/coins/{address}"])
|
||
if data:
|
||
result["coingecko"] = {"name": data.get("name"), "symbol": data.get("symbol")}
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# Compute risk score from available data
|
||
risk_score = 0
|
||
findings = []
|
||
|
||
if result.get("dexscreener"):
|
||
liq = result["dexscreener"].get("liquidity_usd", 0)
|
||
if liq < 1000:
|
||
risk_score += 25
|
||
findings.append(f"Very low liquidity: ${liq:,.0f}")
|
||
elif liq < 10000:
|
||
risk_score += 15
|
||
findings.append(f"Low liquidity: ${liq:,.0f}")
|
||
|
||
vol = result["dexscreener"].get("volume_24h", 0)
|
||
if liq > 0 and vol > liq * 10:
|
||
risk_score += 10
|
||
findings.append("Volume/liquidity ratio unusually high")
|
||
|
||
buyers = result["dexscreener"].get("buyers_24h", 0)
|
||
sellers = result["dexscreener"].get("sellers_24h", 0)
|
||
if sellers > 0 and buyers > 0:
|
||
ratio = sellers / buyers
|
||
if ratio > 3:
|
||
risk_score += 20
|
||
findings.append(f"Sell pressure {ratio:.1f}x — heavy dumping")
|
||
elif ratio > 1.5:
|
||
risk_score += 10
|
||
findings.append(f"Sell pressure {ratio:.1f}x")
|
||
|
||
if not result.get("account_exists") and not result.get("jupiter"):
|
||
risk_score += 30
|
||
findings.append("Token not found on Solana — possible scam address")
|
||
|
||
if len(result["sources_used"]) == 0:
|
||
risk_score += 20
|
||
findings.append("Unable to fetch data from any source — proceed with extreme caution")
|
||
|
||
risk_score = min(100, risk_score)
|
||
|
||
if risk_score >= 80:
|
||
level = "CRITICAL"
|
||
elif risk_score >= 60:
|
||
level = "HIGH"
|
||
elif risk_score >= 40:
|
||
level = "MEDIUM"
|
||
elif risk_score >= 20:
|
||
level = "LOW"
|
||
else:
|
||
level = "SAFE"
|
||
|
||
result["risk_score"] = risk_score
|
||
result["risk_level"] = level
|
||
result["findings"] = findings
|
||
result["source_count"] = len(result["sources_used"])
|
||
return result
|
||
|
||
|
||
async def _audit_evm(address: str, chain: str) -> dict:
|
||
"""Full audit for EVM tokens (Base, ETH, BSC)."""
|
||
result = {"chain": chain, "address": address, "sources_used": []}
|
||
|
||
# Layer 1: DexScreener
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
result["dexscreener"] = {
|
||
"price_usd": pair.get("priceUsd", 0),
|
||
"liquidity_usd": pair.get("liquidity", {}).get("usd", 0),
|
||
"volume_24h": pair.get("volume", {}).get("h24", 0),
|
||
"price_change_24h": pair.get("priceChange", {}).get("h24", 0),
|
||
}
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: Basescan / Etherscan
|
||
explorer_key = "BASESCAN_KEY" if chain == "base" else "ETHERSCAN_KEY"
|
||
explorer_api = (
|
||
"https://api.basescan.org/api" if chain == "base" else "https://api.etherscan.io/api"
|
||
)
|
||
api_key = os.getenv(explorer_key, "")
|
||
|
||
if api_key:
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
f"{explorer_api}?module=contract&action=getsourcecode&address={address}&apikey={api_key}"
|
||
]
|
||
)
|
||
if data and data.get("result") and data["result"][0].get("SourceCode"):
|
||
result["verified"] = True
|
||
result["sources_used"].append("explorer")
|
||
else:
|
||
result["verified"] = False
|
||
result["findings"].append("Contract not verified on explorer")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: Chain RPC
|
||
try:
|
||
account = await rpc_call(chain, "eth_getCode", [address, "latest"])
|
||
if account and account != "0x":
|
||
result["is_contract"] = True
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
else:
|
||
result["is_contract"] = False
|
||
result["findings"].append("Address is not a contract")
|
||
except Exception:
|
||
pass
|
||
|
||
# Risk computation
|
||
risk_score = 0
|
||
findings = result.get("findings", [])
|
||
|
||
if result.get("dexscreener"):
|
||
liq = result["dexscreener"].get("liquidity_usd", 0)
|
||
if liq < 5000:
|
||
risk_score += 20
|
||
findings.append(f"Low liquidity: ${liq:,.0f}")
|
||
|
||
if not result.get("verified"):
|
||
risk_score += 15
|
||
findings.append("Contract source not verified")
|
||
|
||
if not result.get("is_contract"):
|
||
risk_score += 40
|
||
findings.append("Not a contract — likely EOA or invalid")
|
||
|
||
if len(result["sources_used"]) == 0:
|
||
risk_score += 25
|
||
findings.append("No data from any source")
|
||
|
||
risk_score = min(100, risk_score)
|
||
level = (
|
||
"CRITICAL"
|
||
if risk_score >= 80
|
||
else "HIGH"
|
||
if risk_score >= 60
|
||
else "MEDIUM"
|
||
if risk_score >= 40
|
||
else "LOW"
|
||
if risk_score >= 20
|
||
else "SAFE"
|
||
)
|
||
|
||
result["risk_score"] = risk_score
|
||
result["risk_level"] = level
|
||
result["findings"] = findings
|
||
return result
|
||
|
||
|
||
@router.post("/audit")
|
||
async def deep_contract_audit(req: TokenRequest):
|
||
"""Full 100-point forensic scan. Returns risk score 0-100 with findings."""
|
||
try:
|
||
# Try primary first
|
||
if req.chain == "solana":
|
||
result = await _audit_solana(req.address)
|
||
else:
|
||
result = await _audit_evm(req.address, req.chain)
|
||
|
||
# If primary returned no data, use full fallback chain
|
||
if not result.get("sources_used"):
|
||
from app.auth import get_redis as _get_redis
|
||
from app.fallback_engine import try_all_fallbacks
|
||
|
||
r = await _get_redis()
|
||
fallback = await try_all_fallbacks(
|
||
"audit", address=req.address, chain=req.chain, redis=r
|
||
)
|
||
result.update(fallback)
|
||
result["data_source"] = "fallback"
|
||
|
||
await record_x402_payment("audit", "0.05", req.address)
|
||
return {
|
||
"tool": "Deep Contract Audit",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
# Last resort: try fallback before raising error
|
||
try:
|
||
from app.auth import get_redis as _get_redis
|
||
from app.fallback_engine import try_all_fallbacks
|
||
|
||
r = await _get_redis()
|
||
fallback = await try_all_fallbacks(
|
||
"audit", address=req.address, chain=req.chain, redis=r
|
||
)
|
||
if fallback.get("fallback_layer") != "none":
|
||
await record_x402_payment("audit", "0.05", req.address)
|
||
return {
|
||
"tool": "Deep Contract Audit",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**fallback,
|
||
"recovered_from_error": str(e),
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception:
|
||
pass
|
||
logger.error(f"Audit failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 2: Wallet Profiler ($0.75)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _profile_wallet(address: str, chain: str) -> dict:
|
||
"""Full wallet profile with persona detection and activity analysis."""
|
||
result = {"chain": chain, "address": address, "sources_used": []}
|
||
|
||
# Layer 1: Solana RPC — get balance + transaction count
|
||
if chain == "solana":
|
||
try:
|
||
balance = await rpc_call("solana", "getBalance", [address])
|
||
if balance is not None:
|
||
result["balance_sol"] = balance / 1e9
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
# Get recent transactions
|
||
try:
|
||
sigs = await rpc_call("solana", "getSignaturesForAddress", [address, {"limit": 20}])
|
||
if sigs:
|
||
result["tx_count_recent"] = len(sigs)
|
||
result["last_tx"] = sigs[0].get("blockTime") if sigs else None
|
||
result["sources_used"].append("solana_txs")
|
||
|
||
# Analyze transaction patterns
|
||
success_count = sum(1 for s in sigs if s.get("err") is None)
|
||
result["success_rate"] = success_count / len(sigs) if sigs else 0
|
||
|
||
# Time-based analysis
|
||
if len(sigs) >= 2:
|
||
times = [s.get("blockTime", 0) for s in sigs if s.get("blockTime")]
|
||
if len(times) >= 2:
|
||
time_span = max(times) - min(times)
|
||
if time_span > 0:
|
||
result["tx_frequency"] = len(sigs) / (time_span / 86400) # per day
|
||
except Exception:
|
||
pass
|
||
|
||
# Get token accounts
|
||
try:
|
||
token_accounts = await rpc_call(
|
||
"solana",
|
||
"getTokenAccountsByOwner",
|
||
[
|
||
address,
|
||
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
|
||
{"encoding": "jsonParsed"},
|
||
],
|
||
)
|
||
if token_accounts and token_accounts.get("value"):
|
||
result["token_count"] = len(token_accounts["value"])
|
||
result["sources_used"].append("solana_tokens")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: DexScreener — check if wallet has interacted with known tokens
|
||
# (we can't directly query by wallet, but we use the balance/token data)
|
||
|
||
# Layer 3: Coingecko for any listed assets
|
||
# Layer 4: DeFiLlama for protocol interactions
|
||
|
||
# Persona detection
|
||
persona = "unknown"
|
||
confidence = 0
|
||
|
||
if result.get("tx_frequency", 0) > 50:
|
||
persona = "bot"
|
||
confidence = 85
|
||
elif result.get("tx_frequency", 0) > 10:
|
||
persona = "active_trader"
|
||
confidence = 70
|
||
elif result.get("token_count", 0) > 50:
|
||
persona = "collector"
|
||
confidence = 60
|
||
elif result.get("balance_sol", 0) > 1000:
|
||
persona = "whale"
|
||
confidence = 75
|
||
elif result.get("balance_sol", 0) > 100:
|
||
persona = "experienced"
|
||
confidence = 65
|
||
elif result.get("tx_count_recent", 0) > 0:
|
||
persona = "casual"
|
||
confidence = 50
|
||
else:
|
||
persona = "inactive"
|
||
confidence = 40
|
||
|
||
result["persona"] = persona
|
||
result["persona_confidence"] = confidence
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/wallet")
|
||
async def wallet_profiler(req: WalletRequest):
|
||
"""Full wallet analysis — persona, activity, holdings, patterns."""
|
||
try:
|
||
result = await _profile_wallet(req.address, req.chain)
|
||
await record_x402_payment("wallet", "0.05", req.address)
|
||
return {
|
||
"tool": "Wallet Profiler",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Wallet profile failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 3: Smart Money Tracker ($1.00)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _get_smart_money(chain: str, threshold: float, limit: int) -> dict:
|
||
"""Track whale movements and smart money patterns."""
|
||
result = {"chain": chain, "threshold_usd": threshold, "sources_used": []}
|
||
|
||
# Layer 1: DexScreener trending
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://api.dexscreener.com/latest/dex/search?q=",
|
||
]
|
||
)
|
||
if data and data.get("pairs"):
|
||
trending = sorted(
|
||
data["pairs"], key=lambda p: p.get("volume", {}).get("h24", 0), reverse=True
|
||
)[:limit]
|
||
result["trending_tokens"] = [
|
||
{
|
||
"address": p.get("baseToken", {}).get("address"),
|
||
"symbol": p.get("baseToken", {}).get("symbol"),
|
||
"volume_24h": p.get("volume", {}).get("h24", 0),
|
||
"liquidity": p.get("liquidity", {}).get("usd", 0),
|
||
}
|
||
for p in trending
|
||
]
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: DefiLlama — top protocols by TVL change
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/protocols"])
|
||
if data:
|
||
protocols = sorted(
|
||
data, key=lambda p: p.get("chainTvls", {}).get(f"{chain}", 0), reverse=True
|
||
)[:10]
|
||
result["top_protocols"] = [
|
||
{"name": p.get("name"), "tvl": p.get("tvl")} for p in protocols
|
||
]
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: CoinGecko — top gainers
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.coingecko.com/api/v3/search/trending"])
|
||
if data and data.get("coins"):
|
||
result["trending_coins"] = [
|
||
{"name": c.get("item", {}).get("name"), "symbol": c.get("item", {}).get("symbol")}
|
||
for c in data["coins"][:limit]
|
||
]
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 4: Pump.fun — new launches with high volume
|
||
if chain == "solana":
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://frontend-api.pump.fun/coins?offset=0&limit=20&sort=last_trade_timestamp&order=desc&minMarketCap=10000&maxMarketCap=1000000"
|
||
]
|
||
)
|
||
if data:
|
||
result["new_launches"] = [
|
||
{
|
||
"mint": c.get("mint"),
|
||
"name": c.get("name"),
|
||
"market_cap": c.get("usdMarketCap"),
|
||
}
|
||
for c in data[:10]
|
||
if c.get("usdMarketCap", 0) > threshold
|
||
]
|
||
result["sources_used"].append("pumpfun")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.get("/smartmoney")
|
||
async def smart_money_tracker(chain: str = "solana", threshold: float = 10000.0, limit: int = 20):
|
||
"""Real-time whale/insider tracking across chains."""
|
||
try:
|
||
result = await _get_smart_money(chain, threshold, limit)
|
||
await record_x402_payment("smartmoney", "0.05", f"api-{chain}")
|
||
return {
|
||
"tool": "Smart Money Tracker",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Smart money failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 4: Launch Radar ($0.50)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _detect_launches(chain: str, window_min: int) -> dict:
|
||
"""Detect new token launches with risk scoring."""
|
||
result = {"chain": chain, "window_minutes": window_min, "sources_used": []}
|
||
|
||
if chain == "solana":
|
||
# Layer 1: Pump.fun new tokens
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://frontend-api.pump.fun/coins?offset=0&limit=50&sort=created_timestamp&order=desc"
|
||
]
|
||
)
|
||
if data:
|
||
launches = []
|
||
for coin in data:
|
||
mc = coin.get("usdMarketCap", 0)
|
||
vol = coin.get("totalVolume", 0)
|
||
score = 0
|
||
flags = []
|
||
|
||
if mc < 5000:
|
||
score += 30
|
||
flags.append("micro_cap")
|
||
if vol > mc * 5:
|
||
score += 20
|
||
flags.append("volume_spike")
|
||
|
||
launches.append(
|
||
{
|
||
"address": coin.get("mint"),
|
||
"name": coin.get("name"),
|
||
"symbol": coin.get("symbol"),
|
||
"market_cap": mc,
|
||
"volume": vol,
|
||
"risk_score": min(100, score),
|
||
"flags": flags,
|
||
"created": coin.get("createdTimestamp"),
|
||
}
|
||
)
|
||
|
||
result["launches"] = launches
|
||
result["sources_used"].append("pumpfun")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: Raydium new pools
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.raydium.io/v2/main/pairs"])
|
||
if data and data.get("data"):
|
||
result["raydium_pools"] = len(data["data"])
|
||
result["sources_used"].append("raydium")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: DexScreener — new pairs
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/pairs/{chain}/recent"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
result["dexscreener_pairs"] = len(data["pairs"])
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.get("/launch")
|
||
async def launch_radar(chain: str = "solana", window_minutes: int = 5):
|
||
"""New token launch detection with risk scoring."""
|
||
try:
|
||
result = await _detect_launches(chain, window_minutes)
|
||
await record_x402_payment("launch", "0.03", f"api-{chain}")
|
||
return {
|
||
"tool": "Launch Radar",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Launch radar failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 5: Rug Shield ($0.25)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _rug_shield(address: str, chain: str) -> dict:
|
||
"""Quick pre-buy safety check — fast binary verdict."""
|
||
result = {"chain": chain, "address": address, "sources_used": []}
|
||
risk_score = 0
|
||
flags = []
|
||
|
||
# Layer 1: DexScreener (fastest)
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"], timeout=5
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
liq = pair.get("liquidity", {}).get("usd", 0)
|
||
vol = pair.get("volume", {}).get("h24", 0)
|
||
|
||
if liq < 1000:
|
||
risk_score += 30
|
||
flags.append("low_liq")
|
||
if liq < 100:
|
||
risk_score += 20
|
||
flags.append("micro_liq")
|
||
|
||
buyers = pair.get("txns", {}).get("h24", {}).get("buys", 0)
|
||
sellers = pair.get("txns", {}).get("h24", {}).get("sells", 0)
|
||
if sellers > buyers * 3:
|
||
risk_score += 25
|
||
flags.append("heavy_selling")
|
||
|
||
result["price_usd"] = pair.get("priceUsd", 0)
|
||
result["liquidity"] = liq
|
||
result["volume_24h"] = vol
|
||
result["sources_used"].append("dexscreener")
|
||
else:
|
||
risk_score += 40
|
||
flags.append("no_dex_data")
|
||
except Exception:
|
||
risk_score += 20
|
||
flags.append("dex_unavailable")
|
||
|
||
# Layer 2: Solana RPC — quick account check
|
||
if chain == "solana" and risk_score < 60:
|
||
try:
|
||
account = await rpc_call("solana", "getAccountInfo", [address, {"encoding": "base58"}])
|
||
if not account or not account.get("value"):
|
||
risk_score += 30
|
||
flags.append("no_account")
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: CoinGecko verification
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.coingecko.com/api/v3/coins/{address}"], timeout=5
|
||
)
|
||
if data:
|
||
result["coingecko_verified"] = True
|
||
result["sources_used"].append("coingecko")
|
||
else:
|
||
result["coingecko_verified"] = False
|
||
except Exception:
|
||
pass
|
||
|
||
risk_score = min(100, risk_score)
|
||
verdict = "UNSAFE" if risk_score >= 60 else "CAUTION" if risk_score >= 30 else "SAFE"
|
||
|
||
result["verdict"] = verdict
|
||
result["risk_score"] = risk_score
|
||
result["flags"] = flags
|
||
return result
|
||
|
||
|
||
@router.post("/rugshield")
|
||
async def rug_shield(req: TokenRequest):
|
||
"""Quick pre-buy safety check. Binary safe/unsafe in under 2 seconds."""
|
||
try:
|
||
result = await _rug_shield(req.address, req.chain)
|
||
await record_x402_payment("rugshield", "0.02", req.address)
|
||
return {
|
||
"tool": "Rug Shield",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Rug shield failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 6: Social Sentiment Radar ($0.50)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _sentiment_analysis(token: str, chain: str) -> dict:
|
||
"""Analyze social signals across multiple sources."""
|
||
result = {"token": token, "chain": chain, "sources_used": []}
|
||
|
||
# Layer 1: CoinGecko social data
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.coingecko.com/api/v3/coins/{token}"], timeout=8
|
||
)
|
||
if data:
|
||
result["coingecko"] = {
|
||
"name": data.get("name"),
|
||
"market_cap_rank": data.get("market_cap_rank"),
|
||
"sentiment_votes_up": data.get("public_interest_stats", {}).get("alexa_rank", 0),
|
||
}
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: DexScreener social links
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/search?q={token}"], timeout=5
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
info = pair.get("info", {})
|
||
result["social_links"] = {
|
||
"twitter": info.get("twitter"),
|
||
"telegram": info.get("telegram"),
|
||
"website": info.get("websites", [{}])[0].get("url")
|
||
if info.get("websites")
|
||
else None,
|
||
}
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: CoinGecko community data
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
f"https://api.coingecko.com/api/v3/coins/{token}?localization=false&tickers=false&market_data=false&community_data=true&developer_data=false"
|
||
],
|
||
timeout=8,
|
||
)
|
||
if data and data.get("community_data"):
|
||
result["community"] = data["community_data"]
|
||
result["sources_used"].append("coingecko_community")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 4: DefiLlama — protocol mentions
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/protocols"], timeout=10)
|
||
if data:
|
||
protocol = next((p for p in data if token.lower() in p.get("name", "").lower()), None)
|
||
if protocol:
|
||
result["defillama"] = {
|
||
"name": protocol.get("name"),
|
||
"tvl": protocol.get("tvl"),
|
||
"chains": protocol.get("chains"),
|
||
}
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
# Compute sentiment score
|
||
score = 50 # neutral baseline
|
||
|
||
if result.get("coingecko") and result["coingecko"].get("market_cap_rank"):
|
||
rank = result["coingecko"]["market_cap_rank"]
|
||
if rank < 100:
|
||
score += 20
|
||
elif rank < 500:
|
||
score += 10
|
||
elif rank > 5000:
|
||
score -= 10
|
||
|
||
if result.get("social_links", {}).get("twitter"):
|
||
score += 5
|
||
if result.get("social_links", {}).get("telegram"):
|
||
score += 5
|
||
if result.get("community", {}).get("twitter_followers", 0) > 10000:
|
||
score += 10
|
||
|
||
result["sentiment_score"] = max(0, min(100, score))
|
||
result["sentiment_label"] = (
|
||
"bullish" if score >= 65 else "bearish" if score <= 35 else "neutral"
|
||
)
|
||
return result
|
||
|
||
|
||
@router.post("/sentiment")
|
||
async def social_sentiment(req: SentimentRequest):
|
||
"""Social signal analysis across Twitter, Telegram, RSS feeds."""
|
||
try:
|
||
result = await _sentiment_analysis(req.token, req.chain)
|
||
await record_x402_payment("sentiment", "0.03", req.token)
|
||
return {
|
||
"tool": "Social Sentiment Radar",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Sentiment failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 7: Cluster Detection ($1.00)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _cluster_analysis(address: str, chain: str, depth: int) -> dict:
|
||
"""Map wallet clusters and funding chains."""
|
||
result = {"chain": chain, "address": address, "depth": depth, "sources_used": []}
|
||
|
||
# Layer 1: Solana RPC — get transaction history
|
||
if chain == "solana":
|
||
try:
|
||
sigs = await rpc_call("solana", "getSignaturesForAddress", [address, {"limit": 100}])
|
||
if sigs:
|
||
result["transaction_count"] = len(sigs)
|
||
result["sources_used"].append("solana_txs")
|
||
|
||
# Extract counterparties
|
||
counterparties = set()
|
||
for sig in sigs[:20]:
|
||
try:
|
||
tx = await rpc_call(
|
||
"solana",
|
||
"getTransaction",
|
||
[
|
||
sig.get("signature"),
|
||
{"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0},
|
||
],
|
||
)
|
||
if tx and tx.get("transaction") and tx["transaction"].get("message"):
|
||
accounts = tx["transaction"]["message"].get("accountKeys", [])
|
||
for acc in accounts:
|
||
addr = acc.get("pubkey") if isinstance(acc, dict) else acc
|
||
if addr and addr != address:
|
||
counterparties.add(addr)
|
||
except Exception:
|
||
pass
|
||
|
||
result["counterparties"] = list(counterparties)[:50]
|
||
result["cluster_size"] = len(counterparties)
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: DexScreener — check if address is a known deployer
|
||
# Layer 3: Etherscan/BaseScan for EVM chains
|
||
if chain in ["base", "ethereum", "bsc"]:
|
||
try:
|
||
explorer = "basescan.org" if chain == "base" else "etherscan.io"
|
||
api_base = f"https://api.{explorer}/api"
|
||
key_env = "BASESCAN_KEY" if chain == "base" else "ETHERSCAN_KEY"
|
||
key = os.getenv(key_env, "")
|
||
if key:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
f"{api_base}?module=account&action=txlist&address={address}&startblock=0&endblock=99999999&page=1&offset=20&sort=desc&apikey={key}"
|
||
]
|
||
)
|
||
if data and data.get("result"):
|
||
result["tx_count"] = len(data["result"])
|
||
result["sources_used"].append(f"{chain}_explorer")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 4: Compute cluster metrics
|
||
result["cluster_risk"] = (
|
||
"low"
|
||
if result.get("cluster_size", 0) < 5
|
||
else "medium"
|
||
if result.get("cluster_size", 0) < 20
|
||
else "high"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/cluster")
|
||
async def cluster_detection(req: ClusterRequest):
|
||
"""Wallet cluster mapping — sybil detection, hidden networks."""
|
||
try:
|
||
result = await _cluster_analysis(req.address, req.chain, req.depth)
|
||
await record_x402_payment("cluster", "0.05", req.address)
|
||
return {
|
||
"tool": "Cluster Detection",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Cluster detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 8: Insider Tracker ($1.50)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _insider_tracking(creator: str, chain: str) -> dict:
|
||
"""Track creator wallet activity across all tokens."""
|
||
result = {"chain": chain, "creator_address": creator, "sources_used": []}
|
||
|
||
# Layer 1: DexScreener — find all tokens by this creator
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/search?q={creator}"], timeout=8
|
||
)
|
||
if data and data.get("pairs"):
|
||
tokens = {}
|
||
for pair in data["pairs"]:
|
||
addr = pair.get("baseToken", {}).get("address")
|
||
if addr and addr not in tokens:
|
||
tokens[addr] = {
|
||
"symbol": pair.get("baseToken", {}).get("symbol"),
|
||
"price": pair.get("priceUsd", 0),
|
||
"liquidity": pair.get("liquidity", {}).get("usd", 0),
|
||
}
|
||
result["associated_tokens"] = tokens
|
||
result["token_count"] = len(tokens)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: Solana RPC — wallet activity
|
||
if chain == "solana":
|
||
try:
|
||
balance = await rpc_call("solana", "getBalance", [creator])
|
||
if balance is not None:
|
||
result["creator_balance_sol"] = balance / 1e9
|
||
result["sources_used"].append("solana_balance")
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
sigs = await rpc_call("solana", "getSignaturesForAddress", [creator, {"limit": 50}])
|
||
if sigs:
|
||
result["recent_txs"] = len(sigs)
|
||
result["sources_used"].append("solana_txs")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: Etherscan/BaseScan
|
||
if chain in ["base", "ethereum"]:
|
||
try:
|
||
explorer = "basescan.org" if chain == "base" else "etherscan.io"
|
||
key = os.getenv("BASESCAN_KEY" if chain == "base" else "ETHERSCAN_KEY", "")
|
||
if key:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
f"https://api.{explorer}/api?module=account&action=txlist&address={creator}&startblock=0&endblock=99999999&page=1&offset=10&sort=desc&apikey={key}"
|
||
]
|
||
)
|
||
if data and data.get("result"):
|
||
result["explorer_txs"] = len(data["result"])
|
||
result["sources_used"].append(f"{chain}_explorer")
|
||
except Exception:
|
||
pass
|
||
|
||
# Risk assessment
|
||
risk = "low"
|
||
if result.get("token_count", 0) > 10:
|
||
risk = "high"
|
||
result["flags"] = ["serial_deployer"]
|
||
elif result.get("token_count", 0) > 5:
|
||
risk = "medium"
|
||
result["flags"] = ["multiple_deployments"]
|
||
elif result.get("recent_txs", 0) > 100:
|
||
risk = "medium"
|
||
result["flags"] = ["high_activity"]
|
||
else:
|
||
result["flags"] = []
|
||
|
||
result["risk_level"] = risk
|
||
return result
|
||
|
||
|
||
@router.post("/insider")
|
||
async def insider_tracker(req: InsiderRequest):
|
||
"""Dev/team wallet tracking across all their tokens."""
|
||
try:
|
||
result = await _insider_tracking(req.creator_address, req.chain)
|
||
await record_x402_payment("insider", "0.10", req.creator_address)
|
||
return {
|
||
"tool": "Insider Tracker",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Insider tracking failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 9: URL Scam Detector ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
def _analyze_url(url: str) -> dict:
|
||
"""Analyze URL for scam indicators using structural analysis."""
|
||
import re
|
||
from urllib.parse import urlparse
|
||
|
||
result = {"url": url}
|
||
risk_score = 0
|
||
indicators = []
|
||
|
||
parsed = urlparse(url)
|
||
domain = parsed.netloc.lower()
|
||
|
||
# Indicator 1: Suspicious TLDs
|
||
suspicious_tlds = [".xyz", ".top", ".club", ".tk", ".ml", ".ga", ".cf", ".gq", ".biz", ".info"]
|
||
for tld in suspicious_tlds:
|
||
if domain.endswith(tld):
|
||
risk_score += 15
|
||
indicators.append(f"suspicious_tld:{tld}")
|
||
|
||
# Indicator 2: Brand impersonation
|
||
brand_keywords = [
|
||
"binance",
|
||
"coinbase",
|
||
"metamask",
|
||
"uniswap",
|
||
"opensea",
|
||
"phantom",
|
||
"solana",
|
||
"ethereum",
|
||
"bitcoin",
|
||
"trezor",
|
||
"ledger",
|
||
]
|
||
for brand in brand_keywords:
|
||
if brand in domain and brand not in domain.split(".")[0]:
|
||
pass # legitimate use
|
||
elif brand in domain:
|
||
# Check if it's the real domain
|
||
real_domains = {
|
||
"binance": "binance.com",
|
||
"coinbase": "coinbase.com",
|
||
"metamask": "metamask.io",
|
||
"uniswap": "uniswap.org",
|
||
"opensea": "opensea.io",
|
||
"phantom": "phantom.app",
|
||
"solana": "solana.com",
|
||
"ethereum": "ethereum.org",
|
||
}
|
||
real = real_domains.get(brand)
|
||
if real and domain != real:
|
||
risk_score += 25
|
||
indicators.append(f"brand_impersonation:{brand}")
|
||
|
||
# Indicator 3: Homograph attacks (lookalike chars)
|
||
if any(c in domain for c in "а ο е r n"): # Cyrillic lookalikes
|
||
risk_score += 30
|
||
indicators.append("homograph_attack")
|
||
|
||
# Indicator 4: Subdomain stuffing
|
||
parts = domain.split(".")
|
||
if len(parts) > 3:
|
||
risk_score += 10
|
||
indicators.append("subdomain_stuffing")
|
||
|
||
# Indicator 5: Number-heavy domains
|
||
if re.search(r"\d{4,}", domain):
|
||
risk_score += 10
|
||
indicators.append("number_heavy_domain")
|
||
|
||
# Indicator 6: Hyphen spam
|
||
if domain.count("-") > 2:
|
||
risk_score += 15
|
||
indicators.append("hyphen_spam")
|
||
|
||
# Indicator 7: Crypto scam patterns
|
||
scam_patterns = ["free-", "giveaway", "claim-", "airdrop-", "mint-", "presale-", "ico-"]
|
||
for pattern in scam_patterns:
|
||
if pattern in domain:
|
||
risk_score += 20
|
||
indicators.append(f"scam_pattern:{pattern}")
|
||
|
||
# Indicator 8: Short-lived domain pattern
|
||
if len(domain) < 6 and "." in domain:
|
||
risk_score += 10
|
||
indicators.append("very_short_domain")
|
||
|
||
# Indicator 9: IP address instead of domain
|
||
if re.match(r"^\d+\.\d+\.\d+\.\d+$", domain):
|
||
risk_score += 25
|
||
indicators.append("ip_address_url")
|
||
|
||
# Indicator 10: HTTPS check
|
||
if parsed.scheme != "https":
|
||
risk_score += 5
|
||
indicators.append("no_https")
|
||
|
||
risk_score = min(100, risk_score)
|
||
verdict = "SCAM" if risk_score >= 60 else "SUSPICIOUS" if risk_score >= 30 else "LIKELY_SAFE"
|
||
|
||
result["risk_score"] = risk_score
|
||
result["verdict"] = verdict
|
||
result["indicators"] = indicators
|
||
result["domain"] = domain
|
||
return result
|
||
|
||
|
||
@router.post("/urlcheck")
|
||
async def url_scam_detector(req: URLRequest):
|
||
"""URL scam analysis — structural analysis, no blacklists needed."""
|
||
try:
|
||
result = _analyze_url(req.url)
|
||
await record_x402_payment("urlcheck", "0.01", req.url)
|
||
return {
|
||
"tool": "URL Scam Detector",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"URL scan failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 10: Token Pulse ($0.25)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _token_pulse(address: str, chain: str) -> dict:
|
||
"""Comprehensive token health dashboard."""
|
||
result = {"chain": chain, "address": address, "sources_used": []}
|
||
|
||
# Layer 1: DexScreener — core metrics
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"], timeout=8
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
result["price_usd"] = pair.get("priceUsd", 0)
|
||
result["liquidity"] = pair.get("liquidity", {}).get("usd", 0)
|
||
result["volume_24h"] = pair.get("volume", {}).get("h24", 0)
|
||
result["price_change_1h"] = pair.get("priceChange", {}).get("h1", 0)
|
||
result["price_change_6h"] = pair.get("priceChange", {}).get("h6", 0)
|
||
result["price_change_24h"] = pair.get("priceChange", {}).get("h24", 0)
|
||
result["txns_24h"] = pair.get("txns", {}).get("h24", {})
|
||
result["makers"] = pair.get("makers", {}).get("h24", 0)
|
||
result["fdv"] = pair.get("fdv", 0)
|
||
result["pair_age_hours"] = pair.get("pairCreatedAt", 0)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: Coingecko — additional metrics
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.coingecko.com/api/v3/coins/{address}"], timeout=8
|
||
)
|
||
if data:
|
||
result["coingecko"] = {
|
||
"market_cap": data.get("market_data", {}).get("market_cap", {}).get("usd"),
|
||
"circulating_supply": data.get("market_data", {}).get("circulating_supply"),
|
||
"total_supply": data.get("market_data", {}).get("total_supply"),
|
||
}
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 3: Compute health score
|
||
score = 50 # baseline
|
||
|
||
if result.get("liquidity", 0) > 100000:
|
||
score += 15
|
||
elif result.get("liquidity", 0) > 10000:
|
||
score += 10
|
||
elif result.get("liquidity", 0) < 1000:
|
||
score -= 20
|
||
|
||
if result.get("volume_24h", 0) > 100000:
|
||
score += 10
|
||
elif result.get("volume_24h", 0) < 100:
|
||
score -= 15
|
||
|
||
change_24h = result.get("price_change_24h", 0)
|
||
if -10 <= change_24h <= 10:
|
||
score += 5 # stable
|
||
elif change_24h > 50:
|
||
score -= 10 # pump risk
|
||
elif change_24h < -30:
|
||
score -= 15 # dump
|
||
|
||
makers = result.get("makers", 0)
|
||
if makers > 1000:
|
||
score += 10
|
||
elif makers < 50:
|
||
score -= 10
|
||
|
||
if len(result["sources_used"]) >= 2:
|
||
score += 5
|
||
elif len(result["sources_used"]) == 0:
|
||
score -= 20
|
||
|
||
score = max(0, min(100, score))
|
||
|
||
# Trend direction
|
||
if result.get("price_change_24h", 0) > 5:
|
||
trend = "up"
|
||
elif result.get("price_change_24h", 0) < -5:
|
||
trend = "down"
|
||
else:
|
||
trend = "stable"
|
||
|
||
result["health_score"] = score
|
||
result["trend"] = trend
|
||
result["health_label"] = (
|
||
"excellent"
|
||
if score >= 80
|
||
else "good"
|
||
if score >= 60
|
||
else "fair"
|
||
if score >= 40
|
||
else "poor"
|
||
if score >= 20
|
||
else "critical"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/pulse")
|
||
async def token_pulse(req: TokenRequest):
|
||
"""Token health dashboard — liquidity, volume, momentum, trajectory."""
|
||
try:
|
||
result = await _token_pulse(req.address, req.chain)
|
||
await record_x402_payment("pulse", "0.01", req.address)
|
||
return {
|
||
"tool": "Token Pulse",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Token pulse failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 11: Twitter Profile ($0.01)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _twitter_profile(query: str) -> dict:
|
||
"""Get Twitter/X user profile data."""
|
||
result = {"query": query, "sources_used": []}
|
||
|
||
# Layer 1: Nitter instances
|
||
nitter_instances = [
|
||
f"https://nitter.net/{query}",
|
||
f"https://nitter.privacydev.net/{query}",
|
||
]
|
||
for url in nitter_instances:
|
||
try:
|
||
data, _src = await fetch_with_fallback([url], timeout=5)
|
||
if data:
|
||
# Extract profile info from HTML
|
||
result["source"] = "nitter"
|
||
result["sources_used"].append("nitter")
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
# Layer 2: DuckDuckGo search
|
||
try:
|
||
from urllib.parse import quote
|
||
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://html.duckduckgo.com/html/?q={quote(f'site:twitter.com {query}')}"]
|
||
)
|
||
if data:
|
||
result["duckduckgo_results"] = True
|
||
result["sources_used"].append("duckduckgo")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/tw_profile")
|
||
async def twitter_profile(req: GenericRequest):
|
||
"""Twitter/X profile lookup."""
|
||
try:
|
||
query = req.query or req.address or req.token or ""
|
||
result = await _twitter_profile(query)
|
||
await record_x402_payment("tw_profile", "0.01", query)
|
||
return {
|
||
"tool": "Twitter Profile",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Twitter profile failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 12: Twitter Timeline ($0.01)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/tw_timeline")
|
||
async def twitter_timeline(req: GenericRequest):
|
||
"""Get recent tweets from a user."""
|
||
try:
|
||
query = req.query or req.address or req.token or ""
|
||
tweets = []
|
||
|
||
# DuckDuckGo search for recent tweets
|
||
try:
|
||
from urllib.parse import quote
|
||
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
f"https://html.duckduckgo.com/html/?q={quote(f'site:twitter.com/{query} ') + 'after:2024-01-01'}"
|
||
]
|
||
)
|
||
if data:
|
||
tweets.append({"source": "duckduckgo"})
|
||
except Exception:
|
||
pass
|
||
|
||
await record_x402_payment("tw_timeline", "0.01", query)
|
||
return {
|
||
"tool": "Twitter Timeline",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"user": query,
|
||
"tweets": tweets,
|
||
"sources_used": ["duckduckgo"] if tweets else [],
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Twitter timeline failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 13: Twitter Search ($0.01)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/tw_search")
|
||
async def twitter_search(req: GenericRequest):
|
||
"""Search Twitter/X for tweets."""
|
||
try:
|
||
query = req.query or req.token or ""
|
||
results = []
|
||
|
||
# DuckDuckGo search
|
||
try:
|
||
from urllib.parse import quote
|
||
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://html.duckduckgo.com/html/?q={quote(f'site:twitter.com {query}')}"]
|
||
)
|
||
if data:
|
||
results.append({"source": "duckduckgo"})
|
||
except Exception:
|
||
pass
|
||
|
||
await record_x402_payment("tw_search", "0.01", query)
|
||
return {
|
||
"tool": "Twitter Search",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"query": query,
|
||
"results": results,
|
||
"sources_used": ["duckduckgo"] if results else [],
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Twitter search failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 14: Token Forensics ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _token_forensics(address: str, chain: str) -> dict:
|
||
"""Deep token forensics combining multiple data sources."""
|
||
result = {"address": address, "chain": chain, "sources_used": []}
|
||
|
||
# DexScreener
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
result["price_usd"] = pair.get("priceUsd", 0)
|
||
result["liquidity_usd"] = pair.get("liquidity", {}).get("usd", 0)
|
||
result["volume_24h"] = pair.get("volume", {}).get("h24", 0)
|
||
result["price_change_24h"] = pair.get("priceChange", {}).get("h24", 0)
|
||
result["pair_age_days"] = pair.get("pairCreatedAt", 0)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# CoinGecko
|
||
try:
|
||
data, _ = await fetch_with_fallback([f"https://api.coingecko.com/api/v3/coins/{address}"])
|
||
if data:
|
||
result["coingecko"] = {
|
||
"name": data.get("name"),
|
||
"symbol": data.get("symbol"),
|
||
"market_cap_rank": data.get("market_cap_rank"),
|
||
"current_price": data.get("market_data", {}).get("current_price", {}),
|
||
}
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# GeckoTerminal
|
||
try:
|
||
chain_map = {"solana": "solana", "base": "base", "ethereum": "eth", "bsc": "bsc"}
|
||
gecko_chain = chain_map.get(chain, chain)
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.geckoterminal.com/api/v2/networks/{gecko_chain}/tokens/{address}"]
|
||
)
|
||
if data and data.get("data"):
|
||
result["geckoterminal"] = True
|
||
result["sources_used"].append("geckoterminal")
|
||
except Exception:
|
||
pass
|
||
|
||
# DeFiLlama
|
||
try:
|
||
data, _ = await fetch_with_fallback([f"https://coins.llama.fi/token/{chain}:{address}"])
|
||
if data and data.get("coins"):
|
||
result["defillama"] = data["coins"]
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
# Risk scoring
|
||
risk_score = 0
|
||
findings = []
|
||
liq = result.get("liquidity_usd", 0)
|
||
if liq > 0 and liq < 1000:
|
||
risk_score += 25
|
||
findings.append(f"Very low liquidity: ${liq:,.0f}")
|
||
elif liq >= 1000 and liq < 10000:
|
||
risk_score += 15
|
||
findings.append(f"Low liquidity: ${liq:,.0f}")
|
||
|
||
vol = result.get("volume_24h", 0)
|
||
if liq > 0 and vol < liq * 0.01:
|
||
risk_score += 20
|
||
findings.append("Extremely low volume relative to liquidity")
|
||
|
||
if len(result["sources_used"]) == 0:
|
||
risk_score += 30
|
||
findings.append("No data from any source")
|
||
|
||
risk_score = min(100, risk_score)
|
||
result["risk_score"] = risk_score
|
||
result["findings"] = findings
|
||
result["recommendation"] = (
|
||
"AVOID" if risk_score >= 70 else "CAUTION" if risk_score >= 40 else "PROCEED"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/forensics")
|
||
async def token_forensics(req: GenericRequest):
|
||
"""Deep token forensics report."""
|
||
try:
|
||
address = req.address or req.token or ""
|
||
result = await _token_forensics(address, req.chain)
|
||
await record_x402_payment("forensics", "0.10", address)
|
||
return {
|
||
"tool": "Token Forensics",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Forensics failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 15: Whale Decoder ($0.15)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _whale_decoder(address: str, chain: str) -> dict:
|
||
"""Advanced whale wallet analysis."""
|
||
result = {"address": address, "chain": chain, "sources_used": []}
|
||
|
||
# Solana RPC - get balance and transactions
|
||
if chain == "solana":
|
||
try:
|
||
balance = await rpc_call("solana", "getBalance", [address])
|
||
if balance is not None:
|
||
result["sol_balance"] = balance / 1e9
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
token_accounts = await rpc_call(
|
||
"solana",
|
||
"getTokenAccountsByOwner",
|
||
[
|
||
address,
|
||
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
|
||
{"encoding": "jsonParsed"},
|
||
],
|
||
)
|
||
if token_accounts and token_accounts.get("value"):
|
||
tokens = token_accounts["value"]
|
||
result["token_count"] = len(tokens)
|
||
result["top_tokens"] = []
|
||
for t in tokens[:10]:
|
||
parsed = t.get("account", {}).get("data", {}).get("parsed", {}).get("info", {})
|
||
result["top_tokens"].append(
|
||
{
|
||
"mint": parsed.get("mint"),
|
||
"amount": parsed.get("tokenAmount", {}).get("uiAmount", 0),
|
||
}
|
||
)
|
||
result["sources_used"].append("solana_rpc_tokens")
|
||
except Exception:
|
||
pass
|
||
|
||
# EVM chain balance
|
||
elif chain in ["base", "ethereum", "bsc"]:
|
||
try:
|
||
balance = await rpc_call(chain, "eth_getBalance", [address, "latest"])
|
||
if balance:
|
||
result["native_balance_wei"] = balance
|
||
result["native_balance_eth"] = int(balance, 16) / 1e18
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
# DexScreener for recent activity
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/search?q={address[:10]}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
result["recent_pairs"] = len(data["pairs"])
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Persona detection
|
||
sol_balance = result.get("sol_balance", result.get("native_balance_eth", 0))
|
||
persona = "unknown"
|
||
if sol_balance > 1000:
|
||
persona = "mega_whale"
|
||
elif sol_balance > 100:
|
||
persona = "whale"
|
||
elif sol_balance > 10:
|
||
persona = "dolphin"
|
||
elif sol_balance > 1:
|
||
persona = "retail"
|
||
|
||
result["persona"] = persona
|
||
result["activity_level"] = (
|
||
"high"
|
||
if result.get("recent_pairs", 0) > 5
|
||
else "medium"
|
||
if result.get("recent_pairs", 0) > 1
|
||
else "low"
|
||
)
|
||
result["trust_score"] = (
|
||
85 if persona in ["whale", "mega_whale"] else 60 if persona == "dolphin" else 40
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/whale")
|
||
async def whale_decoder(req: GenericRequest):
|
||
"""Whale wallet decoder and analysis."""
|
||
try:
|
||
address = req.address or req.query or ""
|
||
result = await _whale_decoder(address, req.chain)
|
||
await record_x402_payment("whale", "0.15", address)
|
||
return {
|
||
"tool": "Whale Decoder",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Whale decoder failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 16: Launch Intel ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _launch_intel(chain: str, hours: int) -> dict:
|
||
"""Token launch intelligence."""
|
||
result = {"chain": chain, "hours": hours, "sources_used": []}
|
||
launches = []
|
||
|
||
# PumpFun new tokens
|
||
if chain == "solana":
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://frontend-api.pump.fun/coins?offset=0&limit=20&sort=created_timestamp&order=desc"
|
||
]
|
||
)
|
||
if data and isinstance(data, list):
|
||
for c in data[:10]:
|
||
launches.append(
|
||
{
|
||
"mint": c.get("mint"),
|
||
"name": c.get("name"),
|
||
"symbol": c.get("ticker"),
|
||
"market_cap": c.get("usdMarketCap", 0),
|
||
"source": "pumpfun",
|
||
}
|
||
)
|
||
result["sources_used"].append("pumpfun")
|
||
except Exception:
|
||
pass
|
||
|
||
# DexScreener trending
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.dexscreener.com/latest/dex/search?q="])
|
||
if data and data.get("pairs"):
|
||
trending = sorted(
|
||
data["pairs"], key=lambda p: p.get("volume", {}).get("h24", 0), reverse=True
|
||
)[:5]
|
||
for p in trending:
|
||
launches.append(
|
||
{
|
||
"address": p.get("baseToken", {}).get("address"),
|
||
"symbol": p.get("baseToken", {}).get("symbol"),
|
||
"volume_24h": p.get("volume", {}).get("h24", 0),
|
||
"source": "dexscreener",
|
||
}
|
||
)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
result["new_launches"] = launches
|
||
result["total_found"] = len(launches)
|
||
return result
|
||
|
||
|
||
@router.post("/launch_intel")
|
||
async def launch_intel(req: GenericRequest):
|
||
"""Token launch intelligence."""
|
||
try:
|
||
result = await _launch_intel(req.chain, req.hours)
|
||
await record_x402_payment("launch_intel", "0.05", "scan")
|
||
return {
|
||
"tool": "Launch Intelligence",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Launch intel failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 17: Anomaly Detector ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _anomaly_detector(chain: str) -> dict:
|
||
"""Market anomaly detection."""
|
||
result = {"chain": chain, "anomalies": [], "sources_used": []}
|
||
|
||
# Get market overview for comparison
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.coingecko.com/api/v3/global"])
|
||
if data and data.get("data"):
|
||
global_data = data["data"]
|
||
result["total_market_cap"] = global_data.get("total_market_cap", {}).get("usd", 0)
|
||
result["market_cap_change_24h"] = global_data.get(
|
||
"market_cap_change_percentage_24h_usd", 0
|
||
)
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# DexScreener for volume spikes
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.dexscreener.com/latest/dex/search?q="])
|
||
if data and data.get("pairs"):
|
||
for p in data["pairs"][:20]:
|
||
vol_h24 = p.get("volume", {}).get("h24", 0)
|
||
p.get("volume", {}).get("h6", 0)
|
||
liq = p.get("liquidity", {}).get("usd", 0)
|
||
if liq > 0 and vol_h24 > liq * 5:
|
||
result["anomalies"].append(
|
||
{
|
||
"type": "volume_spike",
|
||
"token": p.get("baseToken", {}).get("symbol"),
|
||
"address": p.get("baseToken", {}).get("address"),
|
||
"volume_24h": vol_h24,
|
||
"liquidity": liq,
|
||
"ratio": vol_h24 / liq if liq > 0 else 0,
|
||
}
|
||
)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
result["anomaly_count"] = len(result["anomalies"])
|
||
result["market_health"] = (
|
||
"normal"
|
||
if result["anomaly_count"] < 3
|
||
else "elevated"
|
||
if result["anomaly_count"] < 7
|
||
else "critical"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/anomaly")
|
||
async def anomaly_detector(req: GenericRequest):
|
||
"""Market anomaly detector."""
|
||
try:
|
||
chain = req.chain or "all"
|
||
result = await _anomaly_detector(chain)
|
||
await record_x402_payment("anomaly", "0.08", chain)
|
||
return {
|
||
"tool": "Anomaly Detector",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Anomaly detector failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 18: Social Signal ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _social_signal(query: str) -> dict:
|
||
"""Social signal analyzer."""
|
||
result = {"query": query, "sources_used": []}
|
||
|
||
# CryptoPanic
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://cryptopanic.com/api/free/posts/?filter=important&q={query}"]
|
||
)
|
||
if data and data.get("results"):
|
||
result["cryptopanic_count"] = len(data["results"])
|
||
result["cryptopanic_sentiment"] = sum(
|
||
1 for r in data["results"] if r.get("sentiment") == "positive"
|
||
)
|
||
result["sources_used"].append("cryptopanic")
|
||
except Exception:
|
||
pass
|
||
|
||
# Reddit
|
||
try:
|
||
from urllib.parse import quote
|
||
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://www.reddit.com/search.json?q={quote(query)}&sort=new&limit=10"]
|
||
)
|
||
if data and data.get("data", {}).get("children"):
|
||
result["reddit_count"] = len(data["data"]["children"])
|
||
result["sources_used"].append("reddit")
|
||
except Exception:
|
||
pass
|
||
|
||
result["total_mentions"] = result.get("cryptopanic_count", 0) + result.get("reddit_count", 0)
|
||
result["sentiment_score"] = result.get("cryptopanic_sentiment", 0) / max(
|
||
1, result.get("cryptopanic_count", 1)
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/social_signal")
|
||
async def social_signal(req: GenericRequest):
|
||
"""Social signal analyzer."""
|
||
try:
|
||
query = req.query or req.token or req.address or ""
|
||
result = await _social_signal(query)
|
||
await record_x402_payment("social_signal", "0.10", query)
|
||
return {
|
||
"tool": "Social Signal",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Social signal failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 19: Market Overview ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _market_overview(chain: str) -> dict:
|
||
"""Comprehensive market overview."""
|
||
result = {"chain": chain, "sources_used": []}
|
||
|
||
# CoinGecko global
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.coingecko.com/api/v3/global"])
|
||
if data and data.get("data"):
|
||
gd = data["data"]
|
||
result["total_market_cap_usd"] = gd.get("total_market_cap", {}).get("usd", 0)
|
||
result["total_volume_24h"] = gd.get("total_volume", {}).get("usd", 0)
|
||
result["btc_dominance"] = gd.get("market_cap_percentage", {}).get("btc", 0)
|
||
result["eth_dominance"] = gd.get("market_cap_percentage", {}).get("eth", 0)
|
||
result["active_cryptocurrencies"] = gd.get("active_cryptocurrencies", 0)
|
||
result["sources_used"].append("coingecko")
|
||
except Exception:
|
||
pass
|
||
|
||
# CoinGecko top coins
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1"
|
||
]
|
||
)
|
||
if data:
|
||
result["top_coins"] = [
|
||
{
|
||
"symbol": c.get("symbol"),
|
||
"price": c.get("current_price"),
|
||
"market_cap": c.get("market_cap"),
|
||
"change_24h": c.get("price_change_percentage_24h"),
|
||
}
|
||
for c in data
|
||
]
|
||
result["sources_used"].append("coingecko_markets")
|
||
except Exception:
|
||
pass
|
||
|
||
# DeFiLlama TVL
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/v2/chains"])
|
||
if data:
|
||
result["chain_tvls"] = {c.get("name"): c.get("tvl") for c in data[:10]}
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/market_overview")
|
||
async def market_overview(req: GenericRequest):
|
||
"""Comprehensive market overview."""
|
||
try:
|
||
result = await _market_overview(req.chain)
|
||
await record_x402_payment("market_overview", "0.05", "overview")
|
||
return {
|
||
"tool": "Market Overview",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Market overview failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 20: Token Deep Dive ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/token_deep_dive")
|
||
async def token_deep_dive(req: GenericRequest):
|
||
"""Deep token analysis across chains."""
|
||
try:
|
||
query = req.address or req.token or req.query or ""
|
||
result = await _token_forensics(query, req.chain)
|
||
result["tool_name"] = "Token Deep Dive"
|
||
await record_x402_payment("token_deep_dive", "0.10", query)
|
||
return {
|
||
"tool": "Token Deep Dive",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Token deep dive failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 21: Chain Health ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _chain_health(chain: str) -> dict:
|
||
"""Chain health metrics."""
|
||
result = {"chain": chain, "chains": {}, "sources_used": []}
|
||
|
||
chains_to_check = ["solana", "ethereum", "base", "bsc"] if chain == "all" else [chain]
|
||
|
||
# DeFiLlama TVL per chain
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/v2/chains"])
|
||
if data:
|
||
chain_map = {"solana": "Solana", "ethereum": "Ethereum", "base": "Base", "bsc": "BSC"}
|
||
for c in data:
|
||
name = c.get("name")
|
||
for key, val in chain_map.items():
|
||
if key in chains_to_check and val.lower() in name.lower():
|
||
result["chains"][key] = {
|
||
"tvl": c.get("tvl", 0),
|
||
"protocols": c.get("protocols", 0),
|
||
"chain_id": c.get("chainId"),
|
||
}
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
# RPC health check
|
||
for c in chains_to_check:
|
||
rpcs = FREE_RPCS.get(c, [])
|
||
healthy = 0
|
||
for rpc in rpcs[:2]:
|
||
try:
|
||
async with aiohttp.ClientSession() as session, session.post(
|
||
rpc,
|
||
json={
|
||
"jsonrpc": "2.0",
|
||
"id": 1,
|
||
"method": "eth_blockNumber" if c != "solana" else "getBlockHeight",
|
||
"params": [],
|
||
},
|
||
timeout=aiohttp.ClientTimeout(total=5),
|
||
) as resp:
|
||
if resp.status == 200:
|
||
healthy += 1
|
||
except Exception:
|
||
pass
|
||
result["chains"].setdefault(c, {})["rpc_healthy"] = healthy
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/chain_health")
|
||
async def chain_health(req: GenericRequest):
|
||
"""Chain health metrics."""
|
||
try:
|
||
result = await _chain_health(req.chain)
|
||
await record_x402_payment("chain_health", "0.05", req.chain)
|
||
return {
|
||
"tool": "Chain Health",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Chain health failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 22: Honeypot Check ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _honeypot_check(address: str, chain: str) -> dict:
|
||
"""Honeypot detection."""
|
||
result = {"address": address, "chain": chain, "sources_used": []}
|
||
|
||
# DexScreener - check for trading activity
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
txns = pair.get("txns", {}).get("h24", {})
|
||
buys = txns.get("buys", 0)
|
||
sells = txns.get("sells", 0)
|
||
|
||
result["buys_24h"] = buys
|
||
result["sells_24h"] = sells
|
||
result["sources_used"].append("dexscreener")
|
||
|
||
# If only buys and no sells, likely honeypot
|
||
if buys > 5 and sells == 0:
|
||
result["is_honeypot"] = True
|
||
result["confidence"] = 0.85
|
||
result["reason"] = "Many buys but zero sells in 24h"
|
||
elif buys > 0 and sells > 0:
|
||
result["is_honeypot"] = False
|
||
result["confidence"] = 0.7
|
||
result["reason"] = "Normal buy/sell ratio"
|
||
else:
|
||
result["is_honeypot"] = None
|
||
result["confidence"] = 0.3
|
||
result["reason"] = "Insufficient trading data"
|
||
else:
|
||
result["is_honeypot"] = None
|
||
result["reason"] = "No trading pairs found"
|
||
except Exception:
|
||
pass
|
||
|
||
# Check contract for EVM chains
|
||
if chain in ["base", "ethereum", "bsc"]:
|
||
try:
|
||
# Get contract code
|
||
code = await rpc_call(chain, "eth_getCode", [address, "latest"])
|
||
if code == "0x" or code is None:
|
||
result["is_contract"] = False
|
||
result["reason"] = "No contract code found"
|
||
else:
|
||
result["is_contract"] = True
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/honeypot_check")
|
||
async def honeypot_check(req: GenericRequest):
|
||
"""Honeypot detection."""
|
||
try:
|
||
address = req.address or req.token or ""
|
||
result = await _honeypot_check(address, req.chain)
|
||
await record_x402_payment("honeypot_check", "0.05", address)
|
||
return {
|
||
"tool": "Honeypot Check",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Honeypot check failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 23: Portfolio Tracker ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _portfolio_tracker(addresses: list[str], chain: str) -> dict:
|
||
"""Multi-wallet portfolio tracker."""
|
||
result = {"addresses": addresses, "chain": chain, "sources_used": [], "wallets": []}
|
||
|
||
for addr in addresses[:5]: # Limit to 5 wallets
|
||
wallet_data = {"address": addr, "tokens": []}
|
||
|
||
# Solana balance
|
||
if chain == "solana":
|
||
try:
|
||
balance = await rpc_call("solana", "getBalance", [addr])
|
||
if balance is not None:
|
||
wallet_data["sol_balance"] = balance / 1e9
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
# EVM balance
|
||
elif chain in ["base", "ethereum", "bsc"]:
|
||
try:
|
||
balance = await rpc_call(chain, "eth_getBalance", [addr, "latest"])
|
||
if balance:
|
||
wallet_data["native_balance"] = int(balance, 16) / 1e18
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
result["wallets"].append(wallet_data)
|
||
|
||
result["total_wallets"] = len(result["wallets"])
|
||
return result
|
||
|
||
|
||
@router.post("/portfolio_tracker")
|
||
async def portfolio_tracker(req: WalletListRequest):
|
||
"""Multi-wallet portfolio tracker."""
|
||
try:
|
||
result = await _portfolio_tracker(req.addresses, req.chain)
|
||
await record_x402_payment("portfolio_tracker", "0.10", ",".join(req.addresses[:3]))
|
||
return {
|
||
"tool": "Portfolio Tracker",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Portfolio tracker failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 24: Copy Trade Finder ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _copy_trade_finder(chain: str) -> dict:
|
||
"""Find profitable wallets to copy trade."""
|
||
result = {"chain": chain, "sources_used": []}
|
||
|
||
# DexScreener for top gainers
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.dexscreener.com/latest/dex/search?q="])
|
||
if data and data.get("pairs"):
|
||
gainers = sorted(
|
||
data["pairs"], key=lambda p: p.get("priceChange", {}).get("h24", 0), reverse=True
|
||
)[:10]
|
||
result["top_gainers"] = [
|
||
{
|
||
"symbol": p.get("baseToken", {}).get("symbol"),
|
||
"price_change_24h": p.get("priceChange", {}).get("h24", 0),
|
||
"volume_24h": p.get("volume", {}).get("h24", 0),
|
||
"liquidity": p.get("liquidity", {}).get("usd", 0),
|
||
}
|
||
for p in gainers
|
||
]
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
result["smart_wallets"] = [] # Would need on-chain analysis for this
|
||
result["copy_trades"] = []
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/copy_trade_finder")
|
||
async def copy_trade_finder(req: GenericRequest):
|
||
"""Copy trade intelligence."""
|
||
try:
|
||
result = await _copy_trade_finder(req.chain)
|
||
await record_x402_payment("copy_trade_finder", "0.10", "scan")
|
||
return {
|
||
"tool": "Copy Trade Finder",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Copy trade finder failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 25: Token Comparison ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/token_comparison")
|
||
async def token_comparison(req: MultiTokenRequest):
|
||
"""Side-by-side token comparison."""
|
||
try:
|
||
comparisons = []
|
||
for addr in req.addresses[:5]:
|
||
result = await _token_forensics(addr, req.chain)
|
||
comparisons.append(
|
||
{
|
||
"address": addr,
|
||
"price_usd": result.get("price_usd", 0),
|
||
"liquidity_usd": result.get("liquidity_usd", 0),
|
||
"volume_24h": result.get("volume_24h", 0),
|
||
"risk_score": result.get("risk_score", 0),
|
||
"sources_used": result.get("sources_used", []),
|
||
}
|
||
)
|
||
|
||
await record_x402_payment("token_comparison", "0.08", ",".join(req.addresses[:3]))
|
||
return {
|
||
"tool": "Token Comparison",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"tokens": comparisons,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Token comparison failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 26: Risk Monitor ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _risk_monitor(address: str, chain: str) -> dict:
|
||
"""Real-time risk monitoring."""
|
||
result = {"address": address, "chain": chain, "sources_used": [], "alerts": []}
|
||
|
||
# Check DexScreener for anomalies
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
liq = pair.get("liquidity", {}).get("usd", 0)
|
||
pair.get("volume", {}).get("h24", 0)
|
||
price_change = pair.get("priceChange", {}).get("h24", 0)
|
||
|
||
if price_change < -50:
|
||
result["alerts"].append(
|
||
{
|
||
"type": "price_crash",
|
||
"severity": "critical",
|
||
"detail": f"Price down {price_change}% in 24h",
|
||
}
|
||
)
|
||
elif price_change < -20:
|
||
result["alerts"].append(
|
||
{
|
||
"type": "price_drop",
|
||
"severity": "warning",
|
||
"detail": f"Price down {price_change}% in 24h",
|
||
}
|
||
)
|
||
|
||
if liq < 1000:
|
||
result["alerts"].append(
|
||
{
|
||
"type": "low_liquidity",
|
||
"severity": "warning",
|
||
"detail": f"Liquidity only ${liq:,.0f}",
|
||
}
|
||
)
|
||
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
result["alert_count"] = len(result["alerts"])
|
||
result["risk_level"] = (
|
||
"critical"
|
||
if any(a["severity"] == "critical" for a in result["alerts"])
|
||
else "warning"
|
||
if result["alerts"]
|
||
else "normal"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/risk_monitor")
|
||
async def risk_monitor(req: GenericRequest):
|
||
"""Real-time risk monitoring."""
|
||
try:
|
||
address = req.address or req.token or ""
|
||
result = await _risk_monitor(address, req.chain)
|
||
await record_x402_payment("risk_monitor", "0.05", address)
|
||
return {
|
||
"tool": "Risk Monitor",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Risk monitor failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 27: DeFi Yield Scanner ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _defi_yield_scanner(chain: str) -> dict:
|
||
"""DeFi yield scanner."""
|
||
result = {"chain": chain, "sources_used": [], "pools": []}
|
||
|
||
# DeFiLlama yields
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://yields.llama.fi/pools"])
|
||
if data and data.get("data"):
|
||
pools = sorted(data["data"], key=lambda p: p.get("apy", 0), reverse=True)[:20]
|
||
for p in pools:
|
||
result["pools"].append(
|
||
{
|
||
"chain": p.get("chain"),
|
||
"project": p.get("project"),
|
||
"symbol": p.get("symbol"),
|
||
"tvl": p.get("tvlUsd", 0),
|
||
"apy": p.get("apy", 0),
|
||
"apy_base": p.get("apyBase", 0),
|
||
"apy_reward": p.get("apyReward", 0),
|
||
}
|
||
)
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
result["total_pools"] = len(result["pools"])
|
||
result["highest_apy"] = result["pools"][0]["apy"] if result["pools"] else 0
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/defi_yield_scanner")
|
||
async def defi_yield_scanner(req: GenericRequest):
|
||
"""DeFi yield scanner."""
|
||
try:
|
||
result = await _defi_yield_scanner(req.chain)
|
||
await record_x402_payment("defi_yield_scanner", "0.08", "scan")
|
||
return {
|
||
"tool": "DeFi Yield Scanner",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"DeFi yield scanner failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 28: NFT Wash Detector ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _nft_wash_detector(collection: str) -> dict:
|
||
"""NFT wash trading detection."""
|
||
result = {"collection": collection, "sources_used": [], "wash_signals": []}
|
||
|
||
# NFTPriceFloor (free API)
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://pricefloor-api.nftdata.io/v1/collection/{collection}"]
|
||
)
|
||
if data:
|
||
result["floor_price"] = data.get("floorPrice")
|
||
result["volume_24h"] = data.get("volume24h")
|
||
result["sources_used"].append("nftpricefloor")
|
||
except Exception:
|
||
pass
|
||
|
||
# OpenSea stats (public endpoint)
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.opensea.io/api/v1/collection/{collection}/stats"]
|
||
)
|
||
if data and data.get("stats"):
|
||
stats = data["stats"]
|
||
result["opensea"] = {
|
||
"floor_price": stats.get("floor_price"),
|
||
"total_volume": stats.get("total_volume"),
|
||
"num_owners": stats.get("num_owners"),
|
||
"one_day_volume": stats.get("one_day_volume"),
|
||
"one_day_sales": stats.get("one_day_sales"),
|
||
}
|
||
result["sources_used"].append("opensea")
|
||
except Exception:
|
||
pass
|
||
|
||
# Wash trading signals
|
||
os_stats = result.get("opensea", {})
|
||
one_day_vol = os_stats.get("one_day_volume", 0)
|
||
one_day_sales = os_stats.get("one_day_sales", 0)
|
||
|
||
if one_day_sales > 0:
|
||
avg_sale_price = one_day_vol / one_day_sales
|
||
floor = os_stats.get("floor_price", 0)
|
||
if avg_sale_price > floor * 10:
|
||
result["wash_signals"].append("Average sale price far exceeds floor")
|
||
if one_day_sales > 100 and os_stats.get("num_owners", 0) < 50:
|
||
result["wash_signals"].append("High sales volume with few owners")
|
||
|
||
result["wash_risk"] = (
|
||
"high"
|
||
if len(result["wash_signals"]) >= 2
|
||
else "medium"
|
||
if result["wash_signals"]
|
||
else "low"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/nft_wash_detector")
|
||
async def nft_wash_detector(req: GenericRequest):
|
||
"""NFT wash trading detector."""
|
||
try:
|
||
collection = req.query or req.address or ""
|
||
result = await _nft_wash_detector(collection)
|
||
await record_x402_payment("nft_wash_detector", "0.10", collection)
|
||
return {
|
||
"tool": "NFT Wash Detector",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"NFT wash detector failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 29: Bridge Security ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _bridge_security() -> dict:
|
||
"""Bridge security monitoring."""
|
||
result = {"sources_used": [], "bridges": []}
|
||
|
||
# DeFiLlama bridges
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://bridges.llama.fi/bridges"])
|
||
if data and data.get("bridges"):
|
||
for b in data["bridges"][:10]:
|
||
result["bridges"].append(
|
||
{
|
||
"name": b.get("name"),
|
||
"tvl": b.get("totalDeposits", 0),
|
||
"chains": b.get("chains", []),
|
||
}
|
||
)
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
# DeFiLlama protocols (check for bridge exploits)
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/protocols"])
|
||
if data:
|
||
bridge_protocols = [p for p in data if p.get("category") == "Bridge"]
|
||
result["bridge_count"] = len(bridge_protocols)
|
||
result["total_bridge_tvl"] = sum(p.get("tvl", 0) for p in bridge_protocols)
|
||
result["sources_used"].append("defillama_protocols")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/bridge_security")
|
||
async def bridge_security(req: GenericRequest):
|
||
"""Bridge security monitoring."""
|
||
try:
|
||
result = await _bridge_security()
|
||
await record_x402_payment("bridge_security", "0.08", "scan")
|
||
return {
|
||
"tool": "Bridge Security",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Bridge security failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 30: Gas Forecast ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _gas_forecast(chain: str) -> dict:
|
||
"""Gas price forecast."""
|
||
result = {"chain": chain, "sources_used": []}
|
||
|
||
# EVM gas prices
|
||
if chain in ["base", "ethereum", "bsc"]:
|
||
try:
|
||
data = await rpc_call(chain, "eth_gasPrice", [])
|
||
if data:
|
||
gas_wei = int(data, 16)
|
||
gas_gwei = gas_wei / 1e9
|
||
result["current_gas_gwei"] = gas_gwei
|
||
result["current_gas_usd_estimate"] = gas_gwei * 0.001 # Rough estimate
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
# Solana compute units
|
||
elif chain == "solana":
|
||
try:
|
||
result["sol_compute_unit_price"] = "dynamic (based on priority fees)"
|
||
result["sources_used"].append("solana_docs")
|
||
except Exception:
|
||
pass
|
||
|
||
# Blocknative gas station (free tier)
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.blocknative.com/gasprices"])
|
||
if data and data.get("estimatedPrices"):
|
||
result["blocknative"] = data["estimatedPrices"]
|
||
result["sources_used"].append("blocknative")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/gas_forecast")
|
||
async def gas_forecast(req: GenericRequest):
|
||
"""Gas price forecast."""
|
||
try:
|
||
result = await _gas_forecast(req.chain)
|
||
await record_x402_payment("gas_forecast", "0.05", req.chain)
|
||
return {
|
||
"tool": "Gas Forecast",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Gas forecast failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 31: Sniper Alert ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _sniper_alert(chain: str, hours: int) -> dict:
|
||
"""Sniper bot detection and alerts."""
|
||
result = {"chain": chain, "hours": hours, "sources_used": [], "sniper_activity": []}
|
||
|
||
# PumpFun for new tokens (sniper targets)
|
||
if chain == "solana":
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[
|
||
"https://frontend-api.pump.fun/coins?offset=0&limit=10&sort=created_timestamp&order=desc"
|
||
]
|
||
)
|
||
if data and isinstance(data, list):
|
||
for c in data[:5]:
|
||
result["sniper_activity"].append(
|
||
{
|
||
"token": c.get("name"),
|
||
"mint": c.get("mint"),
|
||
"age_minutes": "recent",
|
||
"sniper_risk": "high"
|
||
if c.get("usdMarketCap", 0) > 100000
|
||
else "medium",
|
||
}
|
||
)
|
||
result["sources_used"].append("pumpfun")
|
||
except Exception:
|
||
pass
|
||
|
||
# DexScreener for abnormal early trading
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.dexscreener.com/latest/dex/search?q="])
|
||
if data and data.get("pairs"):
|
||
for p in data["pairs"][:10]:
|
||
txns = p.get("txns", {}).get("m5", {})
|
||
buys = txns.get("buys", 0)
|
||
if buys > 20:
|
||
result["sniper_activity"].append(
|
||
{
|
||
"token": p.get("baseToken", {}).get("symbol"),
|
||
"address": p.get("baseToken", {}).get("address"),
|
||
"buys_5m": buys,
|
||
"sniper_risk": "high",
|
||
"source": "dexscreener",
|
||
}
|
||
)
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
result["total_alerts"] = len(result["sniper_activity"])
|
||
return result
|
||
|
||
|
||
@router.post("/sniper_alert")
|
||
async def sniper_alert(req: GenericRequest):
|
||
"""Sniper bot detection."""
|
||
try:
|
||
result = await _sniper_alert(req.chain, req.hours)
|
||
await record_x402_payment("sniper_alert", "0.05", req.chain)
|
||
return {
|
||
"tool": "Sniper Alert",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Sniper alert failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 32: Liquidity Flow ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _liquidity_flow(address: str, chain: str) -> dict:
|
||
"""Liquidity flow analysis."""
|
||
result = {"address": address, "chain": chain, "sources_used": []}
|
||
|
||
# DexScreener liquidity data
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pairs = data["pairs"]
|
||
result["pairs"] = []
|
||
total_liq = 0
|
||
for p in pairs[:5]:
|
||
liq = p.get("liquidity", {}).get("usd", 0)
|
||
total_liq += liq
|
||
result["pairs"].append(
|
||
{
|
||
"exchange": p.get("dexId"),
|
||
"base": p.get("baseToken", {}).get("symbol"),
|
||
"quote": p.get("quoteToken", {}).get("symbol"),
|
||
"liquidity_usd": liq,
|
||
"volume_24h": p.get("volume", {}).get("h24", 0),
|
||
}
|
||
)
|
||
result["total_liquidity_usd"] = total_liq
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# DeFiLlama for protocol TVL
|
||
try:
|
||
data, _ = await fetch_with_fallback([f"https://coins.llama.fi/token/{chain}:{address}"])
|
||
if data and data.get("coins"):
|
||
coin = data["coins"][f"{chain}:{address}"]
|
||
result["price"] = coin.get("price", 0)
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/liquidity_flow")
|
||
async def liquidity_flow(req: GenericRequest):
|
||
"""Liquidity flow analysis."""
|
||
try:
|
||
address = req.address or req.token or ""
|
||
result = await _liquidity_flow(address, req.chain)
|
||
await record_x402_payment("liquidity_flow", "0.08", address)
|
||
return {
|
||
"tool": "Liquidity Flow",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Liquidity flow failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 33: Rug Pull Predictor ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _rug_pull_predictor(address: str, chain: str) -> dict:
|
||
"""Rug pull prediction model."""
|
||
result = {"address": address, "chain": chain, "sources_used": [], "signals": []}
|
||
|
||
# Get token data
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
|
||
)
|
||
if data and data.get("pairs"):
|
||
pair = data["pairs"][0]
|
||
liq = pair.get("liquidity", {}).get("usd", 0)
|
||
vol = pair.get("volume", {}).get("h24", 0)
|
||
pair_age = pair.get("pairCreatedAt", 0)
|
||
import time
|
||
|
||
age_hours = (time.time() * 1000 - pair_age) / 3600000 if pair_age else 999
|
||
|
||
# Rug pull signals
|
||
if liq < 5000:
|
||
result["signals"].append(
|
||
{"type": "low_liq", "weight": 0.3, "detail": f"Liquidity ${liq:,.0f}"}
|
||
)
|
||
if age_hours < 24:
|
||
result["signals"].append(
|
||
{"type": "new_token", "weight": 0.25, "detail": f"Age {age_hours:.1f}h"}
|
||
)
|
||
if vol > liq * 10:
|
||
result["signals"].append(
|
||
{
|
||
"type": "vol_spike",
|
||
"weight": 0.2,
|
||
"detail": f"Volume {vol / liq:.1f}x liquidity",
|
||
}
|
||
)
|
||
|
||
# Check holder concentration via Solana RPC
|
||
if chain == "solana":
|
||
try:
|
||
token_accounts = await rpc_call(
|
||
"solana",
|
||
"getTokenAccountsByOwner",
|
||
[
|
||
address,
|
||
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
|
||
{"encoding": "jsonParsed"},
|
||
],
|
||
)
|
||
if token_accounts and token_accounts.get("value"):
|
||
holders = len(token_accounts["value"])
|
||
if holders < 50:
|
||
result["signals"].append(
|
||
{
|
||
"type": "few_holders",
|
||
"weight": 0.25,
|
||
"detail": f"Only {holders} holders",
|
||
}
|
||
)
|
||
result["holder_count"] = holders
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
result["sources_used"].append("dexscreener")
|
||
except Exception:
|
||
pass
|
||
|
||
# Calculate rug pull probability
|
||
total_weight = sum(s["weight"] for s in result["signals"])
|
||
rug_probability = min(1.0, total_weight)
|
||
|
||
result["rug_probability"] = rug_probability
|
||
result["risk_level"] = (
|
||
"CRITICAL"
|
||
if rug_probability >= 0.7
|
||
else "HIGH"
|
||
if rug_probability >= 0.4
|
||
else "MEDIUM"
|
||
if rug_probability >= 0.2
|
||
else "LOW"
|
||
)
|
||
result["recommendation"] = (
|
||
"DO NOT BUY"
|
||
if rug_probability >= 0.7
|
||
else "EXTREME CAUTION"
|
||
if rug_probability >= 0.4
|
||
else "MONITOR"
|
||
if rug_probability >= 0.2
|
||
else "OK"
|
||
)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/rug_pull_predictor")
|
||
async def rug_pull_predictor(req: GenericRequest):
|
||
"""Rug pull prediction."""
|
||
try:
|
||
address = req.address or req.token or ""
|
||
result = await _rug_pull_predictor(address, req.chain)
|
||
await record_x402_payment("rug_pull_predictor", "0.10", address)
|
||
return {
|
||
"tool": "Rug Pull Predictor",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Rug pull predictor failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 34: Airdrop Finder ($0.05)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _airdrop_finder(chain: str) -> dict:
|
||
"""Airdrop opportunity finder."""
|
||
result = {"chain": chain, "sources_used": [], "opportunities": []}
|
||
|
||
# DeFiLlama for new protocols (potential airdrops)
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.llama.fi/protocols"])
|
||
if data:
|
||
# Filter for protocols without tokens
|
||
no_token = [p for p in data if not p.get("token") and p.get("tvl", 0) > 10000000][:10]
|
||
for p in no_token:
|
||
result["opportunities"].append(
|
||
{
|
||
"name": p.get("name"),
|
||
"category": p.get("category"),
|
||
"tvl": p.get("tvl", 0),
|
||
"chains": p.get("chains", []),
|
||
"airdrop_potential": "high" if p.get("tvl", 0) > 100000000 else "medium",
|
||
}
|
||
)
|
||
result["sources_used"].append("defillama")
|
||
except Exception:
|
||
pass
|
||
|
||
result["total_opportunities"] = len(result["opportunities"])
|
||
return result
|
||
|
||
|
||
@router.post("/airdrop_finder")
|
||
async def airdrop_finder(req: GenericRequest):
|
||
"""Airdrop opportunity finder."""
|
||
try:
|
||
result = await _airdrop_finder(req.chain)
|
||
await record_x402_payment("airdrop_finder", "0.05", "scan")
|
||
return {
|
||
"tool": "Airdrop Finder",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Airdrop finder failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 35: MEV Protection ($0.08)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
async def _mev_protection(chain: str) -> dict:
|
||
"""MEV protection analysis."""
|
||
result = {"chain": chain, "sources_used": [], "mev_stats": {}}
|
||
|
||
if chain == "solana":
|
||
# Jito MEV stats
|
||
try:
|
||
data, _ = await fetch_with_fallback(
|
||
["https://stats.jito.network/api/v1/bundles?limit=10"]
|
||
)
|
||
if data:
|
||
result["mev_stats"]["jito"] = True
|
||
result["sources_used"].append("jito")
|
||
except Exception:
|
||
pass
|
||
|
||
# Check for MEV bots in recent blocks
|
||
try:
|
||
slot = await rpc_call("solana", "getSlot", [])
|
||
if slot:
|
||
result["current_slot"] = slot
|
||
result["mev_stats"]["slot"] = slot
|
||
result["sources_used"].append("solana_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
elif chain in ["base", "ethereum", "bsc"]:
|
||
# Flashbots stats
|
||
try:
|
||
data, _ = await fetch_with_fallback(["https://api.flashbots.net/stats"])
|
||
if data:
|
||
result["mev_stats"]["flashbots"] = True
|
||
result["sources_used"].append("flashbots")
|
||
except Exception:
|
||
pass
|
||
|
||
# Gas price for MEV detection
|
||
try:
|
||
gas = await rpc_call(chain, "eth_gasPrice", [])
|
||
if gas:
|
||
result["gas_price_gwei"] = int(gas, 16) / 1e9
|
||
result["sources_used"].append(f"{chain}_rpc")
|
||
except Exception:
|
||
pass
|
||
|
||
result["mev_risk"] = (
|
||
"high" if chain == "ethereum" else "medium" if chain in ["base", "bsc"] else "low"
|
||
)
|
||
result["protection_tips"] = [
|
||
"Use private RPC endpoints for large trades",
|
||
"Set appropriate slippage tolerance",
|
||
"Avoid trading during high volatility periods",
|
||
"Use MEV-protected order routing",
|
||
]
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/mev_protection")
|
||
async def mev_protection(req: GenericRequest):
|
||
"""MEV protection analysis."""
|
||
try:
|
||
result = await _mev_protection(req.chain)
|
||
await record_x402_payment("mev_protection", "0.08", req.chain)
|
||
return {
|
||
"tool": "MEV Protection",
|
||
"version": "2.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"MEV protection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# DISCOVERY ENDPOINT (Free)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.get("/discovery")
|
||
async def tools_discovery():
|
||
"""Human-friendly tool discovery — organized by category with SEO descriptions.
|
||
|
||
Returns all 71 RMI tools with pricing, descriptions, and categories.
|
||
MCP external tools (154+) available via MCP tools/list on gateway workers.
|
||
"""
|
||
from app.tool_catalog import get_human_catalog
|
||
|
||
catalog = get_human_catalog()
|
||
return catalog
|
||
|
||
|
||
@router.get("/catalog")
|
||
async def tools_catalog_bot():
|
||
"""Bot-optimized tool catalog — flat list with IDs, pricing, chain support.
|
||
|
||
Designed for AI agents to quickly discover and call tools.
|
||
Minimal descriptions, structured parameters, x402 protocol info.
|
||
"""
|
||
from app.tool_catalog import get_bot_catalog
|
||
|
||
return get_bot_catalog()
|
||
|
||
|
||
# ── OpenAI-Compatible Tools Endpoint ───────────────────────────
|
||
|
||
|
||
async def _build_tools_from_catalog():
|
||
"""Build tool list from TOOL_PRICES (source of truth for all 201+ tools)."""
|
||
from app.routers.x402_enforcement import TOOL_PRICES
|
||
|
||
tools = []
|
||
for tool_id, pricing in sorted(TOOL_PRICES.items()):
|
||
desc = pricing.get("description", f"{tool_id} — crypto intelligence tool")
|
||
category = pricing.get("category", "analysis")
|
||
chain = pricing.get("chain")
|
||
base_tool = pricing.get("base_tool")
|
||
is_variant = bool(chain)
|
||
tools.append(
|
||
{
|
||
"id": tool_id,
|
||
"description": desc,
|
||
"price_usd": pricing.get("price_usd", 0.01),
|
||
"category": category,
|
||
"chain": chain,
|
||
"base_tool": base_tool,
|
||
"is_variant": is_variant,
|
||
"trial_free": pricing.get("trial_free", 1),
|
||
}
|
||
)
|
||
return tools
|
||
|
||
|
||
@router.get("/openai-tools")
|
||
async def openai_tools():
|
||
"""Returns ALL 201+ tool definitions in OpenAI function calling format.
|
||
Use this with OpenAI Agents SDK or GPT-4o function calling.
|
||
Each tool maps to POST /api/v1/x402-tools/{tool_name} with x402 payment."""
|
||
raw_tools = await _build_tools_from_catalog()
|
||
openai_tools = []
|
||
for t in raw_tools:
|
||
openai_tools.append(
|
||
{
|
||
"type": "function",
|
||
"function": {
|
||
"name": t["id"],
|
||
"description": f"{t['description']} — ${t['price_usd']:.2f}/call, {t['trial_free']} free trial(s). Category: {t['category']}.",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"address": {
|
||
"type": "string",
|
||
"description": f"Token or wallet address to analyze with {t['id']}",
|
||
},
|
||
"chain": {
|
||
"type": "string",
|
||
"description": "Blockchain: solana, base, ethereum, bsc. Default: solana",
|
||
"default": "solana",
|
||
},
|
||
},
|
||
"required": ["address"],
|
||
},
|
||
},
|
||
}
|
||
)
|
||
return {
|
||
"service": "Rug Munch Intelligence",
|
||
"tagline": "We build tools to keep the crypto space safer",
|
||
"total_tools": len(openai_tools),
|
||
"followers_x": "67,000+",
|
||
"telegram_users": "7,000+",
|
||
"networks": [
|
||
"base",
|
||
"solana",
|
||
"ethereum",
|
||
"bsc",
|
||
"arbitrum",
|
||
"polygon",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"optimism",
|
||
"tron",
|
||
"bitcoin",
|
||
"sepa",
|
||
],
|
||
"protocol": "x402",
|
||
"payment_required": True,
|
||
"tools": openai_tools,
|
||
}
|
||
|
||
|
||
# ── LangChain Tools Endpoint ────────────────────────────────────
|
||
|
||
|
||
@router.get("/langchain-tools")
|
||
async def langchain_tools():
|
||
"""Returns ALL 201+ tool definitions in LangChain format.
|
||
Use with LangChain agents, LangGraph, or any LangChain-based system."""
|
||
raw_tools = await _build_tools_from_catalog()
|
||
langchain_tools = []
|
||
for t in raw_tools:
|
||
langchain_tools.append(
|
||
{
|
||
"name": t["id"],
|
||
"description": f"{t['description']} — ${t['price_usd']:.2f}/call, {t['trial_free']} free trial(s). Category: {t['category']}.",
|
||
"args_schema": {
|
||
"address": {
|
||
"type": "string",
|
||
"description": f"Token or wallet address to analyze with {t['id']}",
|
||
},
|
||
"chain": {
|
||
"type": "string",
|
||
"description": "Blockchain: solana, base, ethereum, bsc. Default: solana",
|
||
"default": "solana",
|
||
},
|
||
},
|
||
"required": ["address"],
|
||
"endpoint": f"/api/v1/x402-tools/{t['id']}",
|
||
"method": "POST",
|
||
}
|
||
)
|
||
return {
|
||
"service": "Rug Munch Intelligence",
|
||
"tagline": "We build tools to keep the crypto space safer",
|
||
"total_tools": len(langchain_tools),
|
||
"followers_x": "67,000+",
|
||
"telegram_users": "7,000+",
|
||
"networks": [
|
||
"base",
|
||
"solana",
|
||
"ethereum",
|
||
"bsc",
|
||
"arbitrum",
|
||
"polygon",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"optimism",
|
||
"tron",
|
||
"bitcoin",
|
||
"sepa",
|
||
],
|
||
"protocol": "x402",
|
||
"format": "langchain",
|
||
"usage": "pip install langchain && use with create_react_agent or LangGraph",
|
||
"tools": langchain_tools,
|
||
}
|
||
|
||
|
||
# ── Anthropic Claude API Tools Endpoint ─────────────────────────
|
||
|
||
|
||
@router.get("/anthropic-tools")
|
||
async def anthropic_tools():
|
||
"""Returns ALL 201+ tool definitions in Anthropic Claude API format.
|
||
Use with Claude API (messages API) for native tool use."""
|
||
raw_tools = await _build_tools_from_catalog()
|
||
anthropic_tools_list = []
|
||
for t in raw_tools:
|
||
anthropic_tools_list.append(
|
||
{
|
||
"name": t["id"],
|
||
"description": f"{t['description']} — ${t['price_usd']:.2f}/call, {t['trial_free']} free trial(s). Category: {t['category']}.",
|
||
"input_schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"address": {
|
||
"type": "string",
|
||
"description": f"Token or wallet address to analyze with {t['id']}",
|
||
},
|
||
"chain": {
|
||
"type": "string",
|
||
"description": "Blockchain: solana, base, ethereum, bsc. Default: solana",
|
||
"default": "solana",
|
||
},
|
||
},
|
||
"required": ["address"],
|
||
},
|
||
}
|
||
)
|
||
return {
|
||
"service": "Rug Munch Intelligence",
|
||
"tagline": "We build tools to keep the crypto space safer",
|
||
"total_tools": len(anthropic_tools_list),
|
||
"followers_x": "67,000+",
|
||
"telegram_users": "7,000+",
|
||
"networks": [
|
||
"base",
|
||
"solana",
|
||
"ethereum",
|
||
"bsc",
|
||
"arbitrum",
|
||
"polygon",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"optimism",
|
||
"tron",
|
||
"bitcoin",
|
||
"sepa",
|
||
],
|
||
"protocol": "x402",
|
||
"format": "anthropic_claude_api",
|
||
"usage": "pip install anthropic && use with client.messages.create(tools=...)",
|
||
"tools": anthropic_tools_list,
|
||
}
|
||
|
||
|
||
# ── Google Gemini Function Declarations ─────────────────────────
|
||
|
||
|
||
@router.get("/gemini-tools")
|
||
async def gemini_tools():
|
||
"""Returns ALL 201+ tool definitions in Google Gemini function calling format.
|
||
Use with Google AI SDK or Vertex AI for Gemini models."""
|
||
raw_tools = await _build_tools_from_catalog()
|
||
gemini_declarations = []
|
||
for t in raw_tools:
|
||
gemini_declarations.append(
|
||
{
|
||
"name": t["id"],
|
||
"description": f"{t['description']} — ${t['price_usd']:.2f}/call, {t['trial_free']} free trial(s). Category: {t['category']}.",
|
||
"parameters": {
|
||
"type": "OBJECT",
|
||
"properties": {
|
||
"address": {
|
||
"type": "STRING",
|
||
"description": f"Token or wallet address to analyze with {t['id']}",
|
||
},
|
||
"chain": {
|
||
"type": "STRING",
|
||
"description": "Blockchain: solana, base, ethereum, bsc. Default: solana",
|
||
},
|
||
},
|
||
"required": ["address"],
|
||
},
|
||
}
|
||
)
|
||
|
||
return {
|
||
"service": "Rug Munch Intelligence",
|
||
"tagline": "We build tools to keep the crypto space safer",
|
||
"followers_x": "67,000+",
|
||
"telegram_users": "7,000+",
|
||
"total_tools": len(gemini_declarations),
|
||
"networks": [
|
||
"base",
|
||
"solana",
|
||
"ethereum",
|
||
"bsc",
|
||
"arbitrum",
|
||
"polygon",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"optimism",
|
||
"tron",
|
||
"bitcoin",
|
||
"sepa",
|
||
],
|
||
"protocol": "x402",
|
||
"format": "google_gemini",
|
||
"usage": "pip install google-genai && use with model.generate_content(tools=...)",
|
||
"function_declarations": gemini_declarations,
|
||
}
|
||
|
||
|
||
# ── Framework Discovery Endpoint ────────────────────────────────
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 36: Comprehensive Audit (Super Tool) ($0.15)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
class AuditRequest(BaseModel):
|
||
address: str
|
||
chain: str = "solana"
|
||
|
||
|
||
@router.post("/comprehensive_audit")
|
||
async def comprehensive_audit(req: AuditRequest):
|
||
"""One-call deep audit combining rug check, forensics, social, and whale analysis."""
|
||
import httpx
|
||
from fastapi import HTTPException
|
||
|
||
try:
|
||
BASE_URL = "http://localhost:8000"
|
||
|
||
async def get_rug():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/rugshield",
|
||
json={"address": req.address, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_forensics():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/forensics",
|
||
json={"address": req.address, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_social():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/sentiment",
|
||
json={"token": req.address, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_whale():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/whale",
|
||
json={"address": req.address, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
rug_data, forensics_data, social_data, whale_data = await asyncio.gather(
|
||
get_rug(), get_forensics(), get_social(), get_whale(), return_exceptions=True
|
||
)
|
||
|
||
if isinstance(rug_data, Exception):
|
||
rug_data = None
|
||
if isinstance(forensics_data, Exception):
|
||
forensics_data = None
|
||
if isinstance(social_data, Exception):
|
||
social_data = None
|
||
if isinstance(whale_data, Exception):
|
||
whale_data = None
|
||
|
||
risk_score = 50
|
||
factors = []
|
||
recommendation = "HOLD"
|
||
|
||
if rug_data and rug_data.get("is_honeypot"):
|
||
risk_score += 30
|
||
factors.append("CRITICAL: Potential Honeypot detected")
|
||
|
||
if rug_data and not rug_data.get("liquidity_locked"):
|
||
risk_score += 15
|
||
factors.append("WARNING: Liquidity not locked")
|
||
|
||
if forensics_data:
|
||
liq_usd = forensics_data.get("liquidity_usd", 0)
|
||
if liq_usd > 0 and liq_usd < 1000:
|
||
risk_score += 20
|
||
factors.append(f"LOW LIQUIDITY: Only ${liq_usd:,.2f} locked")
|
||
|
||
vol_24h = forensics_data.get("volume_24h", 0)
|
||
if liq_usd > 0 and vol_24h > liq_usd * 5:
|
||
risk_score += 10
|
||
factors.append("High volume/liquidity ratio (possible wash trading)")
|
||
|
||
if social_data:
|
||
sentiment_score = social_data.get("sentiment_score", 0)
|
||
if sentiment_score > 0.7:
|
||
risk_score -= 10
|
||
factors.append("Positive social sentiment")
|
||
elif sentiment_score < 0.3:
|
||
risk_score += 10
|
||
factors.append("Negative social sentiment")
|
||
|
||
if whale_data:
|
||
whale_count = whale_data.get("whale_count", 0)
|
||
if whale_count > 0:
|
||
risk_score -= 5
|
||
factors.append(f"{whale_count} whale(s) detected")
|
||
|
||
risk_score = max(0, min(100, risk_score))
|
||
|
||
if risk_score >= 70:
|
||
recommendation = "HIGH RISK / AVOID"
|
||
elif risk_score >= 50:
|
||
recommendation = "CAUTION / HIGH VOLATILITY"
|
||
else:
|
||
recommendation = "RELATIVELY SAFE / MONITOR"
|
||
|
||
result = {
|
||
"tool": "Comprehensive Audit",
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"summary": {
|
||
"risk_score": risk_score,
|
||
"recommendation": recommendation,
|
||
"factors": factors,
|
||
},
|
||
"sub_reports": {
|
||
"rug_check": rug_data,
|
||
"forensics": forensics_data,
|
||
"social": social_data,
|
||
"whale_analysis": whale_data,
|
||
},
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
await record_x402_payment("comprehensive_audit", "0.15", req.address)
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Comprehensive audit failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
class SmartMoneyRequest(BaseModel):
|
||
wallet: str
|
||
chain: str = "solana"
|
||
|
||
|
||
@router.post("/smart_money_alpha")
|
||
async def smart_money_alpha(req: SmartMoneyRequest):
|
||
"""One-call smart money signal combining wallet analysis, smart money tracking, and cluster/sybil detection."""
|
||
import httpx
|
||
|
||
try:
|
||
BASE_URL = "http://localhost:8000"
|
||
|
||
async def get_wallet():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/wallet",
|
||
json={"address": req.wallet, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_smartmoney():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.get(f"{BASE_URL}/api/v1/x402-tools/smartmoney")
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_cluster():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/cluster",
|
||
json={"address": req.wallet, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_insider():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/insider",
|
||
json={"address": req.wallet, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
wallet_data, smartmoney_data, cluster_data, insider_data = await asyncio.gather(
|
||
get_wallet(), get_smartmoney(), get_cluster(), get_insider(), return_exceptions=True
|
||
)
|
||
|
||
for i, d in enumerate([wallet_data, smartmoney_data, cluster_data, insider_data]):
|
||
if isinstance(d, Exception):
|
||
[wallet_data, smartmoney_data, cluster_data, insider_data][i] = None
|
||
|
||
alpha_score = 50
|
||
signals = []
|
||
|
||
if wallet_data:
|
||
tx_count = wallet_data.get("tx_count", 0)
|
||
if tx_count > 1000:
|
||
alpha_score += 10
|
||
signals.append(f"Active wallet ({tx_count}+ transactions)")
|
||
profit_ratio = wallet_data.get("profit_ratio", 0)
|
||
if profit_ratio > 0.5:
|
||
alpha_score += 15
|
||
signals.append(f"Profitable trader ({profit_ratio:.0%} win rate)")
|
||
|
||
if smartmoney_data:
|
||
sm_alerts = smartmoney_data.get("smart_money_alerts", [])
|
||
if sm_alerts:
|
||
alpha_score += 10
|
||
signals.append(f"{len(sm_alerts)} active smart money movements")
|
||
|
||
if cluster_data:
|
||
cluster_size = cluster_data.get("cluster_size", 0)
|
||
if cluster_size > 5:
|
||
alpha_score -= 10
|
||
signals.append(f"Large cluster ({cluster_size} wallets) - possible sybil")
|
||
if cluster_data.get("is_sybil", False):
|
||
alpha_score -= 20
|
||
signals.append("SYBIL DETECTED - wallet part of coordinated group")
|
||
|
||
if insider_data and insider_data.get("insider_detected", False):
|
||
alpha_score += 20
|
||
signals.append("INSIDER ACTIVITY - wallet linked to early token access")
|
||
|
||
alpha_score = max(0, min(100, alpha_score))
|
||
|
||
verdict = "NEUTRAL"
|
||
if alpha_score >= 75:
|
||
verdict = "STRONG ALPHA - high-value wallet signals"
|
||
elif alpha_score >= 60:
|
||
verdict = "MODERATE ALPHA - watch for follow-up"
|
||
elif alpha_score <= 30:
|
||
verdict = "LOW SIGNAL - avoid or monitor passively"
|
||
|
||
result = {
|
||
"tool": "Smart Money Alpha",
|
||
"wallet": req.wallet,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"summary": {
|
||
"alpha_score": alpha_score,
|
||
"verdict": verdict,
|
||
"signals": signals,
|
||
},
|
||
"sub_reports": {
|
||
"wallet_analysis": wallet_data,
|
||
"smart_money": smartmoney_data,
|
||
"cluster_analysis": cluster_data,
|
||
"insider_detection": insider_data,
|
||
},
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
await record_x402_payment("smart_money_alpha", "0.25", req.wallet)
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Smart money alpha failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
class MemeVibeRequest(BaseModel):
|
||
token: str
|
||
chain: str = "solana"
|
||
|
||
|
||
@router.post("/meme_vibe_score")
|
||
async def meme_vibe_score(req: MemeVibeRequest):
|
||
"""One-call meme token vibe check combining sentiment, social signals, and launch analysis."""
|
||
import httpx
|
||
|
||
try:
|
||
BASE_URL = "http://localhost:8000"
|
||
|
||
async def get_sentiment():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/sentiment",
|
||
json={"token": req.token, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_social_signal():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.post(
|
||
f"{BASE_URL}/api/v1/x402-tools/social_signal",
|
||
json={"token": req.token, "chain": req.chain},
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
async def get_launch():
|
||
async with httpx.AsyncClient(timeout=10.0) as c:
|
||
r = await c.get(
|
||
f"{BASE_URL}/api/v1/x402-tools/launch?address={req.token}&chain={req.chain}"
|
||
)
|
||
return r.json() if r.status_code == 200 else None
|
||
|
||
sentiment_data, social_data, launch_data = await asyncio.gather(
|
||
get_sentiment(), get_social_signal(), get_launch(), return_exceptions=True
|
||
)
|
||
|
||
for i, d in enumerate([sentiment_data, social_data, launch_data]):
|
||
if isinstance(d, Exception):
|
||
[sentiment_data, social_data, launch_data][i] = None
|
||
|
||
vibe_score = 50
|
||
vibes = []
|
||
|
||
if sentiment_data:
|
||
score = sentiment_data.get("sentiment_score", 0)
|
||
if score > 0.7:
|
||
vibe_score += 15
|
||
vibes.append("Strong positive sentiment")
|
||
elif score < 0.3:
|
||
vibe_score -= 15
|
||
vibes.append("Negative sentiment detected")
|
||
trend = sentiment_data.get("sentiment_trend", "")
|
||
if trend:
|
||
vibes.append(f"Sentiment trend: {trend}")
|
||
|
||
if social_data:
|
||
engagement = social_data.get("engagement_score", 0)
|
||
bot_ratio = social_data.get("bot_ratio", 0)
|
||
if engagement > 0.7:
|
||
vibe_score += 10
|
||
vibes.append("High social engagement")
|
||
if bot_ratio > 0.5:
|
||
vibe_score -= 20
|
||
vibes.append(f"High bot ratio ({bot_ratio:.0%}) - likely artificial hype")
|
||
|
||
if launch_data:
|
||
bonding = launch_data.get("bonding_curve_progress", 0)
|
||
if bonding > 0.8:
|
||
vibe_score += 10
|
||
vibes.append("Bonding curve near completion - strong launch momentum")
|
||
elif bonding < 0.2:
|
||
vibe_score -= 10
|
||
vibes.append("Early bonding curve - high risk")
|
||
if launch_data.get("is_fair_launch", False):
|
||
vibe_score += 5
|
||
vibes.append("Fair launch confirmed")
|
||
|
||
vibe_score = max(0, min(100, vibe_score))
|
||
|
||
if vibe_score >= 75:
|
||
verdict = "VIBE CHECK PASSED - organic momentum"
|
||
elif vibe_score >= 50:
|
||
verdict = "MIXED VIBES - monitor closely"
|
||
elif vibe_score >= 30:
|
||
verdict = "WEAK VIBES - high risk of dump"
|
||
else:
|
||
verdict = "RUG VIBES - likely artificial/scam"
|
||
|
||
result = {
|
||
"tool": "Meme Vibe Score",
|
||
"token": req.token,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"summary": {
|
||
"vibe_score": vibe_score,
|
||
"verdict": verdict,
|
||
"vibes": vibes,
|
||
},
|
||
"sub_reports": {
|
||
"sentiment": sentiment_data,
|
||
"social_signals": social_data,
|
||
"launch_analysis": launch_data,
|
||
},
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
await record_x402_payment("meme_vibe_score", "0.01", req.token)
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Meme vibe score failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ── Bundle Pricing Endpoints ─────────────────────────────────────
|
||
# Bundles aggregate multiple tools at a discount vs individual calls.
|
||
# Each bundle runs its component tools in parallel and returns a unified result.
|
||
|
||
|
||
class BundleRequest(BaseModel):
|
||
address: str = ""
|
||
token: str = ""
|
||
wallet: str = ""
|
||
chain: str = "base"
|
||
url: str = ""
|
||
|
||
|
||
BUNDLES = {
|
||
"security_pack": {
|
||
"name": "Security Pack",
|
||
"description": "Complete pre-trade security check — rug scan, contract audit, URL safety, and honeypot detection.",
|
||
"tools": ["rugshield", "audit", "urlcheck", "honeypot_check"],
|
||
"individual_total": 0.13, # 0.02 + 0.05 + 0.01 + 0.05
|
||
"bundle_price_usd": 0.10, # 23% discount
|
||
"bundle_price_atoms": "100000",
|
||
"category": "bundle",
|
||
"trial_free": 1,
|
||
},
|
||
"intelligence_pack": {
|
||
"name": "Intelligence Pack",
|
||
"description": "Whale tracking suite — decode wallets, follow smart money, detect clusters and insider patterns.",
|
||
"tools": ["whale", "smartmoney", "cluster", "insider"],
|
||
"individual_total": 0.35, # 0.15 + 0.05 + 0.05 + 0.10
|
||
"bundle_price_usd": 0.25, # 29% discount
|
||
"bundle_price_atoms": "250000",
|
||
"category": "bundle",
|
||
"trial_free": 1,
|
||
},
|
||
"all_in_one": {
|
||
"name": "All-in-One Audit",
|
||
"description": "Maximum intelligence — comprehensive audit + smart money alpha + meme vibe score in one call.",
|
||
"tools": ["comprehensive_audit", "smart_money_alpha", "meme_vibe_score"],
|
||
"individual_total": 0.50, # 0.15 + 0.25 + 0.10
|
||
"bundle_price_usd": 0.35, # 30% discount
|
||
"bundle_price_atoms": "350000",
|
||
"category": "bundle",
|
||
"trial_free": 1,
|
||
},
|
||
"forensic_pack": {
|
||
"name": "Forensic Investigation Pack",
|
||
"description": "Complete forensic analysis — valuation, OSINT identity hunt, and investigation report at 33% discount.",
|
||
"tools": ["forensic_valuation", "osint_identity_hunt", "investigation_report"],
|
||
"individual_total": 0.60, # 0.25 + 0.15 + 0.20
|
||
"bundle_price_usd": 0.40, # 33% discount
|
||
"bundle_price_atoms": "400000",
|
||
"category": "bundle",
|
||
"trial_free": 1,
|
||
},
|
||
}
|
||
|
||
|
||
@router.get("/bundles")
|
||
async def list_bundles():
|
||
"""List all available tool bundles with pricing and savings."""
|
||
return {
|
||
"bundles": {
|
||
bid: {
|
||
"name": b["name"],
|
||
"description": b["description"],
|
||
"tools": b["tools"],
|
||
"individual_total": f"${b['individual_total']:.2f}",
|
||
"bundle_price": f"${b['bundle_price_usd']:.2f}",
|
||
"savings": f"{int((1 - b['bundle_price_usd'] / b['individual_total']) * 100)}%",
|
||
"trial_free": b["trial_free"],
|
||
}
|
||
for bid, b in BUNDLES.items()
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/bundles/security_pack")
|
||
async def bundle_security_pack(req: BundleRequest):
|
||
"""Security Pack: rugshield + audit + urlcheck + honeypot_check at 23% discount."""
|
||
target = req.address or req.token or req.url
|
||
if not target:
|
||
raise HTTPException(status_code=400, detail="Provide address, token, or url")
|
||
|
||
tasks = {
|
||
"rugshield": "http://localhost:8000/api/v1/x402-tools/rugshield",
|
||
"audit": "http://localhost:8000/api/v1/x402-tools/audit",
|
||
"urlcheck": "http://localhost:8000/api/v1/x402-tools/urlcheck",
|
||
"honeypot_check": "http://localhost:8000/api/v1/x402-tools/honeypot_check",
|
||
}
|
||
|
||
results = {}
|
||
async with aiohttp.ClientSession() as session:
|
||
coros = {}
|
||
for name, url in tasks.items():
|
||
body = {"address": target, "token_address": target, "url": target, "chain": req.chain}
|
||
coros[name] = session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=30))
|
||
for name, coro in coros.items():
|
||
try:
|
||
resp = await coro
|
||
results[name] = await resp.json() if resp.status == 200 else {"status": resp.status}
|
||
except Exception as e:
|
||
results[name] = {"error": str(e)}
|
||
|
||
verdict = "SAFE"
|
||
risk_scores = []
|
||
for r in results.values():
|
||
if isinstance(r, dict):
|
||
rs = r.get("risk_score")
|
||
if rs is not None:
|
||
risk_scores.append(rs)
|
||
if r.get("risk_level") in ("CRITICAL", "HIGH"):
|
||
verdict = "CAUTION"
|
||
|
||
avg_risk = sum(risk_scores) / len(risk_scores) if risk_scores else 0
|
||
if avg_risk > 60:
|
||
verdict = "HIGH_RISK"
|
||
elif avg_risk > 40:
|
||
verdict = "CAUTION"
|
||
|
||
return {
|
||
"tool": "Security Pack",
|
||
"bundle": "security_pack",
|
||
"target": target,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"results": results,
|
||
"summary": {
|
||
"verdict": verdict,
|
||
"avg_risk_score": round(avg_risk, 1),
|
||
"tools_checked": len(results),
|
||
},
|
||
"price_usd": "0.10",
|
||
"savings": "23% vs individual calls",
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
|
||
@router.post("/bundles/intelligence_pack")
|
||
async def bundle_intelligence_pack(req: BundleRequest):
|
||
"""Intelligence Pack: whale + smartmoney + cluster + insider at 29% discount."""
|
||
wallet = req.wallet or req.address
|
||
if not wallet:
|
||
raise HTTPException(status_code=400, detail="Provide wallet or address")
|
||
|
||
tasks = {
|
||
"whale": "http://localhost:8000/api/v1/x402-tools/whale",
|
||
"smartmoney": "http://localhost:8000/api/v1/x402-tools/smartmoney",
|
||
"cluster": "http://localhost:8000/api/v1/x402-tools/cluster",
|
||
"insider": "http://localhost:8000/api/v1/x402-tools/insider",
|
||
}
|
||
|
||
results = {}
|
||
async with aiohttp.ClientSession() as session:
|
||
coros = {}
|
||
for name, url in tasks.items():
|
||
body = {"address": wallet, "wallet_address": wallet, "chain": req.chain}
|
||
coros[name] = session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=30))
|
||
for name, coro in coros.items():
|
||
try:
|
||
resp = await coro
|
||
results[name] = await resp.json() if resp.status == 200 else {"status": resp.status}
|
||
except Exception as e:
|
||
results[name] = {"error": str(e)}
|
||
|
||
return {
|
||
"tool": "Intelligence Pack",
|
||
"bundle": "intelligence_pack",
|
||
"wallet": wallet,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"results": results,
|
||
"price_usd": "0.25",
|
||
"savings": "29% vs individual calls",
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
|
||
@router.post("/bundles/all_in_one")
|
||
async def bundle_all_in_one(req: BundleRequest):
|
||
"""All-in-One: comprehensive_audit + smart_money_alpha + meme_vibe_score at 30% discount."""
|
||
target = req.address or req.token or req.wallet
|
||
if not target:
|
||
raise HTTPException(status_code=400, detail="Provide address, token, or wallet")
|
||
|
||
tasks = {
|
||
"comprehensive_audit": "http://localhost:8000/api/v1/x402-tools/comprehensive_audit",
|
||
"smart_money_alpha": "http://localhost:8000/api/v1/x402-tools/smart_money_alpha",
|
||
"meme_vibe_score": "http://localhost:8000/api/v1/x402-tools/meme_vibe_score",
|
||
}
|
||
|
||
results = {}
|
||
async with aiohttp.ClientSession() as session:
|
||
coros = {}
|
||
for name, url in tasks.items():
|
||
body = {"address": target, "token": target, "wallet": target, "chain": req.chain}
|
||
coros[name] = session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=30))
|
||
for name, coro in coros.items():
|
||
try:
|
||
resp = await coro
|
||
results[name] = await resp.json() if resp.status == 200 else {"status": resp.status}
|
||
except Exception as e:
|
||
results[name] = {"error": str(e)}
|
||
|
||
return {
|
||
"tool": "All-in-One Audit",
|
||
"bundle": "all_in_one",
|
||
"target": target,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"results": results,
|
||
"price_usd": "0.35",
|
||
"savings": "30% vs individual calls",
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
|
||
@router.post("/forensic_pack")
|
||
async def bundle_forensic_pack(req: BundleRequest):
|
||
"""Forensic Investigation Pack — valuation + OSINT + report at 33% discount."""
|
||
target = req.address or req.token or req.wallet
|
||
if not target:
|
||
raise HTTPException(status_code=400, detail="Provide address, token, or wallet")
|
||
|
||
tasks = {
|
||
"forensic_valuation": "http://localhost:8000/api/v1/x402-tools/forensic_valuation",
|
||
"osint_identity_hunt": "http://localhost:8000/api/v1/x402-tools/osint_identity_hunt",
|
||
"investigation_report": "http://localhost:8000/api/v1/x402-tools/investigation_report",
|
||
}
|
||
|
||
results = {}
|
||
async with aiohttp.ClientSession() as session:
|
||
coros = {}
|
||
for name, url in tasks.items():
|
||
body = {"address": target, "token": target, "wallet": target, "chain": req.chain}
|
||
coros[name] = session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=30))
|
||
for name, coro in coros.items():
|
||
try:
|
||
resp = await coro
|
||
results[name] = await resp.json() if resp.status == 200 else {"status": resp.status}
|
||
except Exception as e:
|
||
results[name] = {"error": str(e)}
|
||
|
||
return {
|
||
"tool": "Forensic Investigation Pack",
|
||
"bundle": "forensic_pack",
|
||
"target": target,
|
||
"chain": req.chain,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"results": results,
|
||
"price_usd": "0.40",
|
||
"savings": "33% vs individual calls",
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
|
||
|
||
@router.get("/frameworks")
|
||
async def framework_discovery():
|
||
"""Returns all available framework integrations with endpoints.
|
||
This is the master discovery endpoint for AI frameworks."""
|
||
base = "https://mcp.rugmunch.io/api/v1/x402-tools"
|
||
return {
|
||
"service": "Rug Munch Intelligence",
|
||
"tagline": "We build tools to keep the crypto space safer",
|
||
"followers_x": "67,000+",
|
||
"telegram_users": "7,000+",
|
||
"networks": ["base", "solana"],
|
||
"protocol": "x402",
|
||
"frameworks": {
|
||
"openai": {
|
||
"endpoint": f"{base}/openai-tools",
|
||
"format": "OpenAI function calling",
|
||
"usage": "client.chat.completions.create(tools=...)",
|
||
"models": ["gpt-4o", "gpt-4o-mini", "o3-mini"],
|
||
"free": True,
|
||
},
|
||
"anthropic": {
|
||
"endpoint": f"{base}/anthropic-tools",
|
||
"format": "Claude API tool use",
|
||
"usage": "client.messages.create(tools=...)",
|
||
"models": ["claude-sonnet-4", "claude-opus-4", "claude-haiku"],
|
||
"free": True,
|
||
},
|
||
"gemini": {
|
||
"endpoint": f"{base}/gemini-tools",
|
||
"format": "Google Gemini function calling",
|
||
"usage": "model.generate_content(tools=...)",
|
||
"models": ["gemini-2.0-flash", "gemini-2.5-pro"],
|
||
"free": True,
|
||
},
|
||
"langchain": {
|
||
"endpoint": f"{base}/langchain-tools",
|
||
"format": "LangChain tool definitions",
|
||
"usage": "create_react_agent or LangGraph",
|
||
"ecosystem": "langchain, langgraph, crewai, autogen",
|
||
"free": True,
|
||
},
|
||
"mcp": {
|
||
"endpoint": "python -m app.mcp.x402_mcp_server",
|
||
"format": "Model Context Protocol (stdio)",
|
||
"usage": "Claude Desktop, Claude Code, any MCP client",
|
||
"free": True,
|
||
},
|
||
"rest_api": {
|
||
"endpoint": f"{base}/{{tool_name}}",
|
||
"format": "HTTP POST/GET with JSON",
|
||
"usage": "Any language, any framework",
|
||
"payment": "x402 (USDC on Base/Solana)",
|
||
},
|
||
},
|
||
"x402_gateways": {
|
||
"base": "https://x402.rugmunch.io/tools/{tool}",
|
||
"solana": "https://x402-sol.rugmunch.io/tools/{tool}",
|
||
"payment": "USDC via x402 protocol",
|
||
},
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# TOOL 41: Whale Accumulation Pattern Detector ($0.10)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/whale_accumulation")
|
||
async def whale_accumulation(req: GenericRequest):
|
||
"""Detect stealth accumulation by large holders before price impact.
|
||
Identifies quiet buying patterns, OTC accumulation signals, and wallet
|
||
funding sequences that precede major positions.
|
||
"""
|
||
try:
|
||
from app.whale_accumulation import detect_accumulation
|
||
|
||
address = req.address or req.token or req.query or ""
|
||
if not address:
|
||
raise HTTPException(status_code=400, detail="Provide token address")
|
||
|
||
result = await detect_accumulation(address, req.chain)
|
||
await record_x402_payment("whale_accumulation", "0.10", address)
|
||
return {
|
||
"tool": "Whale Accumulation Detector",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Whale accumulation failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# MCP Proxy — handles external MCP tool execution from workers
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
|
||
class MCPProxyRequest(BaseModel):
|
||
service: str
|
||
tool: str
|
||
arguments: dict[str, Any] = {}
|
||
|
||
|
||
@router.post("/mcp-proxy")
|
||
async def mcp_proxy(req: MCPProxyRequest):
|
||
"""Proxy MCP tool calls from Cloudflare Workers to external APIs.
|
||
Maps service_tool to the appropriate external API and executes."""
|
||
svc = req.service.lower()
|
||
tool = req.tool
|
||
args = req.arguments
|
||
|
||
# Service routing table — maps to known external APIs
|
||
routes = {
|
||
"dexscreener": "https://api.dexscreener.com",
|
||
"jupiter": "https://quote-api.jup.ag/v6",
|
||
"pumpfun": "https://frontend-api.pump.fun",
|
||
"raydium": "https://api.raydium.io/v2",
|
||
"defillama": "https://api.llama.fi",
|
||
"dexpaprika": "https://api.dexpaprika.com",
|
||
"coincap": "https://api.coincap.io/v2",
|
||
"coinmarketcap": "https://pro-api.coinmarketcap.com/v1",
|
||
"cryptopanic": "https://cryptopanic.com/api/v1",
|
||
"cryptocompare": "https://min-api.cryptocompare.com/data",
|
||
"blockchair": "https://api.blockchair.com",
|
||
"blockchain": "https://blockchain.info",
|
||
"mempool": "https://mempool.space/api",
|
||
"solana": "https://api.mainnet-beta.solana.com",
|
||
"helius": "https://api.helius.xyz/v0",
|
||
"birdeye": "https://public-api.birdeye.com",
|
||
"coingecko": "https://api.coingecko.com/api/v3",
|
||
"cryptoiz": "https://api.cryptoiz.com",
|
||
"blockrun": "https://api.blockrun.ai",
|
||
"agentfi": "https://api.agentfi.xyz",
|
||
"moralis": "https://deep-index.moralis.io/api/v2.2",
|
||
"gmgn": "https://gmgn.ai/api",
|
||
"nansen": "https://api.nansen.ai",
|
||
"arkham": "https://api.arkhamintelligence.com",
|
||
"dune": "https://api.dune.com/api/v1",
|
||
"solscan": "https://public-api.solscan.io",
|
||
"quicknode": "https://api.quicknode.com",
|
||
}
|
||
|
||
base_url = routes.get(svc)
|
||
if not base_url:
|
||
return {"error": f"Unsupported service: {svc}", "available": list(routes.keys())}
|
||
|
||
# Construct the endpoint based on tool name
|
||
tool_endpoints = {
|
||
# DexScreener
|
||
"getLatestTokenProfiles": "/token-profiles/latest/v1",
|
||
"getLatestBoostedTokens": "/token-boosted/latest/v1",
|
||
"getPairs": f"/latest/dex/pairs/solana/{args.get('pairAddresses', args.get('tokenAddresses', ''))}",
|
||
# Jupiter
|
||
"getQuote": "/quote",
|
||
"getPrice": "/price",
|
||
"getTokens": "/tokens",
|
||
# CoinGecko
|
||
"getPrice": "/simple/price",
|
||
# DeFiLlama
|
||
"getTVL": f"/tvl/{args.get('protocol', '')}",
|
||
"getProtocols": "/protocols",
|
||
# Solana RPC
|
||
"getHealth": "",
|
||
# General fallback
|
||
}
|
||
|
||
endpoint = tool_endpoints.get(tool, f"/{tool}")
|
||
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
url = f"{base_url}{endpoint}"
|
||
headers = {"Accept": "application/json"}
|
||
|
||
# Add API keys for services that need them
|
||
if svc == "coingecko":
|
||
headers["x-cg-pro-api-key"] = os.getenv("COINGECKO_API_KEY_PRO", "")
|
||
elif svc == "helius":
|
||
headers["Authorization"] = f"Bearer {os.getenv('HELIUS_API_KEY', '')}"
|
||
elif svc == "moralis":
|
||
headers["X-API-Key"] = os.getenv("MORALIS_API_KEY", "")
|
||
elif svc == "birdeye":
|
||
headers["X-API-KEY"] = os.getenv("BIRDEYE_API_KEY", "")
|
||
|
||
async with session.get(
|
||
url, params=args, headers=headers, timeout=aiohttp.ClientTimeout(total=15)
|
||
) as resp:
|
||
data = await resp.json()
|
||
return {
|
||
"service": svc,
|
||
"tool": tool,
|
||
"status": "success" if resp.status < 400 else "error",
|
||
"data": data,
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"MCP proxy failed for {svc}/{tool}: {e}")
|
||
return {
|
||
"service": svc,
|
||
"tool": tool,
|
||
"status": "error",
|
||
"error": str(e),
|
||
"note": "Direct external API calls failed — try REST endpoint for cached/fallback data",
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# Human Payment — Multi-Chain Wallet Pay-Per-Call
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
# Supported payment tokens across all chains
|
||
HUMAN_PAYMENT_TOKENS = {
|
||
# Base chain
|
||
"USDC-BASE": {
|
||
"chain": "base",
|
||
"network": "eip155:8453",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on Base",
|
||
"icon": "💵",
|
||
},
|
||
# Solana chain
|
||
"USDC-SOL": {
|
||
"chain": "solana",
|
||
"network": "solana:mainnet",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on Solana",
|
||
"icon": "💵",
|
||
},
|
||
"SOL": {
|
||
"chain": "solana",
|
||
"network": "solana:mainnet",
|
||
"asset": "SOL",
|
||
"decimals": 9,
|
||
"label": "SOL native",
|
||
"icon": "◎",
|
||
},
|
||
# Ethereum chain
|
||
"USDC-ETH": {
|
||
"chain": "ethereum",
|
||
"network": "eip155:1",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on Ethereum",
|
||
"icon": "💵",
|
||
},
|
||
"USDT-ETH": {
|
||
"chain": "ethereum",
|
||
"network": "eip155:1",
|
||
"asset": "USDT",
|
||
"decimals": 6,
|
||
"label": "USDT on Ethereum",
|
||
"icon": "💲",
|
||
},
|
||
"ETH": {
|
||
"chain": "ethereum",
|
||
"network": "eip155:1",
|
||
"asset": "ETH",
|
||
"decimals": 18,
|
||
"label": "ETH native",
|
||
"icon": "⟠",
|
||
},
|
||
# BNB Chain
|
||
"USDC-BSC": {
|
||
"chain": "bsc",
|
||
"network": "eip155:56",
|
||
"asset": "USDC",
|
||
"decimals": 18,
|
||
"label": "USDC on BSC",
|
||
"icon": "💵",
|
||
},
|
||
"USDT-BSC": {
|
||
"chain": "bsc",
|
||
"network": "eip155:56",
|
||
"asset": "USDT",
|
||
"decimals": 18,
|
||
"label": "USDT on BSC",
|
||
"icon": "💲",
|
||
},
|
||
# Polygon
|
||
"USDC-POLY": {
|
||
"chain": "polygon",
|
||
"network": "eip155:137",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on Polygon",
|
||
"icon": "💵",
|
||
},
|
||
"POL": {
|
||
"chain": "polygon",
|
||
"network": "eip155:137",
|
||
"asset": "POL",
|
||
"decimals": 18,
|
||
"label": "POL native",
|
||
"icon": "🟣",
|
||
},
|
||
# Arbitrum
|
||
"USDC-ARB": {
|
||
"chain": "arbitrum",
|
||
"network": "eip155:42161",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on Arbitrum",
|
||
"icon": "💵",
|
||
},
|
||
# TRON
|
||
"USDT-TRON": {
|
||
"chain": "tron",
|
||
"network": "tron:mainnet",
|
||
"asset": "USDT",
|
||
"decimals": 6,
|
||
"label": "USDT on TRON",
|
||
"icon": "💲",
|
||
},
|
||
"USDC-TRON": {
|
||
"chain": "tron",
|
||
"network": "tron:mainnet",
|
||
"asset": "USDC",
|
||
"decimals": 6,
|
||
"label": "USDC on TRON",
|
||
"icon": "💵",
|
||
},
|
||
# Bitcoin
|
||
"BTC": {
|
||
"chain": "bitcoin",
|
||
"network": "bitcoin:mainnet",
|
||
"asset": "BTC",
|
||
"decimals": 8,
|
||
"label": "Bitcoin",
|
||
"icon": "₿",
|
||
},
|
||
# Fiat
|
||
"EUR-SEPA": {
|
||
"chain": "sepa",
|
||
"network": "sepa:eur",
|
||
"asset": "EUR",
|
||
"decimals": 2,
|
||
"label": "EUR via SEPA",
|
||
"icon": "€",
|
||
},
|
||
}
|
||
|
||
|
||
# Pay-to addresses — pulled dynamically from WalletManagerV2
|
||
# Falls back to env vars if wallet manager unavailable
|
||
def _resolve_pay_to(chain: str) -> str:
|
||
"""Get active x402 payment address from WalletManagerV2."""
|
||
try:
|
||
from app.wallet_manager_v2 import get_wallet_manager_v2
|
||
|
||
mgr = get_wallet_manager_v2(os.getenv("WALLET_VAULT_PASSWORD", ""))
|
||
# Map chain key to wallet manager chain key
|
||
chain_map = {
|
||
"base": "eth",
|
||
"ethereum": "eth",
|
||
"bsc": "eth",
|
||
"polygon": "eth",
|
||
"arbitrum": "eth",
|
||
"optimism": "eth",
|
||
"avalanche": "eth",
|
||
"fantom": "eth",
|
||
"gnosis": "eth",
|
||
"solana": "sol",
|
||
"tron": "trx",
|
||
"bitcoin": "btc",
|
||
}
|
||
wm_chain = chain_map.get(chain, chain)
|
||
for w in mgr._wallets.values():
|
||
if w.chain == wm_chain and w.x402_enabled and w.status == "active":
|
||
return w.address
|
||
except Exception:
|
||
pass
|
||
# Fallback: env vars
|
||
fallbacks = {
|
||
"base": "X402_EVM_PAY_TO",
|
||
"ethereum": "X402_EVM_PAY_TO",
|
||
"bsc": "X402_EVM_PAY_TO",
|
||
"polygon": "X402_EVM_PAY_TO",
|
||
"arbitrum": "X402_EVM_PAY_TO",
|
||
"solana": "X402_SOL_PAY_TO",
|
||
"tron": "X402_TRON_PAY_TO",
|
||
"bitcoin": "X402_BTC_PAY_TO",
|
||
"sepa": "ASTERPAY_SEPA_IBAN",
|
||
}
|
||
env_key = fallbacks.get(chain)
|
||
if env_key:
|
||
return os.getenv(env_key, "")
|
||
return ""
|
||
|
||
|
||
HUMAN_PAY_TO = {
|
||
"base": _resolve_pay_to("base"),
|
||
"ethereum": _resolve_pay_to("ethereum"),
|
||
"bsc": _resolve_pay_to("bsc"),
|
||
"polygon": _resolve_pay_to("polygon"),
|
||
"arbitrum": _resolve_pay_to("arbitrum"),
|
||
"solana": _resolve_pay_to("solana"),
|
||
"tron": _resolve_pay_to("tron"),
|
||
"bitcoin": _resolve_pay_to("bitcoin"),
|
||
"sepa": os.getenv("ASTERPAY_SEPA_IBAN", ""),
|
||
}
|
||
|
||
|
||
class HumanPaymentRequest(BaseModel):
|
||
tool: str = Field(..., description="Tool ID to execute")
|
||
arguments: dict[str, Any] = Field(default_factory=dict, description="Tool parameters")
|
||
payment_token: str = Field(
|
||
..., description=f"Payment token key: {', '.join(HUMAN_PAYMENT_TOKENS.keys())}"
|
||
)
|
||
tx_hash: str = Field(..., description="Transaction hash on-chain")
|
||
wallet: str = Field(..., description="Payer wallet address")
|
||
chain: str | None = Field(
|
||
default=None, description="Blockchain (auto-detected from payment_token if not set)"
|
||
)
|
||
|
||
|
||
class HumanPaymentMethodsResponse(BaseModel):
|
||
"""Response listing all payment methods available to humans."""
|
||
|
||
tokens: list[dict[str, Any]]
|
||
pay_to_addresses: dict[str, str]
|
||
chain_count: int
|
||
token_count: int
|
||
|
||
|
||
@router.get("/payment-methods", response_model=HumanPaymentMethodsResponse)
|
||
async def get_payment_methods():
|
||
"""List all payment methods available for human wallet payments.
|
||
|
||
Returns every supported token, chain, and the destination wallet address.
|
||
Prices shown in USD; actual payment is in the selected token at market rate.
|
||
"""
|
||
return {
|
||
"tokens": [
|
||
{
|
||
"key": key,
|
||
"chain": info["chain"],
|
||
"network": info["network"],
|
||
"asset": info["asset"],
|
||
"label": info["label"],
|
||
"icon": info["icon"],
|
||
"pay_to": HUMAN_PAY_TO.get(info["chain"], ""),
|
||
"decimals": info["decimals"],
|
||
}
|
||
for key, info in HUMAN_PAYMENT_TOKENS.items()
|
||
],
|
||
"pay_to_addresses": {chain: addr for chain, addr in HUMAN_PAY_TO.items() if addr},
|
||
"chain_count": len({i["chain"] for i in HUMAN_PAYMENT_TOKENS.values()}),
|
||
"token_count": len(HUMAN_PAYMENT_TOKENS),
|
||
}
|
||
|
||
|
||
@router.post("/human-execute")
|
||
async def human_execute(req: HumanPaymentRequest):
|
||
"""Execute a tool after human wallet payment verification.
|
||
|
||
Multi-chain: Base, Solana, Ethereum, BSC, Polygon, Arbitrum, TRON, Bitcoin, SEPA/EUR.
|
||
Verification routed through the same facilitator system as bots.
|
||
All payments go to your wallets — no middleman.
|
||
"""
|
||
tool_name = req.tool
|
||
token_info = HUMAN_PAYMENT_TOKENS.get(req.payment_token)
|
||
|
||
if not token_info:
|
||
return {
|
||
"success": False,
|
||
"error": f"Unsupported payment token: {req.payment_token}",
|
||
"supported_tokens": list(HUMAN_PAYMENT_TOKENS.keys()),
|
||
}
|
||
|
||
chain = req.chain or token_info["chain"]
|
||
expected_pay_to = HUMAN_PAY_TO.get(chain, "")
|
||
|
||
# ── Payment Verification ──────────────────────────────────
|
||
verified = False
|
||
verification_method = "unknown"
|
||
facilitator_used = None
|
||
|
||
try:
|
||
# Try facilitator router first (same as bot payments)
|
||
from app.facilitators.router import get_facilitator_router
|
||
|
||
router = get_facilitator_router()
|
||
|
||
# Build a minimal payload that the router can work with
|
||
payload = {
|
||
"x402Version": 2,
|
||
"txHash": req.tx_hash,
|
||
"payer": req.wallet,
|
||
"accepted": {
|
||
"network": token_info["network"],
|
||
"asset": token_info["asset"],
|
||
"amount": "0", # Will be verified by facilitator
|
||
"payTo": expected_pay_to,
|
||
},
|
||
}
|
||
|
||
result = await router.verify(
|
||
payload=payload,
|
||
chain_key=chain,
|
||
token_symbol=token_info["asset"],
|
||
)
|
||
|
||
if result.verified:
|
||
verified = True
|
||
verification_method = f"facilitator:{result.facilitator}"
|
||
facilitator_used = result.facilitator
|
||
logger.info(
|
||
f"Human payment verified via {facilitator_used}: {req.tx_hash[:16]}... on {chain}"
|
||
)
|
||
|
||
except ImportError:
|
||
logger.debug(
|
||
"Facilitator router not available for human payment — falling back to on-chain check"
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"Facilitator verify failed for human payment, trying on-chain: {e}")
|
||
|
||
# Fallback: direct on-chain verification
|
||
if not verified:
|
||
try:
|
||
verified = await _verify_onchain_direct(req.tx_hash, chain, token_info, expected_pay_to)
|
||
verification_method = "onchain-direct"
|
||
except Exception as e:
|
||
logger.error(f"On-chain verification failed: {e}")
|
||
|
||
if not verified:
|
||
return {
|
||
"success": False,
|
||
"error": "Payment verification failed. Transaction not confirmed or wrong recipient.",
|
||
"tx_hash": req.tx_hash,
|
||
"chain": chain,
|
||
"expected_pay_to": expected_pay_to[:10] + "...",
|
||
}
|
||
|
||
# ── Anti-abuse: check trial limits ────────────────────────
|
||
from app.routers.x402_enforcement import check_trial
|
||
|
||
_can_trial, _remaining = check_trial(tool_name, req.wallet)
|
||
|
||
# ── Execute the tool ──────────────────────────────────────
|
||
try:
|
||
# Determine gateway
|
||
if chain in ("base", "ethereum", "bsc", "polygon", "arbitrum"):
|
||
gw = "https://base.rugmunch.io"
|
||
elif chain == "tron":
|
||
gw = "https://base.rugmunch.io" # TRON tools proxied through base gateway
|
||
else:
|
||
gw = "https://sol.rugmunch.io"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(
|
||
f"{gw}/tools/{tool_name}",
|
||
json=req.arguments,
|
||
headers={"Content-Type": "application/json", "X-RMI-Human-Payment": "verified"},
|
||
timeout=aiohttp.ClientTimeout(total=30),
|
||
) as resp,
|
||
):
|
||
text = await resp.text()
|
||
try:
|
||
result_data = json.loads(text)
|
||
except json.JSONDecodeError:
|
||
result_data = {"raw": text[:500]}
|
||
|
||
# Record payment in Redis (same as bot payments)
|
||
try:
|
||
from app.routers.x402_enforcement import get_redis
|
||
|
||
r = get_redis()
|
||
if r:
|
||
import time as _time
|
||
|
||
r.setex(
|
||
f"x402:spent_tx:{req.tx_hash}",
|
||
86400,
|
||
json.dumps(
|
||
{
|
||
"chain": chain,
|
||
"payer": req.wallet,
|
||
"amount": "0",
|
||
"tool": tool_name,
|
||
"timestamp": _time.time(),
|
||
"method": "human-wallet",
|
||
"facilitator": facilitator_used,
|
||
}
|
||
),
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return {
|
||
"success": resp.status < 400,
|
||
"tool": tool_name,
|
||
"payment_token": req.payment_token,
|
||
"chain": chain,
|
||
"tx_hash": req.tx_hash,
|
||
"verified": True,
|
||
"verification": verification_method,
|
||
"facilitator": facilitator_used,
|
||
"pay_to": expected_pay_to,
|
||
"result": result_data,
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Tool execution failed: {e}")
|
||
return {"success": False, "error": f"Tool execution failed: {e!s}"}
|
||
|
||
|
||
async def _verify_onchain_direct(
|
||
tx_hash: str, chain: str, token_info: dict, expected_pay_to: str
|
||
) -> bool:
|
||
"""Direct on-chain verification fallback for human payments."""
|
||
import aiohttp
|
||
|
||
async with aiohttp.ClientSession() as session:
|
||
if chain == "solana":
|
||
async with session.post(
|
||
"https://api.mainnet-beta.solana.com",
|
||
json={
|
||
"jsonrpc": "2.0",
|
||
"id": 1,
|
||
"method": "getTransaction",
|
||
"params": [
|
||
tx_hash,
|
||
{"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0},
|
||
],
|
||
},
|
||
timeout=aiohttp.ClientTimeout(total=15),
|
||
) as resp:
|
||
data = await resp.json()
|
||
tx = data.get("result", {})
|
||
if not tx:
|
||
return False
|
||
# Check if any transfer goes to our wallet
|
||
meta = tx.get("meta", {})
|
||
return any(bal.get("owner") == expected_pay_to for bal in meta.get("postTokenBalances", []))
|
||
|
||
elif chain in ("tron", "bitcoin", "sepa"):
|
||
# For these chains, assume verified if facilitator passed
|
||
# (they don't have simple Etherscan-style APIs)
|
||
return True
|
||
|
||
else:
|
||
# EVM chains — use Etherscan-family APIs
|
||
explorers = {
|
||
"base": "https://api.basescan.org/api",
|
||
"ethereum": "https://api.etherscan.io/api",
|
||
"bsc": "https://api.bscscan.com/api",
|
||
"polygon": "https://api.polygonscan.com/api",
|
||
"arbitrum": "https://api.arbiscan.io/api",
|
||
}
|
||
explorer_url = explorers.get(chain, explorers["ethereum"])
|
||
api_key = os.getenv("ETHERSCAN_API_KEY", "")
|
||
|
||
async with session.get(
|
||
f"{explorer_url}?module=transaction&action=gettxreceiptstatus&txhash={tx_hash}&apikey={api_key}",
|
||
timeout=aiohttp.ClientTimeout(total=10),
|
||
) as resp:
|
||
data = await resp.json()
|
||
result = data.get("result", {})
|
||
if isinstance(result, dict):
|
||
return result.get("status") == "1"
|
||
return str(data.get("status")) == "1"
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# SENTINEL SCANNER — Deep Multi-Module Token Security
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
class SentinelScanRequest(BaseModel):
|
||
"""Full 9-module SENTINEL deep scan."""
|
||
|
||
address: str
|
||
chain: str = "solana"
|
||
dev_address: str | None = None
|
||
|
||
|
||
class SentinelModuleRequest(BaseModel):
|
||
"""Single SENTINEL module request."""
|
||
|
||
address: str
|
||
chain: str = "solana"
|
||
dev_address: str | None = None
|
||
|
||
|
||
@router.post("/sentinel_scan")
|
||
async def sentinel_full_scan(req: SentinelScanRequest):
|
||
"""Full SENTINEL deep scan — all 9 modules in parallel with graceful degradation.
|
||
|
||
Pricing: $0.15 — the most comprehensive token security scan available.
|
||
Returns composite risk score (0-100), per-module breakdown, and aggregated red flags.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import dataclass_to_dict, run_sentinel_scan
|
||
|
||
report = await run_sentinel_scan(
|
||
token_address=req.address,
|
||
chain=req.chain,
|
||
dev_address=req.dev_address,
|
||
)
|
||
result = dataclass_to_dict(report)
|
||
|
||
await record_x402_payment("sentinel_scan", "0.15", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Full Deep Scan",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"SENTINEL scan failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/holder_analysis")
|
||
async def holder_analysis_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Holder Analysis — HHI concentration, fake diversification detection.
|
||
|
||
Pricing: $0.05
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_holder_analysis
|
||
|
||
result = await run_holder_analysis(req.address, req.chain)
|
||
await record_x402_payment("holder_analysis", "0.05", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Holder Analysis",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Holder analysis failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/bundle_detect")
|
||
async def bundle_detect_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Bundle Detection — enhanced bundle/sniper detection, funding chain analysis.
|
||
|
||
Pricing: $0.08
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_bundle_detection
|
||
|
||
result = await run_bundle_detection(req.address, req.chain)
|
||
await record_x402_payment("bundle_detect", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Bundle Detection",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Bundle detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/exchange_fund_check")
|
||
async def exchange_fund_check_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Exchange Funder Check — CEX-funded wallet detection for token buyers.
|
||
|
||
Pricing: $0.05
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_exchange_funding
|
||
|
||
result = await run_exchange_funding(req.address, req.chain)
|
||
await record_x402_payment("exchange_fund_check", "0.05", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Exchange Funder Check",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Exchange fund check failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/liquidity_verify")
|
||
async def liquidity_verify_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Liquidity Verification — lock verification, fake locker detection, expiry monitoring.
|
||
|
||
Pricing: $0.05
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_liquidity_verification
|
||
|
||
result = await run_liquidity_verification(req.address, req.chain)
|
||
await record_x402_payment("liquidity_verify", "0.05", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Liquidity Verification",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Liquidity verification failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/dev_reputation")
|
||
async def dev_reputation_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Dev Reputation — serial rugg detection, cross-chain dev tracking.
|
||
|
||
Pricing: $0.08
|
||
Requires dev_address (deployer/creator wallet). Falls back to address if dev_address not provided.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_dev_reputation
|
||
|
||
dev_wallet = req.dev_address or req.address
|
||
result = await run_dev_reputation(dev_wallet, chains=[req.chain])
|
||
await record_x402_payment("dev_reputation", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Dev Reputation",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": dev_wallet,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Dev reputation failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/wash_trading")
|
||
async def wash_trading_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Wash Trading Detection — circular transfer detection, cross-DEX loop analysis.
|
||
|
||
Pricing: $0.08
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_wash_trading
|
||
|
||
result = await run_wash_trading(req.address, req.chain)
|
||
await record_x402_payment("wash_trading", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Wash Trading Detection",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Wash trading detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/social_engineering")
|
||
async def social_engineering_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Social Engineering & Identity Fraud Detection.
|
||
|
||
Detects fake teams, AI-generated profiles, phishing domains,
|
||
copied whitepapers, and social engineering campaigns.
|
||
|
||
Pricing: $0.15
|
||
"""
|
||
try:
|
||
from app.social_engineering_detector import detect_social_engineering
|
||
|
||
result = await detect_social_engineering(
|
||
token_address=req.address,
|
||
chain=req.chain,
|
||
team_members=req.team_members if hasattr(req, "team_members") else None,
|
||
social_links=req.social_links if hasattr(req, "social_links") else None,
|
||
domain=req.domain if hasattr(req, "domain") else None,
|
||
whitepaper_text=req.whitepaper if hasattr(req, "whitepaper") else None,
|
||
)
|
||
await record_x402_payment("social_engineering", "0.15", req.address)
|
||
|
||
return {
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Social engineering detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/metadata_fingerprint")
|
||
async def metadata_fingerprint_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Metadata Fingerprint — HTML structure hashing, description similarity, social overlap detection.
|
||
|
||
Pricing: $0.05
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_metadata_fingerprint
|
||
|
||
result = await run_metadata_fingerprint(req.address, req.chain)
|
||
await record_x402_payment("metadata_fingerprint", "0.05", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Metadata Fingerprint",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Metadata fingerprint failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/sentiment_check")
|
||
async def sentiment_check_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Sentiment Check — social sentiment scoring, bot campaign detection, pump probability.
|
||
|
||
Pricing: $0.05
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_sentiment
|
||
|
||
result = await run_sentiment(req.address, req.chain)
|
||
await record_x402_payment("sentiment_check", "0.05", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Sentiment Check",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Sentiment check failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/pumpfun_analysis")
|
||
async def pumpfun_analysis_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL PumpFun Analysis — bonding curve progress, bot detection, graduation probability (Solana only).
|
||
|
||
Pricing: $0.08
|
||
Only works for Solana tokens. Returns error for other chains.
|
||
"""
|
||
try:
|
||
if req.chain.lower() != "solana":
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="PumpFun analysis is only available for Solana tokens. "
|
||
f"Received chain={req.chain}. Use chain='solana'.",
|
||
)
|
||
|
||
from app.scanners.sentinel_pipeline import run_pumpfun_analysis
|
||
|
||
result = await run_pumpfun_analysis(req.address)
|
||
await record_x402_payment("pumpfun_analysis", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL PumpFun Analysis",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": "solana",
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"PumpFun analysis failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# SENTINEL TIER 2 — Advanced threat detection modules
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/flash_loan_detect")
|
||
async def flash_loan_detect_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Flash Loan Detection — borrow-then-dump patterns, single-block exploits.
|
||
|
||
Pricing: $0.08
|
||
Detects wallet activity that matches flash loan attack patterns:
|
||
large buy/sell in consecutive blocks, near-zero pre-trade balance,
|
||
volume exceeding pool liquidity ratio.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_flash_loan_detection
|
||
|
||
result = await run_flash_loan_detection(req.address, req.chain)
|
||
await record_x402_payment("flash_loan_detect", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Flash Loan Detection",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Flash loan detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/pump_dump_detect")
|
||
async def pump_dump_detect_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Pump-and-Dump Detection — volume spikes, coordinated buys, lifecycle stage.
|
||
|
||
Pricing: $0.08
|
||
Detects pump-and-dump patterns: sudden volume spikes (>5x average),
|
||
coordinated fresh-wallet buy clusters, price-volume divergence,
|
||
and rug pull lifecycle stage (deploy/pump/distribution/dump).
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_pump_dump_detection
|
||
|
||
result = await run_pump_dump_detection(req.address, req.chain)
|
||
await record_x402_payment("pump_dump_detect", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Pump-and-Dump Detection",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Pump-and-dump detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/oracle_manipulation")
|
||
async def oracle_manipulation_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Oracle Manipulation — oracle source analysis, price manipulation vulnerability.
|
||
|
||
Pricing: $0.08
|
||
Analyzes oracle risk: single-source dependency, pool depth vulnerability,
|
||
DEX-vs-oracle price deviation, and overall manipulable score.
|
||
Critical for lending/borrowing protocols that rely on price feeds.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_oracle_manipulation
|
||
|
||
result = await run_oracle_manipulation(req.address, req.chain)
|
||
await record_x402_payment("oracle_manipulation", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Oracle Manipulation",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Oracle manipulation detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/governance_attack")
|
||
async def governance_attack_endpoint(req: SentinelModuleRequest):
|
||
"""x402 Governance Attack — governance concentration, timelock/quorum risk detection.
|
||
|
||
Pricing: $0.08 (x402 tool #44)
|
||
Detects governance risks: top holder dominance (>50%), missing timelock,
|
||
low quorum thresholds enabling flash-loan governance attacks,
|
||
and admin key concentration on Solana programs.
|
||
|
||
Standalone module: app/governance_attack_detector.py
|
||
"""
|
||
try:
|
||
from app.governance_attack_detector import detect_governance_attack
|
||
|
||
result = await detect_governance_attack(req.address, req.chain)
|
||
await record_x402_payment("governance_attack", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "Governance Attack & Concentration Risk Detector",
|
||
"version": "1.0",
|
||
"pricing": "$0.08 per analysis, 1 free trial",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"token_address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Governance attack detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/proxy_detect")
|
||
async def proxy_detect_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Proxy Detection — proxy resolution, implementation fingerprinting, upgrade risk.
|
||
|
||
Pricing: $0.08
|
||
Resolves proxy contracts to their real implementation, checks upgrade authority
|
||
and timelock status, and fingerprints implementation bytecode against known
|
||
rug contract patterns. Essential for EVM tokens behind proxies.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_proxy_detection
|
||
|
||
result = await run_proxy_detection(req.address, req.chain)
|
||
await record_x402_payment("proxy_detect", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Proxy Detection",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Proxy detection failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# SENTINEL TIER 3 — Static analysis, decompilation, address labeling
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
|
||
@router.post("/static_analysis")
|
||
async def static_analysis_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Static Analysis — Slither vulnerability detection + Forta alert integration.
|
||
|
||
Pricing: $0.12
|
||
Runs Slither static analysis on verified (or Heimdall-decompiled) contracts
|
||
and queries Forta public alerts for the address. Returns vulnerability findings
|
||
and active threat alerts.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_static_analysis
|
||
|
||
result = await run_static_analysis(req.address, req.chain)
|
||
await record_x402_payment("static_analysis", "0.12", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Static Analysis",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Static analysis failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/decompiler_analysis")
|
||
async def decompiler_analysis_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Decompiler Analysis — Heimdall decompilation + whatsABI function extraction.
|
||
|
||
Pricing: $0.10
|
||
Decompiles unverified contract bytecode using Heimdall-rs, extracts function
|
||
selectors via PUSH4 scanning, and identifies dangerous rug-pull function signatures.
|
||
Essential for tokens with unverified source code.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_decompiler_analysis
|
||
|
||
result = await run_decompiler_analysis(req.address, req.chain)
|
||
await record_x402_payment("decompiler_analysis", "0.10", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Decompiler Analysis",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Decompiler analysis failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/address_labels")
|
||
async def address_labels_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Address Labels — multi-source wallet address labeling.
|
||
|
||
Pricing: $0.08
|
||
Resolves any address across Etherscan labels, RolodETH, walletLabels.xyz,
|
||
bluepages.fyi, and internal RAG. Returns unified labels (exchange, MEV bot,
|
||
known scammer, deployer, etc.).
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_address_labels
|
||
|
||
result = await run_address_labels(req.address, req.chain)
|
||
await record_x402_payment("address_labels", "0.08", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Address Labels",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Address labels failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# SENTINEL TIER 4 — Visualization & Contract Diff
|
||
@router.post("/fund_flow")
|
||
async def fund_flow_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Fund Flow Visualization — SVG fund flow graph for token analysis.
|
||
Pricing: $0.10
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_fund_flow
|
||
|
||
result = await run_fund_flow(req.address, req.chain)
|
||
await record_x402_payment("fund_flow", "0.10", req.address)
|
||
return {
|
||
"tool": "SENTINEL Fund Flow",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Fund flow visualization failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/contract_diff")
|
||
async def contract_diff_endpoint(req: SentinelModuleRequest):
|
||
"""SENTINEL Contract Diff — bytecode hash comparison against known rug contracts.
|
||
|
||
Pricing: $0.10
|
||
Compares a token's contract bytecode against a database of known rug contracts.
|
||
Detects clones, forks, and near-identical contracts by hashing function selectors,
|
||
bytecode sections, and metadata patterns. Identifies dangerous function signatures
|
||
(withdrawAll, drain, setOwner, emergencyWithdraw) and rug-specific bytecode patterns.
|
||
"""
|
||
try:
|
||
from app.scanners.sentinel_pipeline import run_contract_diff
|
||
|
||
result = await run_contract_diff(req.address, req.chain)
|
||
await record_x402_payment("contract_diff", "0.10", req.address)
|
||
|
||
return {
|
||
"tool": "SENTINEL Contract Diff",
|
||
"version": "1.0",
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"address": req.address,
|
||
"chain": req.chain,
|
||
**result,
|
||
"guarantee": "Data delivered or auto-refund via x402 receipt",
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Contract diff analysis failed: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ALIAS ROUTES — Map dead tool IDs to real handler endpoints
|
||
# These tools appear in TOOL_PRICES and x402 manifest but had no routes.
|
||
# Each alias proxies the request to the real implementation.
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
TOOL_ALIASES: dict[str, str] = {
|
||
# Security
|
||
"airdrop_check": "airdrop_finder",
|
||
"bundler_detect": "mev_protection",
|
||
"clone_detect": "contract_diff",
|
||
"deployer_history": "insider",
|
||
"fresh_pair": "launch",
|
||
"liquidity_migration": "rugshield",
|
||
"mev_alert": "mev_protection",
|
||
"profile_flip": "social_signal",
|
||
"protocol_risk": "chain_health",
|
||
"scam_database": "urlcheck",
|
||
"token_age": "audit",
|
||
"wash_trading": "nft_wash_detector",
|
||
# Intelligence
|
||
"alpha_digest": "smart_money_alpha",
|
||
"insider_network": "insider",
|
||
"kol_performance": "social_signal",
|
||
"listing_predictor": "market_overview",
|
||
"sniper_detect": "sniper_alert",
|
||
"syndicate_scan": "cluster",
|
||
"syndicate_track": "cluster",
|
||
"whale_accumulation": "whale",
|
||
"whale_profile": "whale",
|
||
"whale_scan": "whale",
|
||
"wallet_graph": "cluster",
|
||
# Market
|
||
"arbitrage_scan": "market_overview",
|
||
"liquidity_depth": "market_overview",
|
||
"unlock_calendar": "market_overview",
|
||
# Social
|
||
"sentiment_spike": "sentiment",
|
||
# Analysis
|
||
"portfolio_aggregate": "portfolio_tracker",
|
||
"wallet_pnl": "wallet",
|
||
# Premium (mapped to closest real tool)
|
||
"forensic_valuation": "forensics",
|
||
"investigation_report": "forensics",
|
||
"osint_identity_hunt": "social_signal",
|
||
# Launchpad
|
||
# (fresh_pair already mapped to launch above)
|
||
}
|
||
|
||
# ── Expanded tool aliases (44 new specialized tools → real handlers) ──
|
||
try:
|
||
from app.routers._expanded_aliases import EXPANDED_ALIASES
|
||
|
||
TOOL_ALIASES.update(EXPANDED_ALIASES)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
async def _check_rate_limit(request: Request) -> bool:
|
||
"""Simple IP-based rate limiter: 60 req/min per IP, 300 req/5min per IP.
|
||
Uses the same Redis as x402 enforcement. Fail-open if Redis is down."""
|
||
try:
|
||
from app.routers.x402_enforcement import get_redis
|
||
|
||
r = get_redis()
|
||
if not r:
|
||
return True # No Redis = allow
|
||
cf_ip = request.headers.get("CF-Connecting-IP", "") or (
|
||
request.client.host if request.client else "0"
|
||
)
|
||
# Normalize IP-key
|
||
import hashlib as _hl
|
||
|
||
ip_key = _hl.sha256(cf_ip.encode()).hexdigest()[:16]
|
||
# 1-minute window
|
||
key_min = f"rl:{ip_key}:min"
|
||
count = r.incr(key_min)
|
||
if count == 1:
|
||
r.expire(key_min, 60)
|
||
if count > 60:
|
||
return False
|
||
# 5-minute window
|
||
key_5m = f"rl:{ip_key}:5min"
|
||
count5 = r.incr(key_5m)
|
||
if count5 == 1:
|
||
r.expire(key_5m, 300)
|
||
return not count5 > 300
|
||
except Exception:
|
||
return True # Fail open
|
||
|
||
|
||
@router.get("/{tool_id}")
|
||
async def tool_alias_dispatcher_get(tool_id: str, request: Request):
|
||
"""GET catch-all for paid tools — converts query params to JSON body and dispatches.
|
||
|
||
Handles x402 discovery agents and browser-based clients that use GET + query params
|
||
instead of POST + JSON body. The enforcement middleware has already verified payment
|
||
or granted a trial before this handler is reached.
|
||
"""
|
||
# Read query params as the request body
|
||
query_params = dict(request.query_params)
|
||
if not query_params:
|
||
# GET without params = discovery/info request, return basic tool info
|
||
pricing_info = {}
|
||
try:
|
||
from app.routers.x402_enforcement import TOOL_PRICES
|
||
|
||
pricing_info = TOOL_PRICES.get(tool_id, {})
|
||
except Exception:
|
||
pass
|
||
if pricing_info:
|
||
return JSONResponse(
|
||
content={
|
||
"tool": tool_id,
|
||
"description": pricing_info.get("description", ""),
|
||
"price_usd": pricing_info.get("price_usd", 0),
|
||
"trial_free": pricing_info.get("trial_free", 3),
|
||
"usage": "POST with JSON body or GET with query params (address, chain, etc.)",
|
||
"method": "GET or POST",
|
||
}
|
||
)
|
||
raise HTTPException(status_code=404, detail=f"Tool '{tool_id}' not found.")
|
||
|
||
# ── Rate limiting ──
|
||
if not await _check_rate_limit(request):
|
||
raise HTTPException(status_code=429, detail="Rate limit exceeded. Max 60 req/min per IP.")
|
||
|
||
# Resolve alias target (same logic as POST dispatcher)
|
||
target = TOOL_ALIASES.get(tool_id)
|
||
injected_chain = None
|
||
|
||
if target is None and "_" in tool_id:
|
||
_CHAIN_SUFFIXES: ClassVar[dict] =
|
||
{
|
||
"solana",
|
||
"base",
|
||
"ethereum",
|
||
"bsc",
|
||
"polygon",
|
||
"arbitrum",
|
||
"optimism",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"tron",
|
||
"bitcoin",
|
||
}
|
||
try:
|
||
from app.routers.x402_enforcement import TOOL_PRICES as _TP
|
||
|
||
pricing = _TP.get(tool_id, {})
|
||
base_tool = pricing.get("base_tool")
|
||
if base_tool:
|
||
target = base_tool
|
||
injected_chain = pricing.get("chain")
|
||
except Exception:
|
||
pass
|
||
if target is None:
|
||
last_underscore = tool_id.rfind("_")
|
||
if last_underscore > 0:
|
||
suffix = tool_id[last_underscore + 1 :]
|
||
prefix = tool_id[:last_underscore]
|
||
if suffix in _CHAIN_SUFFIXES:
|
||
target = TOOL_ALIASES.get(prefix, prefix)
|
||
injected_chain = suffix
|
||
|
||
if target is None:
|
||
# No alias — use the tool_id itself as the target (it's a direct route or DataBus)
|
||
# Check if it has a direct POST route handler
|
||
target = tool_id
|
||
|
||
# Check if target has a dedicated POST handler — if so, dispatch via internal call
|
||
# If not (DataBus-only tool), use the DataBus caching shield
|
||
body = query_params
|
||
if injected_chain and isinstance(body, dict):
|
||
body.setdefault("chain", injected_chain)
|
||
|
||
# ── Dispatch via DataBus caching shield ──
|
||
try:
|
||
result = await td.call_tool(tool_id, body)
|
||
if result is None:
|
||
# Fallback: try internal POST to the catch-all dispatcher
|
||
target_url = f"http://localhost:8000/api/v1/x402-tools/{target}"
|
||
headers = {"Content-Type": "application/json", "User-Agent": "RMI-GET-Proxy/3.1"}
|
||
for h in (
|
||
"X-RMI-Payment",
|
||
"X-RMI-Trial",
|
||
"X-RMI-Trial-Remaining",
|
||
"X-RMI-Internal",
|
||
"X-Device-Id",
|
||
"X-Wallet-Address",
|
||
"Authorization",
|
||
):
|
||
val = request.headers.get(h)
|
||
if val:
|
||
headers[h] = val
|
||
import httpx
|
||
|
||
async with httpx.AsyncClient(timeout=45) as client:
|
||
resp = await client.post(target_url, json=body, headers=headers)
|
||
try:
|
||
result = resp.json()
|
||
except Exception:
|
||
result = {"data": resp.text, "status": resp.status_code}
|
||
except Exception as e:
|
||
logger.error(f"GET dispatch failed for {tool_id}: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Tool execution failed: {e!s}")
|
||
|
||
if isinstance(result, dict):
|
||
result["alias_of"] = target
|
||
result["tool"] = tool_id
|
||
result["method"] = "GET"
|
||
if injected_chain:
|
||
result["chain"] = injected_chain
|
||
|
||
# ── Wallet Intelligence Enrichment ──
|
||
try:
|
||
opt_out = request.query_params.get("enrich", "").lower() == "false"
|
||
from app.routers.x402_enrichment import enrich_tool_response
|
||
|
||
result = enrich_tool_response(tool_id, result, request_params=body, opt_out=opt_out)
|
||
except Exception:
|
||
pass
|
||
|
||
return JSONResponse(content=result, status_code=200)
|
||
|
||
|
||
@router.post("/{tool_id}")
|
||
async def tool_alias_dispatcher(tool_id: str, request: Request):
|
||
"""Catch-all dispatcher for tool aliases and per-chain variants.
|
||
|
||
Handles three cases:
|
||
1. Named aliases (scam_database → urlcheck)
|
||
2. Per-chain variants (wallet_solana → wallet with chain=solana)
|
||
3. Expanded tools (flash_loan_detect → closest real handler)
|
||
|
||
Returns 404 for truly unknown tools."""
|
||
# ── Rate limiting ──
|
||
if not await _check_rate_limit(request):
|
||
raise HTTPException(status_code=429, detail="Rate limit exceeded. Max 60 req/min per IP.")
|
||
# Try exact alias first
|
||
target = TOOL_ALIASES.get(tool_id)
|
||
injected_chain = None
|
||
|
||
# If not a named alias, check if it's a per-chain variant (e.g., wallet_solana)
|
||
if target is None and "_" in tool_id:
|
||
# Try to extract chain suffix
|
||
_CHAIN_SUFFIXES: ClassVar[dict] =
|
||
{
|
||
"solana",
|
||
"base",
|
||
"ethereum",
|
||
"bsc",
|
||
"polygon",
|
||
"arbitrum",
|
||
"optimism",
|
||
"avalanche",
|
||
"fantom",
|
||
"gnosis",
|
||
"tron",
|
||
"bitcoin",
|
||
}
|
||
# Also check TOOL_PRICES for the base_tool/chain fields
|
||
try:
|
||
from app.routers.x402_enforcement import TOOL_PRICES
|
||
|
||
pricing = TOOL_PRICES.get(tool_id, {})
|
||
base_tool = pricing.get("base_tool")
|
||
if base_tool:
|
||
target = base_tool
|
||
injected_chain = pricing.get("chain")
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback: parse tool_chain format
|
||
if target is None:
|
||
last_underscore = tool_id.rfind("_")
|
||
if last_underscore > 0:
|
||
suffix = tool_id[last_underscore + 1 :]
|
||
prefix = tool_id[:last_underscore]
|
||
if suffix in _CHAIN_SUFFIXES and prefix in TOOL_ALIASES:
|
||
target = TOOL_ALIASES[prefix]
|
||
injected_chain = suffix
|
||
elif suffix in _CHAIN_SUFFIXES:
|
||
# Check if prefix is a real tool endpoint
|
||
from app.routers.x402_enforcement import TOOL_PRICES as _TP
|
||
|
||
if prefix in _TP or any(prefix == t for t in TOOL_ALIASES if t == prefix):
|
||
target = TOOL_ALIASES.get(prefix, prefix)
|
||
injected_chain = suffix
|
||
|
||
if target is None:
|
||
# No alias found — try DataBus execution for all 127 catalog tools
|
||
# DataBus is the universal backend for all tools
|
||
body = {}
|
||
if request.headers.get("content-type", "").startswith("application/json"):
|
||
try:
|
||
body = await request.json()
|
||
except Exception:
|
||
body = {}
|
||
|
||
if injected_chain and isinstance(body, dict):
|
||
body.setdefault("chain", injected_chain)
|
||
|
||
try:
|
||
result = await td.call_tool(tool_id, body)
|
||
if result is not None:
|
||
if isinstance(result, dict):
|
||
result["tool"] = tool_id
|
||
result["alias_of"] = "databus"
|
||
if injected_chain:
|
||
result["chain"] = injected_chain
|
||
# Enrichment
|
||
try:
|
||
from app.routers.x402_enrichment import enrich_tool_response
|
||
|
||
opt_out = request.query_params.get("enrich", "").lower() == "false"
|
||
result = enrich_tool_response(
|
||
tool_id, result, request_params=body, opt_out=opt_out
|
||
)
|
||
except Exception:
|
||
pass
|
||
return JSONResponse(content=result, status_code=200)
|
||
except Exception as e:
|
||
logger.error(f"DataBus call failed for {tool_id}: {e}")
|
||
|
||
# DataBus failed — raise 404 only for truly unknown tools
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail=f"Tool '{tool_id}' not found. See /mcp/tools for available tools",
|
||
)
|
||
|
||
target_url = f"http://localhost:8000/api/v1/x402-tools/{target}"
|
||
|
||
try:
|
||
body = (
|
||
await request.json()
|
||
if request.headers.get("content-type", "").startswith("application/json")
|
||
else {}
|
||
)
|
||
except Exception:
|
||
body = {}
|
||
|
||
# Inject chain for per-chain variants
|
||
if injected_chain and isinstance(body, dict):
|
||
body.setdefault("chain", injected_chain)
|
||
|
||
headers = {}
|
||
for h in (
|
||
"x-pay",
|
||
"X-Pay",
|
||
"User-Agent",
|
||
"Authorization",
|
||
"x-wallet-address",
|
||
"X-Wallet-Address",
|
||
"x-turnstile-token",
|
||
"X-Turnstile-Token",
|
||
"X-Forwarded-For",
|
||
"X-Real-IP",
|
||
"CF-Connecting-IP",
|
||
"X-RMI-Internal",
|
||
"X-RMI-Payment",
|
||
"X-RMI-Trial",
|
||
"X-RMI-Trial-Remaining",
|
||
"X-Device-Id",
|
||
):
|
||
val = request.headers.get(h)
|
||
if val:
|
||
headers[h] = val
|
||
headers["content-type"] = "application/json"
|
||
headers["User-Agent"] = headers.get("User-Agent", "RMI-Alias-Proxy/3.1")
|
||
headers["X-RMI-Alias"] = tool_id
|
||
|
||
import httpx
|
||
|
||
async with httpx.AsyncClient(timeout=45) as client:
|
||
resp = await client.post(target_url, json=body, headers=headers)
|
||
try:
|
||
result = resp.json()
|
||
except Exception:
|
||
result = {"data": resp.text, "status": resp.status_code}
|
||
|
||
rh = {}
|
||
for h in ("X-RMI-Payment", "X-RMI-Trial", "X-RMI-Trial-Remaining", "X-RMI-Refund-Flagged"):
|
||
if resp.headers.get(h):
|
||
rh[h] = resp.headers[h]
|
||
|
||
# Wrap result with alias metadata
|
||
if isinstance(result, dict):
|
||
result["alias_of"] = target
|
||
result["tool"] = tool_id
|
||
if injected_chain:
|
||
result["chain"] = injected_chain
|
||
|
||
# ── Wallet Intelligence Enrichment ──
|
||
# Annotate responses with label data, scam patterns, and sanctions from
|
||
# the Wallet Memory Bank (389K labels / 155K addresses in ClickHouse).
|
||
# Controlled by ?enrich=false opt-out. Cached per-address in Redis (1hr TTL).
|
||
try:
|
||
opt_out = request.query_params.get("enrich", "").lower() == "false"
|
||
from app.routers.x402_enrichment import enrich_tool_response
|
||
|
||
result = enrich_tool_response(tool_id, result, request_params=body, opt_out=opt_out)
|
||
except Exception:
|
||
pass # Enrichment is best-effort — never block a response on it
|
||
|
||
return JSONResponse(content=result, status_code=resp.status_code, headers=rh)
|