snm_brightdata_client/tools/
crypto.rs

1// src/tools/crypto.rs - Enhanced with priority-aware filtering and token budget management
2use crate::tool::{Tool, ToolResult, McpContent};
3use crate::error::BrightDataError;
4use crate::extras::logger::JSON_LOGGER;
5use crate::filters::{ResponseFilter, ResponseStrategy, ResponseType};
6use async_trait::async_trait;
7use reqwest::Client;
8use serde_json::{json, Value};
9use std::env;
10use std::time::Duration;
11use std::collections::HashMap;
12
13pub struct CryptoDataTool;
14
15#[async_trait]
16impl Tool for CryptoDataTool {
17    fn name(&self) -> &str {
18        "get_crypto_data"
19    }
20
21    fn description(&self) -> &str {
22        "Get cryptocurrency data with enhanced search parameters including prices, market cap, trading volumes with intelligent filtering"
23    }
24
25    fn input_schema(&self) -> Value {
26        json!({
27            "type": "object",
28            "properties": {
29                "query": {
30                    "type": "string",
31                    "description": "Crypto symbol (BTC, ETH, ADA), crypto name (Bitcoin, Ethereum), comparison query (BTC vs ETH), or market overview (crypto market today, top cryptocurrencies)"
32                },
33                "page": {
34                    "type": "integer",
35                    "description": "Page number for pagination (1-based)",
36                    "minimum": 1,
37                    "default": 1
38                },
39                "num_results": {
40                    "type": "integer",
41                    "description": "Number of results per page (5-50)",
42                    "minimum": 5,
43                    "maximum": 50,
44                    "default": 20
45                },
46                "time_filter": {
47                    "type": "string",
48                    "enum": ["any", "hour", "day", "week", "month", "year"],
49                    "description": "Time-based filter for crypto data",
50                    "default": "day"
51                },
52                "crypto_type": {
53                    "type": "string",
54                    "enum": ["any", "bitcoin", "altcoins", "defi", "nft", "stablecoins"],
55                    "description": "Type of cryptocurrency to focus on",
56                    "default": "any"
57                },
58                "data_points": {
59                    "type": "array",
60                    "items": {
61                        "type": "string",
62                        "enum": ["price", "market_cap", "volume", "change", "supply", "dominance", "fear_greed"]
63                    },
64                    "description": "Specific data points to focus on",
65                    "default": ["price", "market_cap", "volume"]
66                },
67                "safe_search": {
68                    "type": "string",
69                    "enum": ["off", "moderate", "strict"],
70                    "description": "Safe search filter level",
71                    "default": "moderate"
72                },
73                "use_serp_api": {
74                    "type": "boolean",
75                    "description": "Use enhanced SERP API with advanced parameters",
76                    "default": true
77                }
78            },
79            "required": ["query"]
80        })
81    }
82
83    // FIXED: Remove the execute method override to use the default one with metrics logging
84    // async fn execute(&self, parameters: Value) -> Result<ToolResult, BrightDataError> {
85    //     self.execute_internal(parameters).await
86    // }
87
88    async fn execute_internal(&self, parameters: Value) -> Result<ToolResult, BrightDataError> {
89        let query = parameters
90            .get("query")
91            .and_then(|v| v.as_str())
92            .ok_or_else(|| BrightDataError::ToolError("Missing 'query' parameter".into()))?;
93
94        let page = parameters
95            .get("page")
96            .and_then(|v| v.as_i64())
97            .unwrap_or(1) as u32;
98
99        let num_results = parameters
100            .get("num_results")
101            .and_then(|v| v.as_i64())
102            .unwrap_or(20) as u32;
103
104        let time_filter = parameters
105            .get("time_filter")
106            .and_then(|v| v.as_str())
107            .unwrap_or("day");
108
109        let crypto_type = parameters
110            .get("crypto_type")
111            .and_then(|v| v.as_str())
112            .unwrap_or("any");
113
114        let data_points = parameters
115            .get("data_points")
116            .and_then(|v| v.as_array())
117            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
118            .unwrap_or_else(|| vec!["price", "market_cap", "volume"]);
119
120        let safe_search = parameters
121            .get("safe_search")
122            .and_then(|v| v.as_str())
123            .unwrap_or("moderate");
124
125        let use_serp_api = parameters
126            .get("use_serp_api")
127            .and_then(|v| v.as_bool())
128            .unwrap_or(true);
129
130        // ENHANCED: Priority classification and token allocation
131        let query_priority = ResponseStrategy::classify_query_priority(query);
132        let recommended_tokens = ResponseStrategy::get_recommended_token_allocation(query);
133
134        // Early validation using strategy only if TRUNCATE_FILTER is enabled
135        if std::env::var("TRUNCATE_FILTER")
136            .map(|v| v.to_lowercase() == "true")
137            .unwrap_or(false) {
138            
139            // Budget check for crypto queries
140            let (_, remaining_tokens) = ResponseStrategy::get_token_budget_status();
141            if remaining_tokens < 150 && !matches!(query_priority, crate::filters::strategy::QueryPriority::Critical) {
142                return Ok(ResponseStrategy::create_response("", query, "crypto", "budget_limit", json!({}), ResponseType::Skip));
143            }
144        }
145
146        let execution_id = format!("crypto_{}", chrono::Utc::now().format("%Y%m%d_%H%M%S%.3f"));
147        
148        let result = if use_serp_api {
149            self.fetch_crypto_data_enhanced_with_priority(
150                query, page, num_results, time_filter, crypto_type, &data_points, 
151                safe_search, query_priority, recommended_tokens, &execution_id
152            ).await?
153        } else {
154            self.fetch_crypto_data_legacy_with_priority(query, query_priority, recommended_tokens, &execution_id).await?
155        };
156
157        let content = result.get("content").and_then(|c| c.as_str()).unwrap_or("");
158        let source_used = if use_serp_api { "Enhanced SERP" } else { "Legacy" };
159        
160        // Create appropriate response based on whether filtering is enabled
161        let tool_result = if std::env::var("TRUNCATE_FILTER")
162            .map(|v| v.to_lowercase() == "true")
163            .unwrap_or(false) {
164            
165            ResponseStrategy::create_financial_response(
166                "crypto", query, "crypto", source_used, content, result.clone()
167            )
168        } else {
169            // No filtering - create standard response
170            let content_text = if use_serp_api {
171                result.get("formatted_content").and_then(|c| c.as_str()).unwrap_or(content)
172            } else {
173                content
174            };
175
176            let mcp_content = vec![McpContent::text(format!(
177                "💰 **Enhanced Crypto Data for {}**\n\nCrypto Type: {} | Priority: {:?} | Tokens: {} | Data Points: {:?}\nPage: {} | Results: {} | Time Filter: {} | Safe Search: {}\nExecution ID: {}\n\n{}",
178                query, crypto_type, query_priority, recommended_tokens, data_points, page, num_results, time_filter, safe_search, execution_id, content_text
179            ))];
180            ToolResult::success_with_raw(mcp_content, result)
181        };
182
183        // Apply size limits only if filtering enabled
184        if std::env::var("TRUNCATE_FILTER")
185            .map(|v| v.to_lowercase() == "true")
186            .unwrap_or(false) {
187            Ok(ResponseStrategy::apply_size_limits(tool_result))
188        } else {
189            Ok(tool_result)
190        }
191    }
192}
193
194impl CryptoDataTool {
195    // ENHANCED: Priority-aware crypto data fetching
196    async fn fetch_crypto_data_enhanced_with_priority(
197        &self, 
198        query: &str, 
199        page: u32,
200        num_results: u32,
201        time_filter: &str,
202        crypto_type: &str,
203        data_points: &[&str],
204        safe_search: &str,
205        query_priority: crate::filters::strategy::QueryPriority,
206        token_budget: usize,
207        execution_id: &str
208    ) -> Result<Value, BrightDataError> {
209        let api_token = env::var("BRIGHTDATA_API_TOKEN")
210            .or_else(|_| env::var("API_TOKEN"))
211            .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
212
213        let base_url = env::var("BRIGHTDATA_BASE_URL")
214            .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
215
216        let zone = env::var("BRIGHTDATA_SERP_ZONE")
217            .unwrap_or_else(|_| "serp_api2".to_string());
218
219        // Build enhanced search query with priority awareness
220        let search_query = self.build_priority_aware_crypto_query(query, crypto_type, data_points, query_priority);
221        
222        // ENHANCED: Adjust results based on priority and token budget
223        let effective_num_results = match query_priority {
224            crate::filters::strategy::QueryPriority::Critical => num_results,
225            crate::filters::strategy::QueryPriority::High => std::cmp::min(num_results, 25),
226            crate::filters::strategy::QueryPriority::Medium => std::cmp::min(num_results, 15),
227            crate::filters::strategy::QueryPriority::Low => std::cmp::min(num_results, 10),
228        };
229        
230        // Build enhanced query parameters
231        let mut query_params = HashMap::new();
232        query_params.insert("q".to_string(), search_query.clone());
233        
234        // Pagination
235        if page > 1 {
236            let start = (page - 1) * effective_num_results;
237            query_params.insert("start".to_string(), start.to_string());
238        }
239        query_params.insert("num".to_string(), effective_num_results.to_string());
240        
241        // Global settings for crypto (not region-specific)
242        query_params.insert("gl".to_string(), "us".to_string());
243        query_params.insert("hl".to_string(), "en".to_string());
244        
245        // Safe search
246        let safe_value = match safe_search {
247            "off" => "off",
248            "strict" => "strict",
249            _ => "moderate"
250        };
251        query_params.insert("safe".to_string(), safe_value.to_string());
252        
253        // Time-based filtering (skip for low priority to save tokens)
254        if time_filter != "any" && !matches!(query_priority, crate::filters::strategy::QueryPriority::Low) {
255            let tbs_value = match time_filter {
256                "hour" => "qdr:h",
257                "day" => "qdr:d", 
258                "week" => "qdr:w",
259                "month" => "qdr:m",
260                "year" => "qdr:y",
261                _ => ""
262            };
263            if !tbs_value.is_empty() {
264                query_params.insert("tbs".to_string(), tbs_value.to_string());
265            }
266        }
267
268        // Build URL with query parameters
269        let mut search_url = "https://www.google.com/search".to_string();
270        let query_string = query_params.iter()
271            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
272            .collect::<Vec<_>>()
273            .join("&");
274        
275        if !query_string.is_empty() {
276            search_url = format!("{}?{}", search_url, query_string);
277        }
278
279        let mut payload = json!({
280            "url": search_url,
281            "zone": zone,
282            "format": "raw",
283            "render": true,
284            "data_format": "markdown"
285        });
286
287        // Add priority processing hints
288        if std::env::var("TRUNCATE_FILTER")
289            .map(|v| v.to_lowercase() == "true")
290            .unwrap_or(false) {
291            
292            payload["processing_priority"] = json!(format!("{:?}", query_priority));
293            payload["token_budget"] = json!(token_budget);
294            payload["focus_data_points"] = json!(data_points);
295            payload["crypto_focus"] = json!(crypto_type);
296        }
297
298        let client = Client::builder()
299            .timeout(Duration::from_secs(120))
300            .build()
301            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
302
303        let response = client
304            .post(&format!("{}/request", base_url))
305            .header("Authorization", format!("Bearer {}", api_token))
306            .header("Content-Type", "application/json")
307            .json(&payload)
308            .send()
309            .await
310            .map_err(|e| BrightDataError::ToolError(format!("Enhanced crypto request failed: {}", e)))?;
311
312        let status = response.status().as_u16();
313        let response_headers: HashMap<String, String> = response
314            .headers()
315            .iter()
316            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
317            .collect();
318
319        // Log BrightData request
320        if let Err(e) = JSON_LOGGER.log_brightdata_request(
321            execution_id,
322            &zone,
323            &format!("Enhanced Crypto: {} ({})", search_query, crypto_type),
324            payload.clone(),
325            status,
326            response_headers,
327            "markdown"
328        ).await {
329            log::warn!("Failed to log BrightData request: {}", e);
330        }
331
332        if !response.status().is_success() {
333            let error_text = response.text().await.unwrap_or_default();
334            return Err(BrightDataError::ToolError(format!(
335                "BrightData enhanced crypto error {}: {}",
336                status, error_text
337            )));
338        }
339
340        let raw_content = response.text().await
341            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
342
343        // ENHANCED: Apply priority-based filtering with token awareness
344        let filtered_content = if std::env::var("TRUNCATE_FILTER")
345            .map(|v| v.to_lowercase() == "true")
346            .unwrap_or(false) {
347            
348            if ResponseFilter::is_error_page(&raw_content) {
349                return Err(BrightDataError::ToolError("Enhanced crypto search returned error page".into()));
350            } else if ResponseStrategy::should_try_next_source(&raw_content) {
351                return Err(BrightDataError::ToolError("Content quality too low".into()));
352            } else {
353                // Use enhanced extraction with token budget awareness
354                let max_tokens = token_budget / 3; // Reserve tokens for formatting
355                ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
356            }
357        } else {
358            raw_content.clone()
359        };
360
361        // Format the results with priority awareness
362        let formatted_content = self.format_crypto_results_with_priority(&filtered_content, query, crypto_type, data_points, page, effective_num_results, time_filter, query_priority);
363
364        Ok(json!({
365            "content": filtered_content,
366            "formatted_content": formatted_content,
367            "query": query,
368            "search_query": search_query,
369            "crypto_type": crypto_type,
370            "data_points": data_points,
371            "priority": format!("{:?}", query_priority),
372            "token_budget": token_budget,
373            "page": page,
374            "num_results": effective_num_results,
375            "time_filter": time_filter,
376            "safe_search": safe_search,
377            "zone": zone,
378            "execution_id": execution_id,
379            "raw_response": raw_content,
380            "success": true,
381            "api_type": "enhanced_priority_serp"
382        }))
383    }
384
385    async fn fetch_crypto_data_legacy_with_priority(&self, query: &str, priority: crate::filters::strategy::QueryPriority, token_budget: usize, execution_id: &str) -> Result<Value, BrightDataError> {
386        let api_token = env::var("BRIGHTDATA_API_TOKEN")
387            .or_else(|_| env::var("API_TOKEN"))
388            .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
389
390        let base_url = env::var("BRIGHTDATA_BASE_URL")
391            .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
392
393        let zone = env::var("WEB_UNLOCKER_ZONE")
394            .unwrap_or_else(|_| "default".to_string());
395
396        let search_url = format!(
397            "https://www.google.com/search?q={} cryptocurrency price market cap coinmarketcap",
398            urlencoding::encode(query)
399        );
400
401        let mut payload = json!({
402            "url": search_url,
403            "zone": zone,
404            "format": "raw",
405            "data_format": "markdown"
406        });
407
408        // Add priority processing hints
409        if std::env::var("TRUNCATE_FILTER")
410            .map(|v| v.to_lowercase() == "true")
411            .unwrap_or(false) {
412            
413            payload["processing_priority"] = json!(format!("{:?}", priority));
414            payload["token_budget"] = json!(token_budget);
415        }
416
417        let client = Client::builder()
418            .timeout(Duration::from_secs(120))
419            .build()
420            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
421
422        let response = client
423            .post(&format!("{}/request", base_url))
424            .header("Authorization", format!("Bearer {}", api_token))
425            .header("Content-Type", "application/json")
426            .json(&payload)
427            .send()
428            .await
429            .map_err(|e| BrightDataError::ToolError(format!("Crypto data request failed: {}", e)))?;
430
431        let status = response.status().as_u16();
432        let response_headers: HashMap<String, String> = response
433            .headers()
434            .iter()
435            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
436            .collect();
437
438        // Log BrightData request
439        if let Err(e) = JSON_LOGGER.log_brightdata_request(
440            execution_id,
441            &zone,
442            &search_url,
443            payload.clone(),
444            status,
445            response_headers,
446            "markdown"
447        ).await {
448            log::warn!("Failed to log BrightData request: {}", e);
449        }
450
451        if !response.status().is_success() {
452            let error_text = response.text().await.unwrap_or_default();
453            return Err(BrightDataError::ToolError(format!(
454                "BrightData crypto data error {}: {}",
455                status, error_text
456            )));
457        }
458
459        let raw_content = response.text().await
460            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
461
462        // Apply filters with priority awareness
463        let filtered_content = if std::env::var("TRUNCATE_FILTER")
464            .map(|v| v.to_lowercase() == "true")
465            .unwrap_or(false) {
466            
467            if ResponseFilter::is_error_page(&raw_content) {
468                return Err(BrightDataError::ToolError("Crypto search returned error page".into()));
469            } else {
470                let max_tokens = token_budget / 2;
471                ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
472            }
473        } else {
474            raw_content.clone()
475        };
476
477        Ok(json!({
478            "content": filtered_content,
479            "query": query,
480            "priority": format!("{:?}", priority),
481            "token_budget": token_budget,
482            "execution_id": execution_id,
483            "success": true,
484            "api_type": "legacy_priority"
485        }))
486    }
487
488    // ENHANCED: Priority-aware crypto query building
489    fn build_priority_aware_crypto_query(&self, query: &str, crypto_type: &str, data_points: &[&str], priority: crate::filters::strategy::QueryPriority) -> String {
490        let mut search_terms = vec![query.to_string()];
491        
492        // Add cryptocurrency identifier
493        search_terms.push("cryptocurrency".to_string());
494        
495        // Priority-based term selection
496        match priority {
497            crate::filters::strategy::QueryPriority::Critical => {
498                // Focus on current, real-time data for critical queries
499                search_terms.extend_from_slice(&["price".to_string(), "current".to_string(), "live".to_string()]);
500                for data_point in data_points.iter().take(2) { // Limit data points for focus
501                    match *data_point {
502                        "price" => search_terms.push("price".to_string()),
503                        "market_cap" => search_terms.push("market cap".to_string()),
504                        _ => {}
505                    }
506                }
507            }
508            crate::filters::strategy::QueryPriority::High => {
509                // Include key metrics
510                search_terms.extend_from_slice(&["price".to_string(), "market cap".to_string()]);
511                for data_point in data_points.iter().take(3) {
512                    match *data_point {
513                        "volume" => search_terms.push("volume".to_string()),
514                        "change" => search_terms.push("change".to_string()),
515                        _ => {}
516                    }
517                }
518            }
519            crate::filters::strategy::QueryPriority::Medium => {
520                // Basic financial terms
521                search_terms.extend_from_slice(&["price".to_string(), "market".to_string()]);
522                if crypto_type != "any" {
523                    search_terms.push(crypto_type.to_string());
524                }
525            }
526            crate::filters::strategy::QueryPriority::Low => {
527                // General terms for lower priority
528                if crypto_type != "any" {
529                    search_terms.push(crypto_type.to_string());
530                }
531                search_terms.push("overview".to_string());
532            }
533        }
534        
535        // Add crypto type if specified and priority allows
536        if crypto_type != "any" && !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
537            match crypto_type {
538                "bitcoin" => search_terms.push("bitcoin BTC".to_string()),
539                "altcoins" => search_terms.push("altcoins ethereum".to_string()),
540                "defi" => search_terms.push("DeFi decentralized finance".to_string()),
541                "nft" => search_terms.push("NFT non-fungible token".to_string()),
542                "stablecoins" => search_terms.push("stablecoin USDT USDC".to_string()),
543                _ => {}
544            }
545        }
546        
547        // Add popular crypto data sources (only for high priority to save tokens)
548        if matches!(priority, crate::filters::strategy::QueryPriority::Critical | crate::filters::strategy::QueryPriority::High) {
549            search_terms.extend_from_slice(&[
550                "coinmarketcap".to_string(),
551                "coingecko".to_string()
552            ]);
553        }
554        
555        search_terms.join(" ")
556    }
557
558    // ENHANCED: Priority-aware result formatting
559    fn format_crypto_results_with_priority(&self, content: &str, query: &str, crypto_type: &str, data_points: &[&str], page: u32, num_results: u32, time_filter: &str, _priority: crate::filters::strategy::QueryPriority) -> String {
560        // Check if we need compact formatting
561        if std::env::var("TRUNCATE_FILTER")
562            .map(|v| v.to_lowercase() == "true")
563            .unwrap_or(false) {
564            
565            // Ultra-compact formatting for filtered mode
566            return format!("💰 {}: {}", 
567                ResponseStrategy::ultra_abbreviate_query(query), 
568                content
569            );
570        }
571
572        // Regular formatting for non-filtered mode
573        self.format_crypto_results(content, query, crypto_type, data_points, page, num_results, time_filter)
574    }
575
576    fn format_crypto_results(&self, content: &str, query: &str, crypto_type: &str, data_points: &[&str], page: u32, num_results: u32, time_filter: &str) -> String {
577        let mut formatted = String::new();
578        
579        // Add header with search parameters
580        formatted.push_str(&format!("# Cryptocurrency Data: {}\n\n", query));
581        formatted.push_str(&format!("**Crypto Type**: {} | **Data Points**: {:?} | **Time Filter**: {}\n", 
582            crypto_type, data_points, time_filter));
583        formatted.push_str(&format!("**Page**: {} | **Results**: {}\n\n", page, num_results));
584        
585        // Try to parse JSON response if available
586        if let Ok(json_data) = serde_json::from_str::<Value>(content) {
587            // If we get structured JSON, format it nicely
588            if let Some(results) = json_data.get("organic_results").and_then(|r| r.as_array()) {
589                formatted.push_str("## Cryptocurrency Information\n\n");
590                for (i, result) in results.iter().take(num_results as usize).enumerate() {
591                    let title = result.get("title").and_then(|t| t.as_str()).unwrap_or("No title");
592                    let link = result.get("link").and_then(|l| l.as_str()).unwrap_or("");
593                    let snippet = result.get("snippet").and_then(|s| s.as_str()).unwrap_or("");
594                    
595                    formatted.push_str(&format!("### {}. {}\n", i + 1, title));
596                    if !link.is_empty() {
597                        formatted.push_str(&format!("**Source**: {}\n", link));
598                    }
599                    if !snippet.is_empty() {
600                        formatted.push_str(&format!("**Details**: {}\n", snippet));
601                    }
602                    
603                    // Highlight specific data points if found in snippet
604                    self.highlight_crypto_data_points(&mut formatted, snippet, data_points);
605                    formatted.push_str("\n");
606                }
607            } else {
608                // JSON but no organic_results, return formatted JSON
609                formatted.push_str("## Crypto Data\n\n");
610                formatted.push_str("```json\n");
611                formatted.push_str(&serde_json::to_string_pretty(&json_data).unwrap_or_else(|_| content.to_string()));
612                formatted.push_str("\n```\n");
613            }
614        } else {
615            // Plain text/markdown response
616            formatted.push_str("## Cryptocurrency Information\n\n");
617            formatted.push_str(content);
618        }
619        
620        // Add pagination info
621        if page > 1 || num_results < 100 {
622            formatted.push_str(&format!("\n---\n*Page {} of cryptocurrency results*\n", page));
623            if page > 1 {
624                formatted.push_str("💡 *To get more results, use page parameter*\n");
625            }
626        }
627        
628        formatted
629    }
630
631    fn highlight_crypto_data_points(&self, formatted: &mut String, snippet: &str, data_points: &[&str]) {
632        let snippet_lower = snippet.to_lowercase();
633        let mut found_points = Vec::new();
634        
635        for data_point in data_points {
636            match *data_point {
637                "price" if snippet_lower.contains("price") || snippet_lower.contains("$") || snippet_lower.contains("usd") => {
638                    found_points.push("💰 Price data detected");
639                }
640                "market_cap" if snippet_lower.contains("market cap") || snippet_lower.contains("mcap") => {
641                    found_points.push("📊 Market cap information");
642                }
643                "volume" if snippet_lower.contains("volume") || snippet_lower.contains("traded") => {
644                    found_points.push("📈 Volume data found");
645                }
646                "change" if snippet_lower.contains("%") || snippet_lower.contains("change") || snippet_lower.contains("gain") => {
647                    found_points.push("📊 Price change information");
648                }
649                "supply" if snippet_lower.contains("supply") || snippet_lower.contains("circulation") => {
650                    found_points.push("🪙 Supply data available");
651                }
652                "dominance" if snippet_lower.contains("dominance") => {
653                    found_points.push("👑 Market dominance data");
654                }
655                "fear_greed" if snippet_lower.contains("fear") || snippet_lower.contains("greed") => {
656                    found_points.push("😰 Fear & Greed index");
657                }
658                _ => {}
659            }
660        }
661        
662        if !found_points.is_empty() {
663            formatted.push_str("**Key Data Points**: ");
664            formatted.push_str(&found_points.join(" | "));
665            formatted.push_str("\n");
666        }
667    }
668}