| """ |
| SENTINEL β Enhanced Bundle & Sniper Detection |
| ============================================= |
| Detects coordinated wallet groups that purchase simultaneously at token launch, |
| funded by the same source, often in the same block. |
| |
| Detection heuristics: |
| - Same-block purchase (very high confidence) |
| - Deployer funding chain (5-hop trace, very high) |
| - Multi-hop funding (high) |
| - Identical buy sizes (medium) |
| - Coordinated timing (medium) |
| - Shared RPC/Jito endpoint (medium) |
| - Compute budget pattern analysis (Solana-specific) |
| |
| Uses direct API calls: Helius (transaction/signature data), |
| DexScreener (pairs/trades), Solscan (account transfers, transaction detail). |
| """ |
|
|
| import logging |
| import os |
| from dataclasses import dataclass, field |
|
|
| import httpx |
|
|
| from app.chain_client import ChainClient |
| from app.chain_registry import is_solana |
| from app.free_solscan_client import FreeSolscanClient |
|
|
| logger = logging.getLogger("bundle_detector") |
|
|
|
|
| @dataclass |
| class FundingStep: |
| address: str |
| amount: float |
| token: str |
| timestamp: int |
| tx_signature: str = "" |
|
|
|
|
| @dataclass |
| class BundledWallet: |
| address: str |
| funding_chain: list[FundingStep] |
| buy_tx: str | None = None |
| buy_amount: float = 0.0 |
| buy_block: int = 0 |
| confidence: str = "MEDIUM" |
| evidence: list[str] = field(default_factory=list) |
|
|
|
|
| @dataclass |
| class SameBlockSniper: |
| address: str |
| block_number: int |
| buy_amount: float = 0.0 |
| compute_units: int = 0 |
| priority_fee: float = 0.0 |
| is_jito: bool = False |
|
|
|
|
| @dataclass |
| class BehaviorCluster: |
| wallets: list[str] |
| pattern: str |
| combined_percentage: float |
| confidence: float |
|
|
|
|
| @dataclass |
| class BundleReport: |
| token_address: str |
| chain: str |
| deployer: str |
| deploy_block: int |
| deploy_timestamp: int |
| total_bundled_percentage: float |
| deployer_funded: list[BundledWallet] |
| same_block_snipers: list[SameBlockSniper] |
| behavior_clusters: list[BehaviorCluster] |
| compute_budget_patterns: dict[str, int] = field(default_factory=dict) |
| jito_bundles_detected: int = 0 |
| risk_score: int = 0 |
| risk_level: str = "LOW" |
| warnings: list[str] = field(default_factory=list) |
|
|
|
|
| class BundleDetector: |
| """Enhanced bundle and sniper detection with funding chain analysis. |
| |
| Fetches data directly from Helius, DexScreener, and Solscan. |
| """ |
|
|
| |
| JITO_PROGRAMS = { |
| "Jito4APyf6rDt1pD1jH3nD3is4v7TwzFNWjjMP7B2RK", |
| "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", |
| "ADuCadRmgjMe6UzXVxR1Kn9CS2CXAf8bjiKkC4xFcegX", |
| } |
|
|
| |
| BUNDLER_PROGRAMS = { |
| "Smithii": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", |
| "PumpFun": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", |
| "LaunchLab": "LaunchLabzbP1j4R7KzEFmYSF3EXGnZ6tY7AZ8eg3P2qJ", |
| } |
|
|
| def __init__(self): |
| self._http = httpx.AsyncClient(timeout=15.0) |
| self._chain = ChainClient() |
| self._solscan = FreeSolscanClient() |
| self._helius_key = os.getenv("HELIUS_API_KEY", "") |
|
|
| async def close(self): |
| await self._http.aclose() |
|
|
| |
|
|
| async def _fetch_dexscreener_token(self, token_address: str) -> dict | None: |
| """Fetch token pair data from DexScreener (free, no key).""" |
| try: |
| resp = await self._http.get(f"https://api.dexscreener.com/latest/dex/tokens/{token_address}") |
| if resp.status_code == 200: |
| data = resp.json() |
| pairs = data.get("pairs") or [] |
| return pairs[0] if pairs else None |
| except Exception as e: |
| logger.warning(f"DexScreener fetch failed for {token_address}: {e}") |
| return None |
|
|
| async def _fetch_solscan_token_data(self, token_address: str) -> dict | None: |
| """Fetch token metadata from Solscan (Solana).""" |
| try: |
| return self._solscan.token_data(token_address) |
| except Exception as e: |
| logger.warning(f"Solscan token data failed for {token_address}: {e}") |
| return None |
|
|
| async def _fetch_helius_signatures(self, address: str, limit: int = 50) -> list[dict]: |
| """Fetch recent signatures for an address via Helius RPC.""" |
| result = await self._chain.rpc_call("getSignaturesForAddress", [address, {"limit": limit}]) |
| if result and "result" in result: |
| return result["result"] |
| return [] |
|
|
| async def _fetch_helius_transaction(self, tx_signature: str) -> dict | None: |
| """Fetch full transaction details via Helius RPC.""" |
| result = await self._chain.rpc_call( |
| "getTransaction", |
| [tx_signature, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}], |
| ) |
| if result and "result" in result: |
| return result["result"] |
| return None |
|
|
| async def _fetch_solscan_transactions(self, address: str, page: int = 1, page_size: int = 40) -> list[dict]: |
| """Fetch transaction history from Solscan.""" |
| try: |
| return self._solscan.account_transactions(address, page=page, page_size=page_size) or [] |
| except Exception as e: |
| logger.warning(f"Solscan transactions failed for {address}: {e}") |
| return [] |
|
|
| async def _fetch_solscan_transaction_detail(self, tx_hash: str) -> dict | None: |
| """Fetch transaction detail from Solscan.""" |
| try: |
| return self._solscan.transaction_detail(tx_hash) |
| except Exception as e: |
| logger.warning(f"Solscan transaction detail failed for {tx_hash}: {e}") |
| return None |
|
|
| |
|
|
| async def _get_deployment_info(self, token: str, chain: str, deploy_tx: str | None) -> tuple[str, int, int]: |
| """Fetch deployment transaction info using direct APIs. |
| |
| Strategy: |
| 1. If deploy_tx is provided, fetch that transaction directly |
| 2. Otherwise, look up the token on DexScreener (has pairCreation metadata) |
| 3. Fallback: use Solscan token data for creation info |
| """ |
| |
| if deploy_tx: |
| if is_solana(chain): |
| tx_data = await self._fetch_helius_transaction(deploy_tx) |
| if tx_data: |
| |
| msg = tx_data.get("transaction", {}).get("message", {}) |
| account_keys = msg.get("accountKeys", []) |
| signers = [a for a in account_keys if a.get("signer", a.get("writable", False))] |
| deployer = "" |
| if signers: |
| deployer = signers[0].get("pubkey", signers[0] if isinstance(signers[0], str) else "") |
| elif account_keys: |
| first = account_keys[0] |
| deployer = first.get("pubkey", first) if isinstance(first, dict) else first |
| block_time = tx_data.get("blockTime", 0) or 0 |
| slot = tx_data.get("slot", 0) or 0 |
| return (deployer, slot, block_time * 1000 if block_time else 0) |
|
|
| |
| dex_pair = await self._fetch_dexscreener_token(token) |
| if dex_pair: |
| info = dex_pair.get("info", {}) or {} |
| deployer = info.get("creator", dex_pair.get("pairCreator", "")) |
| block_time = dex_pair.get("pairCreatedAt", 0) |
| if deployer: |
| return (deployer, 0, block_time) |
|
|
| |
| if is_solana(chain): |
| sol_data = await self._fetch_solscan_token_data(token) |
| if sol_data: |
| deployer = sol_data.get("creator", sol_data.get("owner", "")) |
| creation_time = sol_data.get("blockTime", 0) or 0 |
| if deployer: |
| return (deployer, 0, creation_time) |
|
|
| return "unknown", 0, 0 |
|
|
| async def _get_early_buyers(self, token: str, chain: str, deploy_block: int) -> list: |
| """Get wallets that bought within first 10 blocks of deployment. |
| |
| Uses DexScreener boosts/transactions + Solscan account data. |
| For Solana: queries Helius for early signatures after deploy. |
| """ |
| early_buyers = [] |
|
|
| if is_solana(chain) and deploy_block > 0: |
| |
| sigs = await self._fetch_helius_signatures(token, limit=50) |
| for sig_info in sigs[:20]: |
| slot = sig_info.get("slot", 0) |
| |
| if 0 < slot - deploy_block <= 10: |
| early_buyers.append( |
| _EarlyBuyer( |
| address=sig_info.get("signer", ""), |
| first_buy_tx=sig_info.get("signature", ""), |
| first_buy_block=slot, |
| ) |
| ) |
|
|
| return early_buyers |
|
|
| async def _trace_funding_chain( |
| self, wallet: str, chain: str, max_depth: int = 5, before_time: int = 0 |
| ) -> list[FundingStep]: |
| """Trace SOL/token transfers backward from a wallet to find funding source. |
| |
| Uses Solscan top transfers + Helius RPC for on-chain data. |
| """ |
| chain_result: list[FundingStep] = [] |
|
|
| if is_solana(chain): |
| |
| funding_sources = self._solscan.get_wallet_funding_sources(wallet, days=30) |
| if funding_sources: |
| for src in funding_sources[:max_depth]: |
| chain_result.append( |
| FundingStep( |
| address=src.get("from", ""), |
| amount=float(src.get("amount", 0)), |
| token=src.get("symbol", "SOL"), |
| timestamp=int(src.get("time", 0) or 0), |
| tx_signature=src.get("tx_hash", ""), |
| ) |
| ) |
|
|
| return chain_result |
|
|
| |
|
|
| def _cluster_by_behavior(self, buyers: list) -> list[BehaviorCluster]: |
| """Cluster wallets by identical trading patterns.""" |
| |
| |
| return [] |
|
|
| def _analyze_compute_budgets(self, buyers: list) -> tuple[dict[str, int], int]: |
| """Analyze Solana compute budget patterns to identify bot networks. |
| |
| Key signals: |
| - Casual traders: variable compute, random priority fees |
| - Bot operators: identical compute units, fixed priority fee |
| - Sniper bots: max compute (1.4M CU), max priority fee |
| """ |
| patterns = {} |
| jito_count = 0 |
|
|
| for buyer in buyers: |
| cu = getattr(buyer, "compute_units", 0) |
| pf = getattr(buyer, "priority_fee", 0.0) |
| is_jito = getattr(buyer, "is_jito", False) |
|
|
| if is_jito: |
| jito_count += 1 |
|
|
| |
| if cu > 1_300_000: |
| pattern = "max_compute_sniper" |
| elif cu > 200_000 and pf < 0.001: |
| pattern = "fixed_fee_bot" |
| elif cu < 200_000: |
| pattern = "normal_trader" |
| else: |
| pattern = "standard_bot" |
|
|
| patterns[pattern] = patterns.get(pattern, 0) + 1 |
|
|
| return patterns, jito_count |
|
|
| def _calculate_risk( |
| self, |
| deployer_funded: list, |
| same_block: list, |
| behavior_clusters: list, |
| jito_count: int, |
| bundled_pct: float, |
| ) -> tuple[int, str, list[str]]: |
| """Calculate overall bundle risk score and warnings.""" |
| score = 0 |
| warnings = [] |
|
|
| |
| if deployer_funded: |
| fund_pct = sum(b.buy_amount for b in deployer_funded) / max(bundled_pct, 0.01) * 100 |
| score += min(50, len(deployer_funded) * 10) |
| warnings.append( |
| f"DEPLOYER_FUNDED: {len(deployer_funded)} wallets funded by deployer ({fund_pct:.1f}% of supply)" |
| ) |
|
|
| |
| if same_block: |
| score += min(30, len(same_block) * 5) |
| warnings.append(f"SAME_BLOCK: {len(same_block)} wallets bought in deployment block") |
|
|
| |
| if jito_count > 0: |
| score += min(20, jito_count * 5) |
| warnings.append(f"JITO_BUNDLE: {jito_count} wallets used Jito tips") |
|
|
| |
| if behavior_clusters: |
| score += min(15, len(behavior_clusters) * 5) |
| warnings.append(f"BEHAVIOR_CLUSTER: {len(behavior_clusters)} coordinated wallet groups detected") |
|
|
| |
| if bundled_pct > 20: |
| score += 20 |
| warnings.append(f"BUNDLED_SUPPLY: {bundled_pct:.1f}% of supply in bundled wallets") |
| elif bundled_pct > 10: |
| score += 10 |
|
|
| score = min(100, score) |
|
|
| if score >= 70: |
| level = "CRITICAL" |
| elif score >= 45: |
| level = "HIGH" |
| elif score >= 20: |
| level = "MEDIUM" |
| else: |
| level = "LOW" |
|
|
| return score, level, warnings |
|
|
| |
|
|
| async def detect_bundles(self, token_address: str, chain: str, deploy_tx: str | None = None) -> BundleReport: |
| """Full bundle detection for a token launch. |
| |
| Steps: |
| 1. Find deployment transaction and deployer |
| 2. Get all buyers in first 10 blocks |
| 3. Trace funding chains for each buyer (5 hops) |
| 4. Identify deployer-funded wallets |
| 5. Identify same-block snipers |
| 6. Analyze compute budget patterns (Solana) |
| 7. Cluster by behavioral patterns |
| 8. Calculate risk score |
| """ |
| |
| deployer, deploy_block, deploy_ts = await self._get_deployment_info(token_address, chain, deploy_tx) |
|
|
| if not deployer or deployer == "unknown": |
| return BundleReport( |
| token_address=token_address, |
| chain=chain, |
| deployer="unknown", |
| deploy_block=0, |
| deploy_timestamp=0, |
| total_bundled_percentage=0, |
| deployer_funded=[], |
| same_block_snipers=[], |
| behavior_clusters=[], |
| warnings=["Could not determine deployer"], |
| ) |
|
|
| |
| early_buyers = await self._get_early_buyers(token_address, chain, deploy_block) |
|
|
| |
| deployer_funded = [] |
| for buyer in early_buyers: |
| chain_trace = await self._trace_funding_chain(buyer.address, chain, max_depth=5, before_time=deploy_ts) |
| chain_addrs = [step.address for step in chain_trace] |
| if deployer in chain_addrs: |
| deployer_funded.append( |
| BundledWallet( |
| address=buyer.address, |
| funding_chain=chain_trace, |
| buy_tx=buyer.first_buy_tx, |
| buy_amount=buyer.buy_amount, |
| buy_block=buyer.first_buy_block, |
| confidence="VERY_HIGH", |
| evidence=[f"Funded by deployer via {len(chain_trace)} hops"], |
| ) |
| ) |
|
|
| |
| same_block = [ |
| SameBlockSniper( |
| address=b.address, |
| block_number=b.first_buy_block, |
| buy_amount=b.buy_amount, |
| compute_units=getattr(b, "compute_units", 0), |
| priority_fee=getattr(b, "priority_fee", 0.0), |
| is_jito=getattr(b, "is_jito", False), |
| ) |
| for b in early_buyers |
| if b.first_buy_block == deploy_block |
| ] |
|
|
| |
| behavior_clusters = self._cluster_by_behavior(early_buyers) |
|
|
| |
| compute_patterns = {} |
| jito_count = 0 |
| if is_solana(chain): |
| compute_patterns, jito_count = self._analyze_compute_budgets(early_buyers) |
|
|
| |
| total_bundled_pct = sum(b.buy_amount for b in deployer_funded) |
| total_supply = sum(b.buy_amount for b in early_buyers) or 1 |
| total_bundled_pct = (total_bundled_pct / total_supply * 100) if total_supply else 0 |
|
|
| |
| risk_score, risk_level, warnings = self._calculate_risk( |
| deployer_funded, same_block, behavior_clusters, jito_count, total_bundled_pct |
| ) |
|
|
| return BundleReport( |
| token_address=token_address, |
| chain=chain, |
| deployer=deployer, |
| deploy_block=deploy_block, |
| deploy_timestamp=deploy_ts, |
| total_bundled_percentage=round(total_bundled_pct, 2), |
| deployer_funded=deployer_funded, |
| same_block_snipers=same_block, |
| behavior_clusters=behavior_clusters, |
| compute_budget_patterns=compute_patterns, |
| jito_bundles_detected=jito_count, |
| risk_score=risk_score, |
| risk_level=risk_level, |
| warnings=warnings, |
| ) |
|
|
|
|
| |
| from dataclasses import dataclass as _dataclass |
|
|
|
|
| @_dataclass |
| class _EarlyBuyer: |
| address: str |
| first_buy_tx: str = "" |
| first_buy_block: int = 0 |
| buy_amount: float = 0.0 |
| compute_units: int = 0 |
| priority_fee: float = 0.0 |
| is_jito: bool = False |
|
|