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
147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""#13 - Social-Seed Detection Engine. Watches X/Twitter for new token tickers,
|
|
cross-references against DataBus, runs SENTINEL scan, posts risk assessment."""
|
|
|
|
import os
|
|
import re
|
|
from datetime import UTC, datetime
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Query
|
|
|
|
router = APIRouter(prefix="/api/v1/social-seed", tags=["social-seed-detector"])
|
|
|
|
BACKEND = os.environ.get("BACKEND_URL", "http://localhost:8000")
|
|
DEXSCREENER = "https://api.dexscreener.com/latest/dex/search"
|
|
|
|
# In-memory store for detected social seeds (Redis in prod)
|
|
_detected_seeds: list[dict] = []
|
|
|
|
# Token ticker pattern: $SYMBOL or #SYMBOL
|
|
TICKER_RE = re.compile(r"\$([A-Z]{2,8})|#([A-Za-z]{3,12})", re.IGNORECASE)
|
|
|
|
# Known scam keywords
|
|
SCAM_KEYWORDS = [
|
|
"airdrop",
|
|
"claim now",
|
|
"presale",
|
|
"whitelist",
|
|
"giveaway",
|
|
"free mint",
|
|
"stealth launch",
|
|
"100x",
|
|
"1000x",
|
|
"moon",
|
|
"wen lambo",
|
|
"gem",
|
|
"alpha leak",
|
|
]
|
|
|
|
|
|
@router.get("/detect")
|
|
async def detect_social_seeds(q: str = Query("crypto new token launch")):
|
|
"""Search for social mentions of new token launches. Uses web search as proxy for X."""
|
|
seeds: list[dict] = []
|
|
|
|
# Use web search to find recent mentions (proxy for X/Twitter API)
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(
|
|
"https://html.duckduckgo.com/html/",
|
|
params={"q": f"{q} site:twitter.com after:{datetime.now(UTC).strftime('%Y-%m-%d')}"},
|
|
headers={"User-Agent": "RMI-SocialSeed/1.0"},
|
|
)
|
|
if resp.status_code == 200:
|
|
html = resp.text
|
|
# Extract potential tickers
|
|
found_tickers = set()
|
|
for match in TICKER_RE.finditer(html):
|
|
ticker = (match.group(1) or match.group(2)).upper()
|
|
if len(ticker) >= 3 and ticker not in ("THE", "AND", "FOR", "NEW", "ETH", "BTC", "SOL"):
|
|
found_tickers.add(ticker)
|
|
|
|
for ticker in list(found_tickers)[:20]:
|
|
# Check if this ticker exists on DexScreener
|
|
try:
|
|
r2 = await client.get(f"{DEXSCREENER}?q={ticker}", timeout=8)
|
|
if r2.status_code == 200:
|
|
pairs = r2.json().get("pairs", [])
|
|
if pairs:
|
|
top = pairs[0]
|
|
seeds.append(
|
|
{
|
|
"ticker": ticker,
|
|
"chain": top.get("chainId", "?"),
|
|
"dex": top.get("dexId", "?"),
|
|
"price_usd": float(top.get("priceUsd", 0) or 0),
|
|
"liquidity_usd": top.get("liquidity", {}).get("usd", 0) or 0,
|
|
"age_hours": int(
|
|
(
|
|
(datetime.now(UTC).timestamp() * 1000 - top.get("pairCreatedAt", 0))
|
|
/ 3600000
|
|
)
|
|
or 0
|
|
),
|
|
}
|
|
)
|
|
except Exception:
|
|
continue
|
|
except Exception:
|
|
pass
|
|
|
|
# Score risk for each seed
|
|
for s in seeds:
|
|
s["risk_score"] = 50
|
|
s["risk_flags"] = []
|
|
if s["liquidity_usd"] < 10000:
|
|
s["risk_score"] -= 20
|
|
s["risk_flags"].append("low_liquidity")
|
|
if s["age_hours"] < 1:
|
|
s["risk_score"] -= 10
|
|
s["risk_flags"].append("brand_new")
|
|
s["risk_level"] = "high" if s["risk_score"] < 40 else "medium" if s["risk_score"] < 70 else "low"
|
|
|
|
seeds.sort(key=lambda s: s["age_hours"])
|
|
_detected_seeds.extend(seeds[:30])
|
|
return {"query": q, "seeds_detected": len(seeds), "seeds": seeds}
|
|
|
|
|
|
@router.get("/recent")
|
|
async def get_recent_seeds(limit: int = Query(20, le=50)):
|
|
"""Get recently detected social seeds."""
|
|
return {"seeds": _detected_seeds[-limit:], "total_detected": len(_detected_seeds)}
|
|
|
|
|
|
@router.post("/scan-seed")
|
|
async def scan_detected_seed(ticker: str = Query(...), chain: str = Query("solana")):
|
|
"""Run full SENTINEL scan on a socially detected token."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
# First find the pair on DexScreener
|
|
r1 = await client.get(f"{DEXSCREENER}?q={ticker}", timeout=8)
|
|
if r1.status_code != 200 or not r1.json().get("pairs"):
|
|
return {"error": f"No pair found for {ticker}"}
|
|
|
|
pair = r1.json()["pairs"][0]
|
|
addr = pair.get("pairAddress", "")
|
|
|
|
# Run SENTINEL scan
|
|
r2 = await client.post(
|
|
f"{BACKEND}/api/v1/token/scan",
|
|
json={"token_address": addr, "chain": chain},
|
|
headers={"X-RMI-Key": "rmi-internal-2026"},
|
|
)
|
|
if r2.status_code != 200:
|
|
return {"error": "Scan failed"}
|
|
|
|
scan = r2.json()
|
|
return {
|
|
"ticker": ticker,
|
|
"address": addr,
|
|
"chain": chain,
|
|
"safety_score": scan.get("safety_score", 50),
|
|
"risk_flags": scan.get("risk_flags", []),
|
|
"social_risk": "HIGH" if any(kw in str(scan).lower() for kw in SCAM_KEYWORDS) else "MEDIUM",
|
|
}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|