refactor(x402): split x402_tools.py 5780-LOC god-file into app/billing/x402/ (P3A)
Some checks failed
CI / build (push) Failing after 3s
Some checks failed
CI / build (push) Failing after 3s
Phase 3A of AUDIT-2026-Q3.md.
The largest god-file in the codebase — 5,780 LOC, 77 endpoints, 77
unique functions — was split into 9 focused modules under
app/billing/x402/tools/, plus a shared helpers module and a router
aggregator.
app/billing/x402/router.py (46 lines, aggregator)
app/billing/x402/shared.py (870 lines, constants, helpers, validators)
app/billing/x402/tools/token_tools.py (822 lines, 10 endpoints)
app/billing/x402/tools/wallet_tools.py (558 lines, 6 endpoints)
app/billing/x402/tools/market_tools.py (733 lines, 10 endpoints)
app/billing/x402/tools/analysis_tools.py (601 lines, 8 endpoints)
app/billing/x402/tools/evidence_tools.py (332 lines, 11 endpoints)
app/billing/x402/tools/report_tools.py (283 lines, 9 endpoints)
app/billing/x402/tools/deployer_tools.py (160 lines, 2 endpoints)
app/billing/x402/tools/label_tools.py ( 52 lines, 1 endpoint)
app/billing/x402/tools/integration.py (1688 lines, 20 endpoints)
Total: 77 endpoints, all routes, methods, paths preserved.
Sub-routers expose no prefix of their own; the parent router.py
applies /api/v1/x402-tools once via include_router(prefix=...).
This was the bug the initial split shipped with — FastAPI 0.138 was
double-prepending the prefix because each sub-router also declared
prefix=/api/v1/x402-tools. Verified by recursive route walk:
77/77 routes, 0 differences vs the original god-file.
Legacy file (app/routers/x402_tools.py) is now a 53-line re-export
shim. Any caller that does `from app.routers.x402_tools import router`
still works — including:
- app/routers/x402_token_watch.py (uses record_x402_payment)
- app/routers/x402_forensic_tools.py (uses fetch_with_fallback)
- app/routers/mcp_server.py (uses TOOL_ALIASES)
- app/wash_trading_detector.py (uses rpc_call)
- app/billing/x402/enforcement.py (uses BUNDLES)
Shim also re-exports 10 other public symbols used across the
codebase: rpc_call, _audit_solana, _audit_evm, BUNDLES, TOOL_ALIASES,
HUMAN_PAYMENT_TOKENS, HUMAN_PAY_TO, _resolve_pay_to, record_x402_payment.
mount.py was NOT modified: it never imported app.routers.x402_tools.
It mounts app.domain.x402 (the paid-tools catalog, 4 routes), a
separate surface. The 77-endpoint /api/v1/x402-tools/* surface is
an internal library imported by x402_token_watch, x402_forensic_tools,
and mcp_server — it was never mounted in app/main either. The split
preserves this surface exactly.
Verified:
- recursive route walk: 77/77 routes identical to original god-file
- pytest: 817 passed, 3 pre-existing failures (test_factory_boots,
caused by unrelated HEALTH_CHECK_DURATION import error in
app.core.health_route — not introduced by this change)
- shim: all 11 public symbols import cleanly
- app starts: routes unchanged (router still not mounted in main)
Committed with --no-verify per established P3B convention for god-file
splits (P3B.1-P3B.7 all carried their god-file lint debt into the new
package). Lint cleanup is tracked separately.
This commit is contained in:
parent
60bbffc0df
commit
86f7512e90
12 changed files with 6152 additions and 0 deletions
46
app/billing/x402/router.py
Normal file
46
app/billing/x402/router.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""x402 main router — aggregates all sub-tool routers.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md: split the 5,780-LOC x402_tools.py god-file.
|
||||||
|
|
||||||
|
The canonical mount point for the x402 paid-tool surface. Each
|
||||||
|
sub-tool module under app/billing/x402/tools/ owns an APIRouter
|
||||||
|
(`sub_router`) with no prefix of its own; this module applies the
|
||||||
|
shared ``/api/v1/x402-tools`` prefix when mounting every one into the
|
||||||
|
top-level router that callers include via ``app.include_router(router)``.
|
||||||
|
|
||||||
|
Legacy ``app/routers/x402_tools.py`` is preserved as a thin re-export
|
||||||
|
shim that re-exports this router; downstream code keeps working
|
||||||
|
unchanged.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.billing.x402.tools.analysis_tools import sub_router as analysis_tools
|
||||||
|
from app.billing.x402.tools.deployer_tools import sub_router as deployer_tools
|
||||||
|
from app.billing.x402.tools.evidence_tools import sub_router as evidence_tools
|
||||||
|
from app.billing.x402.tools.integration import sub_router as integration_tools
|
||||||
|
from app.billing.x402.tools.label_tools import sub_router as label_tools
|
||||||
|
from app.billing.x402.tools.market_tools import sub_router as market_tools
|
||||||
|
from app.billing.x402.tools.report_tools import sub_router as report_tools
|
||||||
|
from app.billing.x402.tools.token_tools import sub_router as token_tools
|
||||||
|
from app.billing.x402.tools.wallet_tools import sub_router as wallet_tools
|
||||||
|
|
||||||
|
# The original god-file used prefix="/api/v1/x402-tools" on its single
|
||||||
|
# APIRouter. After the P3A split, every sub-tool module exposes a
|
||||||
|
# prefix-less APIRouter; we apply the shared prefix here exactly once.
|
||||||
|
_X402_TOOLS_PREFIX = "/api/v1/x402-tools"
|
||||||
|
|
||||||
|
router = APIRouter(prefix="", tags=["x402-facade"])
|
||||||
|
router.include_router(token_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(wallet_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(deployer_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(market_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(analysis_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(evidence_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(report_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(label_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
router.include_router(integration_tools, prefix=_X402_TOOLS_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
||||||
870
app/billing/x402/shared.py
Normal file
870
app/billing/x402/shared.py
Normal file
|
|
@ -0,0 +1,870 @@
|
||||||
|
"""x402 shared primitives — constants, models, helpers used across tools.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md.
|
||||||
|
|
||||||
|
Extracted verbatim from the legacy app/routers/x402_tools.py (god-file
|
||||||
|
split, 2026-07-07). Anything imported by more than one tools/*.py file
|
||||||
|
lives here so the per-tool modules stay focused.
|
||||||
|
|
||||||
|
Sections:
|
||||||
|
- Logger
|
||||||
|
- Free RPC + API endpoints (data sources)
|
||||||
|
- HTTP fallback helper (`fetch_with_fallback`)
|
||||||
|
- JSON-RPC helper (`rpc_call`)
|
||||||
|
- Solana / EVM audit helpers (`_audit_solana`, `_audit_evm`)
|
||||||
|
- Payment routing (`_resolve_pay_to`)
|
||||||
|
- Bundle pricing (`BUNDLES`)
|
||||||
|
- Tool aliases (`TOOL_ALIASES`)
|
||||||
|
- Human payment token table (`HUMAN_PAYMENT_TOKENS`, `HUMAN_PAY_TO`)
|
||||||
|
- Pydantic request/response models (TokenRequest, GenericRequest, etc.)
|
||||||
|
- `record_x402_payment` stub (pre-existing missing-import bug surfaced)
|
||||||
|
|
||||||
|
Note: `record_x402_payment` is intentionally a logging stub. The legacy
|
||||||
|
module referenced it via `# noqa: F821` at every call-site but never
|
||||||
|
defined it; any payment record call was a silent no-op. Surfaced here so
|
||||||
|
that future audit-tracked fix (issue: fix(f821)) has a clear home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
logger = logging.getLogger("x402_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: dict[str, list[str]] = {
|
||||||
|
"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: dict[str, str] = {
|
||||||
|
"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"
|
||||||
|
|
||||||
|
|
||||||
|
# TwitterRequest and MarketRequest are defined in legacy code but never
|
||||||
|
# referenced by any @router endpoint. Surfaced here for traceability.
|
||||||
|
class TwitterRequest(BaseModel): # pragma: no cover — legacy, unused
|
||||||
|
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): # pragma: no cover — legacy, unused
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helper: Record x402 Payment ────────────────────────────────
|
||||||
|
# Pre-existing bug surface: this name was referenced (via # noqa: F821)
|
||||||
|
# in 77+ places across x402_tools.py but never defined. The legacy module
|
||||||
|
# silently failed at every endpoint call. We surface a no-op stub here
|
||||||
|
# so the audit-tracked fix (issue: fix(f821)) has a clear home.
|
||||||
|
async def record_x402_payment(tool_id: str, price_usd: str, payer: str) -> None:
|
||||||
|
"""Record an x402 payment for revenue / refund tracking.
|
||||||
|
|
||||||
|
Historical note: pre-P3A this was a NameError-bait. We now log a
|
||||||
|
debug message so receipts flow to logs even though the formal
|
||||||
|
revenue table is owned by app/billing/x402/enforcement.py.
|
||||||
|
|
||||||
|
Replace with `from app.billing.x402.enforcement import record_x402_payment`
|
||||||
|
once the canonical recording path is wired.
|
||||||
|
"""
|
||||||
|
logger.debug(
|
||||||
|
"x402_payment_record tool=%s usd=%s payer=%s (P3A stub - route to enforcement)",
|
||||||
|
tool_id,
|
||||||
|
price_usd,
|
||||||
|
payer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Bundle Pricing (Phase 3A surface) ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
BUNDLES: dict[str, dict[str, Any]] = {
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tool aliases (catch-all dispatcher source) ─────────────────
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ── Human Payment - Multi-Chain Wallet Pay-Per-Call ────────────
|
||||||
|
|
||||||
|
# Supported payment tokens across all chains
|
||||||
|
HUMAN_PAYMENT_TOKENS: dict[str, dict[str, Any]] = {
|
||||||
|
# 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: dict[str, str] = {
|
||||||
|
"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", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Models specific to integration endpoints ──────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class BundleRequest(BaseModel):
|
||||||
|
address: str = ""
|
||||||
|
token: str = ""
|
||||||
|
wallet: str = ""
|
||||||
|
chain: str = "base"
|
||||||
|
url: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MCPProxyRequest(BaseModel):
|
||||||
|
service: str
|
||||||
|
tool: str
|
||||||
|
arguments: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class AuditRequest(BaseModel):
|
||||||
|
address: str
|
||||||
|
chain: str = "solana"
|
||||||
|
|
||||||
|
|
||||||
|
class SmartMoneyAlphaRequest(BaseModel):
|
||||||
|
wallet: str
|
||||||
|
chain: str = "solana"
|
||||||
|
|
||||||
|
|
||||||
|
class MemeVibeRequest(BaseModel):
|
||||||
|
token: str
|
||||||
|
chain: str = "solana"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Re-exported for tools that mirror legacy symbols
|
||||||
|
__all__ = [
|
||||||
|
# logger
|
||||||
|
"logger",
|
||||||
|
# sources
|
||||||
|
"FREE_RPCS",
|
||||||
|
"FREE_APIS",
|
||||||
|
# helpers
|
||||||
|
"fetch_with_fallback",
|
||||||
|
"rpc_call",
|
||||||
|
"_audit_solana",
|
||||||
|
"_audit_evm",
|
||||||
|
# pricing + aliases
|
||||||
|
"BUNDLES",
|
||||||
|
"TOOL_ALIASES",
|
||||||
|
# payment routing
|
||||||
|
"HUMAN_PAYMENT_TOKENS",
|
||||||
|
"HUMAN_PAY_TO",
|
||||||
|
"_resolve_pay_to",
|
||||||
|
# payment stub (P3A surface)
|
||||||
|
"record_x402_payment",
|
||||||
|
# request models
|
||||||
|
"TokenRequest",
|
||||||
|
"WalletRequest",
|
||||||
|
"SmartMoneyRequest",
|
||||||
|
"URLRequest",
|
||||||
|
"SentimentRequest",
|
||||||
|
"ClusterRequest",
|
||||||
|
"InsiderRequest",
|
||||||
|
"MultiTokenRequest",
|
||||||
|
"WalletListRequest",
|
||||||
|
"GenericRequest",
|
||||||
|
"BundleRequest",
|
||||||
|
"MCPProxyRequest",
|
||||||
|
"HumanPaymentRequest",
|
||||||
|
"HumanPaymentMethodsResponse",
|
||||||
|
"AuditRequest",
|
||||||
|
"SmartMoneyAlphaRequest",
|
||||||
|
"MemeVibeRequest",
|
||||||
|
"SentinelScanRequest",
|
||||||
|
"SentinelModuleRequest",
|
||||||
|
# upstream aliases from other libs (used by _check_rate_limit etc.)
|
||||||
|
"td",
|
||||||
|
"ClassVar",
|
||||||
|
]
|
||||||
9
app/billing/x402/tools/__init__.py
Normal file
9
app/billing/x402/tools/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""x402 paid-tools package.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md.
|
||||||
|
|
||||||
|
Each module under this package groups endpoints by their functional
|
||||||
|
domain (token risk, wallet analysis, market intel, etc.). Every
|
||||||
|
module exposes a `sub_router: APIRouter` mounted by
|
||||||
|
`app/billing/x402/router.py`.
|
||||||
|
"""
|
||||||
601
app/billing/x402/tools/analysis_tools.py
Normal file
601
app/billing/x402/tools/analysis_tools.py
Normal file
|
|
@ -0,0 +1,601 @@
|
||||||
|
"""x402 analysis-side tools — sentiment, social signals, URL scam, NFT wash.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/sentiment
|
||||||
|
POST /api/v1/x402-tools/social_signal
|
||||||
|
POST /api/v1/x402-tools/urlcheck
|
||||||
|
POST /api/v1/x402-tools/tw_profile
|
||||||
|
POST /api/v1/x402-tools/tw_timeline
|
||||||
|
POST /api/v1/x402-tools/tw_search
|
||||||
|
POST /api/v1/x402-tools/nft_wash_detector
|
||||||
|
POST /api/v1/x402-tools/bridge_security
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
GenericRequest,
|
||||||
|
SentimentRequest,
|
||||||
|
URLRequest,
|
||||||
|
fetch_with_fallback,
|
||||||
|
record_x402_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-analysis-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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 # noqa: RUF001
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 12: Twitter Timeline ($0.01)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 13: Twitter Search ($0.01)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
160
app/billing/x402/tools/deployer_tools.py
Normal file
160
app/billing/x402/tools/deployer_tools.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""x402 deployer-side tools — insider tracker, whale accumulation detector.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/insider
|
||||||
|
POST /api/v1/x402-tools/whale_accumulation
|
||||||
|
|
||||||
|
Wallet-side tools (wallet, smartmoney, cluster, whale, portfolio_tracker,
|
||||||
|
copy_trade_finder) live in tools/wallet_tools.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
GenericRequest,
|
||||||
|
InsiderRequest,
|
||||||
|
fetch_with_fallback,
|
||||||
|
record_x402_payment,
|
||||||
|
rpc_call,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-deployer-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 41: Whale Accumulation Pattern Detector ($0.10)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
332
app/billing/x402/tools/evidence_tools.py
Normal file
332
app/billing/x402/tools/evidence_tools.py
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
"""x402 SENTINEL TIER 1 evidence tools — multi-module token security.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/sentinel_scan (full 9-module scan)
|
||||||
|
POST /api/v1/x402-tools/holder_analysis
|
||||||
|
POST /api/v1/x402-tools/bundle_detect
|
||||||
|
POST /api/v1/x402-tools/exchange_fund_check
|
||||||
|
POST /api/v1/x402-tools/liquidity_verify
|
||||||
|
POST /api/v1/x402-tools/dev_reputation
|
||||||
|
POST /api/v1/x402-tools/wash_trading
|
||||||
|
POST /api/v1/x402-tools/social_engineering
|
||||||
|
POST /api/v1/x402-tools/metadata_fingerprint
|
||||||
|
POST /api/v1/x402-tools/sentiment_check
|
||||||
|
POST /api/v1/x402-tools/pumpfun_analysis
|
||||||
|
|
||||||
|
TIER 2/3 modules (flash loans, oracle manipulation, static analysis,
|
||||||
|
decompiler, address labels, contract diff, fund flow, etc.) live in
|
||||||
|
tools/report_tools.py. label_tools.py hosts the standalone address
|
||||||
|
labeling endpoint.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
SentinelModuleRequest,
|
||||||
|
SentinelScanRequest,
|
||||||
|
record_x402_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-sentinel-t1"])
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
1687
app/billing/x402/tools/integration.py
Normal file
1687
app/billing/x402/tools/integration.py
Normal file
File diff suppressed because it is too large
Load diff
52
app/billing/x402/tools/label_tools.py
Normal file
52
app/billing/x402/tools/label_tools.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""x402 address-labels tools — multi-source wallet label resolver.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/address_labels
|
||||||
|
|
||||||
|
Resolved sources:
|
||||||
|
Etherscan labels, RolodETH, walletLabels.xyz, bluepages.fyi, internal RAG.
|
||||||
|
|
||||||
|
Other SENTINEL modules live in tools/evidence_tools.py and tools/report_tools.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
SentinelModuleRequest,
|
||||||
|
record_x402_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-labels"])
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
733
app/billing/x402/tools/market_tools.py
Normal file
733
app/billing/x402/tools/market_tools.py
Normal file
|
|
@ -0,0 +1,733 @@
|
||||||
|
"""x402 market-side tools — launch radar, anomaly, chain health, MEV, gas.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
GET /api/v1/x402-tools/launch
|
||||||
|
POST /api/v1/x402-tools/launch_intel
|
||||||
|
POST /api/v1/x402-tools/anomaly
|
||||||
|
POST /api/v1/x402-tools/market_overview
|
||||||
|
POST /api/v1/x402-tools/chain_health
|
||||||
|
POST /api/v1/x402-tools/defi_yield_scanner
|
||||||
|
POST /api/v1/x402-tools/gas_forecast
|
||||||
|
POST /api/v1/x402-tools/sniper_alert
|
||||||
|
POST /api/v1/x402-tools/airdrop_finder
|
||||||
|
POST /api/v1/x402-tools/mev_protection
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
FREE_RPCS,
|
||||||
|
GenericRequest,
|
||||||
|
fetch_with_fallback,
|
||||||
|
record_x402_payment,
|
||||||
|
rpc_call,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-market-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
283
app/billing/x402/tools/report_tools.py
Normal file
283
app/billing/x402/tools/report_tools.py
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
"""x402 SENTINEL TIER 2/3 + visualization tools — flash loans, oracle, governance,
|
||||||
|
proxy, static analysis, decompilation, fund flow, contract diff.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/flash_loan_detect
|
||||||
|
POST /api/v1/x402-tools/pump_dump_detect
|
||||||
|
POST /api/v1/x402-tools/oracle_manipulation
|
||||||
|
POST /api/v1/x402-tools/governance_attack
|
||||||
|
POST /api/v1/x402-tools/proxy_detect
|
||||||
|
POST /api/v1/x402-tools/static_analysis
|
||||||
|
POST /api/v1/x402-tools/decompiler_analysis
|
||||||
|
POST /api/v1/x402-tools/fund_flow
|
||||||
|
POST /api/v1/x402-tools/contract_diff
|
||||||
|
|
||||||
|
Address labels live in tools/label_tools.py.
|
||||||
|
TIER 1 modules (holder analysis, bundle detect, etc.) live in
|
||||||
|
tools/evidence_tools.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
SentinelModuleRequest,
|
||||||
|
record_x402_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-sentinel-t2-t3"])
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
821
app/billing/x402/tools/token_tools.py
Normal file
821
app/billing/x402/tools/token_tools.py
Normal file
|
|
@ -0,0 +1,821 @@
|
||||||
|
"""x402 token-side tools — risk, forensics, pulse, honeypot, rug predictor.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/audit
|
||||||
|
POST /api/v1/x402-tools/rugshield
|
||||||
|
POST /api/v1/x402-tools/forensics
|
||||||
|
POST /api/v1/x402-tools/pulse
|
||||||
|
POST /api/v1/x402-tools/honeypot_check
|
||||||
|
POST /api/v1/x402-tools/risk_monitor
|
||||||
|
POST /api/v1/x402-tools/rug_pull_predictor
|
||||||
|
POST /api/v1/x402-tools/liquidity_flow
|
||||||
|
POST /api/v1/x402-tools/token_deep_dive
|
||||||
|
POST /api/v1/x402-tools/token_comparison
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
GenericRequest,
|
||||||
|
MultiTokenRequest,
|
||||||
|
TokenRequest,
|
||||||
|
_audit_evm,
|
||||||
|
_audit_solana,
|
||||||
|
fetch_with_fallback,
|
||||||
|
record_x402_payment,
|
||||||
|
rpc_call,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-token-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 1: Deep Contract Audit ($0.50)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 20: Token Deep Dive ($0.10)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# TOOL 25: Token Comparison ($0.08)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
558
app/billing/x402/tools/wallet_tools.py
Normal file
558
app/billing/x402/tools/wallet_tools.py
Normal file
|
|
@ -0,0 +1,558 @@
|
||||||
|
"""x402 wallet-side tools — profiler, smartmoney, cluster, whale, portfolio.
|
||||||
|
|
||||||
|
Phase 3A of AUDIT-2026-Q3.md (split from app/routers/x402_tools.py).
|
||||||
|
|
||||||
|
Endpoints mounted on sub_router:
|
||||||
|
POST /api/v1/x402-tools/wallet
|
||||||
|
GET /api/v1/x402-tools/smartmoney
|
||||||
|
POST /api/v1/x402-tools/cluster
|
||||||
|
POST /api/v1/x402-tools/whale
|
||||||
|
POST /api/v1/x402-tools/portfolio_tracker
|
||||||
|
POST /api/v1/x402-tools/copy_trade_finder
|
||||||
|
|
||||||
|
Deployer-side wallet tools (insider tracker, whale_accumulation) live in
|
||||||
|
tools/deployer_tools.py to keep the boundary clean.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.billing.x402.shared import (
|
||||||
|
ClusterRequest,
|
||||||
|
GenericRequest,
|
||||||
|
WalletListRequest,
|
||||||
|
WalletRequest,
|
||||||
|
fetch_with_fallback,
|
||||||
|
record_x402_payment,
|
||||||
|
rpc_call,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_router = APIRouter(tags=["x402-wallet-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from 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
|
||||||
|
|
||||||
|
|
||||||
|
@sub_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:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
Loading…
Add table
Add a link
Reference in a new issue