merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
225
app/routers/x402_launch_fairness.py
Normal file
225
app/routers/x402_launch_fairness.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue