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
147
app/routers/social_seed.py
Normal file
147
app/routers/social_seed.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#!/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)}
|
||||
Loading…
Add table
Add a link
Reference in a new issue