From d6a40a27bc6876b626128ad88e346c9fe8c7aecb Mon Sep 17 00:00:00 2001 From: nirvagold Date: Tue, 13 Jan 2026 10:57:31 +0700 Subject: [PATCH 1/2] fix(risk): Change no-liquidity score from LOW(30) to HIGH(70) BREAKING: Tokens without V2 liquidity are now HIGH RISK - Cannot verify buy/sell safety = should NOT be trusted - Updated reason message to show UNVERIFIED warning - Added risk factors explaining the limitation This prevents false sense of security for unverifiable tokens --- src/api/handlers.rs | 10 ++++++---- src/core/honeypot.rs | 10 +++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/api/handlers.rs b/src/api/handlers.rs index 5ddecf5..ba40eb6 100644 --- a/src/api/handlers.rs +++ b/src/api/handlers.rs @@ -667,11 +667,13 @@ pub async fn get_stats(State(state): State>) -> Json u8 { - // Special case: No liquidity found - not necessarily dangerous - // Token might just trade on a different DEX + // Special case: No liquidity found - THIS IS SUSPICIOUS! + // If we can't simulate buy/sell, we can't verify safety + // Treat as HIGH RISK (not safe to trade) if !result.buy_success && !result.sell_success && !result.is_honeypot && !result.sell_reverted { - // No liquidity = unknown, give neutral score - return 30; // "LOW" risk - we just couldn't test it + // No liquidity = UNVERIFIED = HIGH RISK + // User should NOT trade tokens we can't verify + return 70; // "HIGH" risk - cannot verify safety } // Base score based on simulation results diff --git a/src/core/honeypot.rs b/src/core/honeypot.rs index fc50c13..45c04e4 100644 --- a/src/core/honeypot.rs +++ b/src/core/honeypot.rs @@ -418,10 +418,10 @@ impl HoneypotDetector { } } - // No liquidity found on any DEX + // No liquidity found on any DEX - HIGH RISK! Ok(HoneypotResult { is_honeypot: false, - reason: format!("No liquidity found on {} (tried: {})", self.chain_name, tried_dexes.join(", ")), + reason: format!("⚠️ UNVERIFIED - No V2 liquidity on {} (tried: {}). Cannot confirm safety!", self.chain_name, tried_dexes.join(", ")), buy_success: false, sell_success: false, sell_reverted: false, @@ -429,7 +429,11 @@ impl HoneypotDetector { sell_tax_percent: 0.0, total_loss_percent: 0.0, access_control_penalty, - risk_factors: vec![format!("No pair on: {}", tried_dexes.join(", "))], + risk_factors: vec![ + format!("No V2 pair found on: {}", tried_dexes.join(", ")), + "Token may trade on V3/unsupported DEX".to_string(), + "Cannot verify buy/sell safety".to_string(), + ], latency_ms, }) } From 42c19e5dd010bc4cb4e1e307fd783eecfbf5716f Mon Sep 17 00:00:00 2001 From: nirvagold Date: Tue, 13 Jan 2026 11:07:51 +0700 Subject: [PATCH 2/2] feat: Add DexScreener market data + skeleton loading Backend: - Add price_usd, liquidity_usd, volume_24h_usd, dex_name, pair_address to API response - Extract market data from DexScreener in all handler paths Frontend: - Add skeleton loading animation while fetching - Display Market Data card with Price, Liquidity, Volume 24h - Add DexScreener link to pair page - Format helpers for USD and price display --- docs/index.html | 88 +++++++++++++++++++++++++++++++++++- src/api/handlers.rs | 67 +++++++++++++++++++++++++-- src/api/types.rs | 19 ++++++++ src/providers/dexscreener.rs | 13 ++++++ 4 files changed, 181 insertions(+), 6 deletions(-) diff --git a/docs/index.html b/docs/index.html index 7e47798..dec72f4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -63,6 +63,13 @@ .risk-bar { height: 8px; border-radius: 4px; background: #21262d; overflow: hidden; } .risk-bar-fill { height: 100%; border-radius: 4px; transition: width 1s ease-out; } + + /* Skeleton Loading */ + .skeleton { background: linear-gradient(90deg, #21262d 25%, #30363d 50%, #21262d 75%); background-size: 200% 100%; animation: skeleton-loading 1.5s infinite; border-radius: 4px; } + @keyframes skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } + .skeleton-text { height: 16px; margin-bottom: 8px; } + .skeleton-title { height: 24px; width: 60%; margin-bottom: 12px; } + .skeleton-box { height: 80px; margin-bottom: 12px; } @@ -688,6 +695,53 @@

Powered By

return { color: '#ef4444', level: 'CRITICAL', bg: 'bg-neon-red' }; } + // Show skeleton loading + function showSkeletonLoading() { + const resultsPanel = document.getElementById('resultsPanel'); + resultsPanel.innerHTML = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `; + } + + // Format number helpers + function formatUSD(value) { + if (!value) return 'N/A'; + if (value >= 1000000) return '$' + (value / 1000000).toFixed(2) + 'M'; + if (value >= 1000) return '$' + (value / 1000).toFixed(2) + 'K'; + return '$' + value.toFixed(2); + } + + function formatPrice(price) { + if (!price) return 'N/A'; + const num = parseFloat(price); + if (num < 0.00001) return '$' + num.toExponential(2); + if (num < 1) return '$' + num.toFixed(6); + return '$' + num.toFixed(2); + } + // Main analyze function async function analyzeToken() { const tokenAddress = document.getElementById('tokenAddress').value.trim(); @@ -701,9 +755,10 @@

Powered By

return; } - // Disable button + // Disable button and show skeleton btn.disabled = true; btn.innerHTML = '
Analyzing...'; + showSkeletonLoading(); // Start simulation animation const animationPromise = animateSimulationLog(); @@ -824,7 +879,38 @@

${data.token_symbol || 'Unknown'}

${data.chain_name} ${data.token_address.slice(0, 10)}...${data.token_address.slice(-8)} + ${data.dex_name ? `${data.dex_name}` : ''} + + + + +
+

+ 📊 Market Data + (via DexScreener) +

+
+
+
Price
+
${formatPrice(data.price_usd)}
+
+
+
Liquidity
+
${formatUSD(data.liquidity_usd)}
+
+
+
Volume 24h
+
${formatUSD(data.volume_24h_usd)}
+
+
+ ${data.pair_address ? ` + + ` : ''}
diff --git a/src/api/handlers.rs b/src/api/handlers.rs index ba40eb6..d6aef55 100644 --- a/src/api/handlers.rs +++ b/src/api/handlers.rs @@ -226,11 +226,15 @@ pub async fn check_honeypot( chain_id: req.chain_id, chain_name: crate::providers::dexscreener::DexScreenerClient::chain_id_to_name_pub(req.chain_id).to_string(), best_dex: discovered, - token_name: best.base_token.name, - token_symbol: best.base_token.symbol, + token_name: best.base_token.name.clone(), + token_symbol: best.base_token.symbol.clone(), all_pairs: vec![], has_v2_liquidity: !v3_only, total_pairs: pairs.len(), + // Market data + price_usd: best.price_usd.clone(), + volume_24h_usd: best.volume.as_ref().and_then(|v| v.h24), + pair_address: Some(best.pair_address.clone()), }), v3_only) } _ => { @@ -260,6 +264,17 @@ pub async fn check_honeypot( .unwrap_or_else(|| "Unknown DEX".to_string()); info!("⚠️ Token only available on V3/Velodrome-style DEX: {}", dex_name); + + // Extract market data from detected_info + let (price_usd, liquidity_usd, volume_24h_usd, pair_address) = match &detected_info { + Some(info) => ( + info.price_usd.clone(), + Some(info.best_dex.liquidity_usd), + info.volume_24h_usd, + info.pair_address.clone(), + ), + None => (None, None, None, None), + }; let data = HoneypotCheckData { token_address: req.token_address, @@ -267,10 +282,10 @@ pub async fn check_honeypot( token_symbol: auto_detected_symbol, token_decimals: None, chain_id: effective_chain_id, - chain_name, + chain_name: chain_name.clone(), native_symbol: "ETH".to_string(), is_honeypot: false, - risk_score: 0, + risk_score: 70, // HIGH risk - cannot verify buy_success: false, sell_success: false, buy_tax_percent: 0.0, @@ -278,6 +293,12 @@ pub async fn check_honeypot( total_loss_percent: 0.0, reason: format!("Token only available on {} (V3/Velodrome-style) - not supported yet. Use DEX directly.", dex_name), simulation_latency_ms: start.elapsed().as_millis() as u64, + // DexScreener market data + price_usd, + liquidity_usd, + volume_24h_usd, + dex_name: Some(dex_name), + pair_address, }; return Ok(Json(ApiResponse::success( @@ -317,11 +338,23 @@ pub async fn check_honeypot( // Use auto-detected name/symbol if available, otherwise fetch from RPC let (token_name, token_symbol, token_decimals) = if auto_detected_name.is_some() { - (auto_detected_name, auto_detected_symbol, None) + (auto_detected_name.clone(), auto_detected_symbol.clone(), None) } else { let token_info = detector.fetch_token_info(token).await; (token_info.name, token_info.symbol, token_info.decimals) }; + + // Extract market data from detected_info + let (price_usd, liquidity_usd, volume_24h_usd, dex_name, pair_address) = match &detected_info { + Some(info) => ( + info.price_usd.clone(), + Some(info.best_dex.liquidity_usd), + info.volume_24h_usd, + Some(info.best_dex.dex_name.clone()), + info.pair_address.clone(), + ), + None => (None, None, None, None, None), + }; // Calculate risk score from cached result let risk_score = calculate_risk_score(&cached_result); @@ -343,6 +376,12 @@ pub async fn check_honeypot( total_loss_percent: cached_result.total_loss_percent, reason: format!("{} (cached)", cached_result.reason), simulation_latency_ms: 0, // Instant from cache + // DexScreener market data + price_usd, + liquidity_usd, + volume_24h_usd, + dex_name, + pair_address, }; return Ok(Json(ApiResponse::success( @@ -407,6 +446,18 @@ pub async fn check_honeypot( (token_info.name, token_info.symbol, token_info.decimals) }; + // Extract market data from detected_info + let (price_usd, liquidity_usd, volume_24h_usd, dex_name, pair_address) = match &detected_info { + Some(info) => ( + info.price_usd.clone(), + Some(info.best_dex.liquidity_usd), + info.volume_24h_usd, + Some(info.best_dex.dex_name.clone()), + info.pair_address.clone(), + ), + None => (None, None, None, None, None), + }; + // Calculate risk score based on actual simulation results let risk_score = calculate_risk_score(&hp_result); @@ -443,6 +494,12 @@ pub async fn check_honeypot( total_loss_percent: hp_result.total_loss_percent, reason: hp_result.reason, simulation_latency_ms: hp_result.latency_ms, + // DexScreener market data + price_usd, + liquidity_usd, + volume_24h_usd, + dex_name, + pair_address, }; Ok(Json(ApiResponse::success( diff --git a/src/api/types.rs b/src/api/types.rs index 89b4a9a..3be238c 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -217,6 +217,25 @@ pub struct HoneypotCheckData { pub total_loss_percent: f64, pub reason: String, pub simulation_latency_ms: u64, + + // ============================================ + // DexScreener Market Data (NEW!) + // ============================================ + /// Price in USD from DexScreener + #[serde(skip_serializing_if = "Option::is_none")] + pub price_usd: Option, + /// Liquidity in USD + #[serde(skip_serializing_if = "Option::is_none")] + pub liquidity_usd: Option, + /// 24h trading volume in USD + #[serde(skip_serializing_if = "Option::is_none")] + pub volume_24h_usd: Option, + /// DEX name where token trades + #[serde(skip_serializing_if = "Option::is_none")] + pub dex_name: Option, + /// Pair address on DEX + #[serde(skip_serializing_if = "Option::is_none")] + pub pair_address: Option, } // ============================================ diff --git a/src/providers/dexscreener.rs b/src/providers/dexscreener.rs index 2059830..0a4e110 100644 --- a/src/providers/dexscreener.rs +++ b/src/providers/dexscreener.rs @@ -225,6 +225,10 @@ impl DexScreenerClient { // Add info about V3-only tokens has_v2_liquidity: v2_pairs_count > 0, total_pairs: all_pairs_count, + // Market data from DexScreener + price_usd: best_pair.price_usd.clone(), + volume_24h_usd: best_pair.volume.as_ref().and_then(|v| v.h24), + pair_address: Some(best_pair.pair_address.clone()), }) } @@ -280,6 +284,15 @@ pub struct AutoDetectedToken { pub has_v2_liquidity: bool, /// Total number of pairs (including V3) pub total_pairs: usize, + // ============================================ + // Market Data from DexScreener (NEW!) + // ============================================ + /// Price in USD + pub price_usd: Option, + /// 24h trading volume + pub volume_24h_usd: Option, + /// Pair address + pub pair_address: Option, } impl DexPair {