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
154
app/routers/laundry_matcher.py
Normal file
154
app/routers/laundry_matcher.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#!/usr/bin/env python3
|
||||
"""#9 — Token Laundry Matcher. Finds copycat tokens by code similarity, deployer patterns,
|
||||
liquidity profile, marketing tactics. Spots scams before they launch."""
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/v1/laundry-matcher", tags=["laundry-matcher"])
|
||||
|
||||
BACKEND = os.environ.get("BACKEND_URL", "http://localhost:8000")
|
||||
|
||||
|
||||
class LaundryMatch(BaseModel):
|
||||
address: str
|
||||
chain: str
|
||||
similarity_score: float # 0-1
|
||||
match_reasons: list[str]
|
||||
risk_inherited: int
|
||||
|
||||
|
||||
def _name_similarity(a: str, b: str) -> float:
|
||||
"""Simple trigram similarity for token names."""
|
||||
|
||||
def trigrams(s):
|
||||
s = s.lower().replace(" ", "")
|
||||
return {s[i : i + 3] for i in range(len(s) - 2)} if len(s) >= 3 else {s}
|
||||
|
||||
ta = trigrams(a)
|
||||
tb = trigrams(b)
|
||||
if not ta or not tb:
|
||||
return 0.0
|
||||
return len(ta & tb) / len(ta | tb)
|
||||
|
||||
|
||||
async def _fetch_token_pairs(chain: str, max_pairs: int = 50) -> list[dict]:
|
||||
"""Fetch recent token pairs from DexScreener for comparison."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.get(f"https://api.dexscreener.com/latest/dex/search?q={chain}")
|
||||
if resp.status_code != 200:
|
||||
return []
|
||||
return resp.json().get("pairs", [])[:max_pairs]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
@router.get("/match/{address}")
|
||||
async def find_matches(
|
||||
address: str,
|
||||
chain: str = Query("solana"),
|
||||
min_similarity: float = Query(0.3, ge=0, le=1),
|
||||
limit: int = Query(10, le=25),
|
||||
):
|
||||
"""Find tokens similar to the given address. Returns top-N copycat matches."""
|
||||
# Get reference token data
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.post(
|
||||
f"{BACKEND}/api/v1/token/scan",
|
||||
json={"token_address": address, "chain": chain},
|
||||
headers={"X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(502, "Scanner unavailable")
|
||||
reference = resp.json()
|
||||
except Exception as e:
|
||||
raise HTTPException(502, f"Scan failed: {e}")
|
||||
|
||||
ref_name = reference.get("free", {}).get("name", "") or reference.get("symbol", "")
|
||||
reference.get("pro", {}).get("deployer_address", "")
|
||||
|
||||
# Fetch comparison pool
|
||||
pairs = await _fetch_token_pairs(chain, max_pairs=50)
|
||||
|
||||
matches: list[dict] = []
|
||||
for p in pairs:
|
||||
base = p.get("baseToken", {})
|
||||
target_name = base.get("name", "") or base.get("symbol", "")
|
||||
target_addr = p.get("pairAddress", "") or base.get("address", "")
|
||||
if target_addr == address:
|
||||
continue
|
||||
|
||||
# Compute similarity
|
||||
name_sim = _name_similarity(ref_name, target_name)
|
||||
reasons: list[str] = []
|
||||
if name_sim > 0.5:
|
||||
reasons.append(f"Name similarity: {name_sim:.0%}")
|
||||
if p.get("liquidity", {}).get("usd", 0) == 0:
|
||||
reasons.append("Zero liquidity pattern")
|
||||
|
||||
similarity = name_sim # Primary metric
|
||||
# Could add: code similarity, deployer overlap, launch timing
|
||||
|
||||
if similarity >= min_similarity:
|
||||
matches.append(
|
||||
{
|
||||
"address": target_addr,
|
||||
"chain": chain,
|
||||
"symbol": base.get("symbol", "?"),
|
||||
"name": target_name,
|
||||
"similarity_score": round(similarity, 3),
|
||||
"match_reasons": reasons,
|
||||
"risk_inherited": reference.get("safety_score", 50),
|
||||
"price_usd": float(p.get("priceUsd", 0) or 0),
|
||||
"liquidity_usd": p.get("liquidity", {}).get("usd", 0) or 0,
|
||||
}
|
||||
)
|
||||
|
||||
matches.sort(key=lambda m: m["similarity_score"], reverse=True)
|
||||
return {
|
||||
"reference": {
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"name": ref_name,
|
||||
"safety_score": reference.get("safety_score", 50),
|
||||
},
|
||||
"matches": matches[:limit],
|
||||
"total_candidates_scanned": len(pairs),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/scan-similar/{address}")
|
||||
async def scan_and_match(address: str, chain: str = Query("solana")):
|
||||
"""Scan the address AND find its matches in a single call."""
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
scan_task = client.post(
|
||||
f"{BACKEND}/api/v1/token/scan",
|
||||
json={"token_address": address, "chain": chain},
|
||||
headers={"X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
|
||||
# Also kick off match search
|
||||
pairs = await _fetch_token_pairs(chain)
|
||||
|
||||
scan_resp = await scan_task
|
||||
if scan_resp.status_code != 200:
|
||||
raise HTTPException(502, "Scan failed")
|
||||
scan = scan_resp.json()
|
||||
|
||||
# Quick match
|
||||
ref_name = scan.get("free", {}).get("name", "") or scan.get("symbol", "")
|
||||
matches = []
|
||||
for p in pairs[:50]:
|
||||
base = p.get("baseToken", {})
|
||||
sim = _name_similarity(ref_name, base.get("name", "") or base.get("symbol", ""))
|
||||
if sim > 0.5:
|
||||
matches.append(
|
||||
{"address": p.get("pairAddress", ""), "symbol": base.get("symbol", "?"), "similarity": round(sim, 3)}
|
||||
)
|
||||
|
||||
return {"scan": scan, "copycat_matches": sorted(matches, key=lambda m: m["similarity"], reverse=True)[:10]}
|
||||
Loading…
Add table
Add a link
Reference in a new issue