Some checks failed
CI / build (push) Failing after 3s
PHASE 2.3 (AUDIT-2026-Q3.md):
Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
- app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
with the v1 /api/v1/scanner/ stub.
- app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
library module consumed by unified_scanner_router via get_wallet_scanner()).
- app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
/api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).
Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
- 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
- 63 flat app/*.py (domain modules never imported by live code).
- 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
tests/unit/test_bridge_health.py which directly imports it; reach graph
considered tests/ only as transitive reach — to be patched in next cycle).
Forced-LIVE (NOT archived per user directive):
- app/ai_pipeline_v3.py (3 importers in audit window, importers themselves DEAD)
- app/splade_bm25.py (LIVE via app.rag_service)
- app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
- app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)
Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
- imports = 348 app.* modules
- reached = 194 files reachable from roots
- archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
- Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)
pyproject.toml updates:
- setuptools.packages.find: added exclude for app._archive*
- ruff.extend-exclude: added "app/_archive/"
- mypy.exclude: added "app/_archive/"
Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).
Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.
Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
225 lines
6.9 KiB
Python
225 lines
6.9 KiB
Python
"""
|
|
x402 Router: launch_fairness
|
|
==============================
|
|
Wraps Launch Fairness Analyzer with:
|
|
- Address validation
|
|
- x402 payment middleware integration
|
|
- Caching (Redis if available)
|
|
- Trial quota tracking
|
|
- Rate limiting support
|
|
|
|
TOOL : launch_fairness
|
|
TIER : premium
|
|
PRICE : $0.10 (100000 atoms)
|
|
TRIAL : 2 free checks
|
|
ROUTER: /api/v1/x402-tools/launch_fairness
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from contextlib import suppress
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from app.launch_fairness_analyzer import analyze_launch_fairness
|
|
|
|
logger = logging.getLogger("x402_launch_fairness")
|
|
|
|
router = APIRouter(prefix="/api/v1/x402-tools", tags=["x402-tools"])
|
|
|
|
# ── Redis cache (best-effort) ───────────────────────────────────
|
|
_redis = None
|
|
try:
|
|
import redis.asyncio as aioredis
|
|
|
|
_redis = aioredis.from_url(
|
|
os.getenv("REDIS_URL", "redis://localhost:6379/0"),
|
|
decode_responses=True,
|
|
socket_connect_timeout=2,
|
|
)
|
|
except Exception:
|
|
logger.debug("Redis not available for launch_fairness cache")
|
|
|
|
|
|
async def _get_cache(key: str) -> dict[str, Any] | None:
|
|
"""Get cached result if Redis is available."""
|
|
if _redis is None:
|
|
return None
|
|
with suppress(Exception):
|
|
data = await _redis.get(f"x402:cache:launch_fairness:{key}")
|
|
if data:
|
|
result: dict[str, Any] = json.loads(data)
|
|
return result
|
|
return None
|
|
|
|
|
|
async def _set_cache(key: str, result: dict[str, Any], ttl: int = 300) -> None:
|
|
"""Cache result in Redis with TTL."""
|
|
if _redis is None:
|
|
return
|
|
with suppress(Exception):
|
|
await _redis.setex(
|
|
f"x402:cache:launch_fairness:{key}", ttl, json.dumps(result, default=str)
|
|
)
|
|
|
|
|
|
# ── Trial tracking ──────────────────────────────────────────────
|
|
|
|
|
|
def _trial_key(wallet: str) -> str:
|
|
return f"x402:launch_fairness_trials:{wallet}"
|
|
|
|
|
|
async def _check_trials(wallet: str) -> int:
|
|
"""Check how many trials this wallet has used."""
|
|
if _redis is None:
|
|
return 0
|
|
try:
|
|
used = await _redis.get(_trial_key(wallet))
|
|
return int(used) if used else 0
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
async def _increment_trials(wallet: str) -> None:
|
|
"""Increment trial usage for a wallet."""
|
|
if _redis is None:
|
|
return
|
|
try:
|
|
await _redis.incr(_trial_key(wallet))
|
|
await _redis.expire(_trial_key(wallet), 86400 * 30) # Reset monthly
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ── Request / Response models ───────────────────────────────────
|
|
|
|
|
|
class LaunchFairnessRequest(BaseModel):
|
|
"""Request model for launch fairness analysis."""
|
|
|
|
address: str = Field(..., description="Token contract address to analyze")
|
|
chain: str = Field(
|
|
"auto",
|
|
description="Blockchain (ethereum, solana, bsc, base, polygon, or 'auto' for detection)",
|
|
)
|
|
simulate_data: bool = Field(True, description="Use simulated data for demonstration / testing")
|
|
|
|
@field_validator("address")
|
|
@classmethod
|
|
def validate_address(cls, v: str) -> str:
|
|
v = v.strip()
|
|
is_evm = v.startswith("0x") and len(v) == 42
|
|
is_solana = not v.startswith("0x") and 32 <= len(v) <= 44 and v.isascii()
|
|
if not is_evm and not is_solana:
|
|
raise ValueError(
|
|
"Address must be a valid token contract address (0x... for EVM, base58 for Solana)"
|
|
)
|
|
return v.lower()
|
|
|
|
@field_validator("chain")
|
|
@classmethod
|
|
def validate_chain(cls, v: str) -> str:
|
|
valid_chains = {
|
|
"auto",
|
|
"ethereum",
|
|
"solana",
|
|
"bsc",
|
|
"base",
|
|
"polygon",
|
|
"arbitrum",
|
|
"avalanche",
|
|
}
|
|
v = v.lower().strip()
|
|
if v not in valid_chains:
|
|
raise ValueError(f"Chain must be one of: {', '.join(sorted(valid_chains))}")
|
|
return v
|
|
|
|
|
|
class LaunchFairnessResponse(BaseModel):
|
|
"""Response model for launch fairness analysis."""
|
|
|
|
success: bool = True
|
|
tool: str = "launch_fairness"
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
cached: bool = False
|
|
scanned_at: str = ""
|
|
|
|
|
|
# ── Endpoint ────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/launch_fairness")
|
|
async def analyze_launch_fairness_endpoint(
|
|
request: Request,
|
|
body: LaunchFairnessRequest,
|
|
) -> LaunchFairnessResponse:
|
|
"""
|
|
Analyze token launch fairness - detect sniped distributions, bundled launches,
|
|
bot activity, LP manipulation, and presale concentration.
|
|
|
|
Returns a fairness score (0-100) with per-signal breakdown and evidence.
|
|
"""
|
|
start = time.time()
|
|
wallet = request.headers.get("x-wallet-address", "anonymous")
|
|
|
|
# ── Trial check ──
|
|
max_trial = 2
|
|
trials_used = await _check_trials(wallet)
|
|
if trials_used < max_trial:
|
|
await _increment_trials(wallet)
|
|
|
|
# ── Cache check ──
|
|
cache_key = f"{body.address}:{body.chain}:{body.simulate_data}"
|
|
cached = await _get_cache(cache_key)
|
|
if cached:
|
|
elapsed = time.time() - start
|
|
logger.info(f"Launch fairness cache hit for {body.address[:12]}... ({elapsed:.2f}s)")
|
|
return LaunchFairnessResponse(
|
|
data=cached,
|
|
cached=True,
|
|
scanned_at=cached.get("scanned_at", ""),
|
|
)
|
|
|
|
# ── Run analysis ──
|
|
try:
|
|
result = await analyze_launch_fairness(
|
|
token_address=body.address,
|
|
chain=body.chain,
|
|
simulate_data=body.simulate_data,
|
|
)
|
|
|
|
# Add metadata
|
|
result["scanned_at"] = datetime.now(tz=UTC).isoformat()
|
|
result["tier"] = "premium"
|
|
result["price_usd"] = 0.10
|
|
|
|
# ── Cache result (short TTL - fairness data changes quickly) ──
|
|
await _set_cache(cache_key, result, ttl=120)
|
|
|
|
logger.info(
|
|
f"Launch fairness analyzed {body.address[:12]}... "
|
|
f"score={result.get('fairness_score', '?')}% "
|
|
f"({time.time() - start:.2f}s)"
|
|
)
|
|
|
|
return LaunchFairnessResponse(
|
|
data=result,
|
|
cached=False,
|
|
scanned_at=result["scanned_at"],
|
|
)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Launch fairness validation error: {e}")
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
except Exception as e:
|
|
logger.error(f"Launch fairness analysis failed: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Launch fairness analysis failed. Please try again.",
|
|
) from e
|