"""Domain valuation service.""" import logging import re from datetime import datetime from typing import Optional, Dict, Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.portfolio import DomainValuation from app.models.tld_price import TLDPrice logger = logging.getLogger(__name__) # TLD value multipliers (higher = more valuable) TLD_VALUES = { # Premium TLDs "com": 1.0, "net": 0.7, "org": 0.65, "io": 0.8, "ai": 1.2, "co": 0.6, # Tech TLDs "dev": 0.5, "app": 0.5, "tech": 0.4, "software": 0.3, # Country codes "de": 0.5, "uk": 0.5, "ch": 0.45, "fr": 0.4, "eu": 0.35, # New gTLDs "xyz": 0.15, "online": 0.2, "site": 0.2, "store": 0.25, "shop": 0.25, # Default "_default": 0.2, } # Common high-value keywords HIGH_VALUE_KEYWORDS = { "crypto", "bitcoin", "btc", "eth", "nft", "web3", "defi", "ai", "ml", "gpt", "chat", "bot", "cloud", "saas", "api", "app", "tech", "finance", "fintech", "bank", "pay", "money", "health", "med", "care", "fit", "game", "gaming", "play", "esport", "shop", "buy", "sell", "deal", "store", "travel", "trip", "hotel", "fly", "food", "eat", "chef", "recipe", "auto", "car", "drive", "ev", "home", "house", "real", "estate", } class DomainValuationService: """ Service for estimating domain values. Uses a multi-factor algorithm considering: - Domain length - TLD value - Keyword relevance - Brandability - Character composition """ def __init__(self): self.base_value = 10 # Base value in USD async def estimate_value( self, domain: str, db: Optional[AsyncSession] = None, save_result: bool = True, ) -> Dict[str, Any]: """ Estimate the value of a domain. Args: domain: The domain name (e.g., "example.com") db: Database session (optional, for saving results) save_result: Whether to save the valuation to database Returns: Dictionary with valuation details """ domain = domain.lower().strip() # Split domain and TLD parts = domain.rsplit(".", 1) if len(parts) != 2: return {"error": "Invalid domain format"} name, tld = parts # Calculate scores length_score = self._calculate_length_score(name) tld_score = self._calculate_tld_score(tld) keyword_score = self._calculate_keyword_score(name) brandability_score = self._calculate_brandability_score(name) # Calculate base value # Formula: base * length_mult * tld_mult * (1 + keyword_bonus + brand_bonus) length_mult = length_score / 50 # 0.0 - 2.0 tld_mult = TLD_VALUES.get(tld, TLD_VALUES["_default"]) keyword_bonus = keyword_score / 200 # 0.0 - 0.5 brand_bonus = brandability_score / 200 # 0.0 - 0.5 # Short premium domains get exponential boost if len(name) <= 3: length_mult *= 5 elif len(name) <= 4: length_mult *= 3 elif len(name) <= 5: length_mult *= 2 # Calculate estimated value estimated_value = self.base_value * length_mult * tld_mult * (1 + keyword_bonus + brand_bonus) # Apply caps estimated_value = max(5, min(estimated_value, 1000000)) # $5 - $1M # Round to reasonable precision if estimated_value < 100: estimated_value = round(estimated_value) elif estimated_value < 1000: estimated_value = round(estimated_value / 10) * 10 elif estimated_value < 10000: estimated_value = round(estimated_value / 100) * 100 else: estimated_value = round(estimated_value / 1000) * 1000 result = { "domain": domain, "estimated_value": estimated_value, "currency": "USD", "scores": { "length": length_score, "tld": tld_score, "keyword": keyword_score, "brandability": brandability_score, "overall": round((length_score + tld_score + keyword_score + brandability_score) / 4), }, "factors": { "length": len(name), "tld": tld, "has_numbers": bool(re.search(r"\d", name)), "has_hyphens": "-" in name, "is_dictionary_word": self._is_common_word(name), }, "confidence": self._calculate_confidence(length_score, tld_score, keyword_score, brandability_score), "source": "internal", "calculated_at": datetime.utcnow().isoformat(), } # Save to database if requested if save_result and db: try: valuation = DomainValuation( domain=domain, estimated_value=estimated_value, length_score=length_score, tld_score=tld_score, keyword_score=keyword_score, brandability_score=brandability_score, source="internal", ) db.add(valuation) await db.commit() except Exception as e: logger.error(f"Failed to save valuation: {e}") return result def _calculate_length_score(self, name: str) -> int: """Calculate score based on domain length (shorter = better).""" length = len(name) if length <= 2: return 100 elif length <= 3: return 95 elif length <= 4: return 85 elif length <= 5: return 75 elif length <= 6: return 65 elif length <= 7: return 55 elif length <= 8: return 45 elif length <= 10: return 35 elif length <= 15: return 25 elif length <= 20: return 15 else: return 5 def _calculate_tld_score(self, tld: str) -> int: """Calculate score based on TLD value.""" value = TLD_VALUES.get(tld, TLD_VALUES["_default"]) return int(value * 100) def _calculate_keyword_score(self, name: str) -> int: """Calculate score based on keyword value.""" name_lower = name.lower() score = 0 # Check for high-value keywords for keyword in HIGH_VALUE_KEYWORDS: if keyword in name_lower: score += 30 break # Bonus for exact keyword match if name_lower in HIGH_VALUE_KEYWORDS: score += 50 # Penalty for numbers if re.search(r"\d", name): score -= 20 # Penalty for hyphens if "-" in name: score -= 30 # Bonus for being a common word if self._is_common_word(name): score += 40 return max(0, min(100, score)) def _calculate_brandability_score(self, name: str) -> int: """Calculate brandability score.""" score = 50 # Start neutral # Bonus for pronounceable names if self._is_pronounceable(name): score += 20 # Bonus for memorable length if 4 <= len(name) <= 8: score += 15 # Penalty for hard-to-spell patterns if re.search(r"(.)\1{2,}", name): # Triple letters score -= 10 # Penalty for confusing patterns if re.search(r"[0oO][1lI]|[1lI][0oO]", name): # 0/O or 1/l confusion score -= 15 # Bonus for all letters if name.isalpha(): score += 10 # Penalty for too many consonants in a row if re.search(r"[bcdfghjklmnpqrstvwxyz]{5,}", name.lower()): score -= 15 return max(0, min(100, score)) def _is_pronounceable(self, name: str) -> bool: """Check if a name is likely pronounceable.""" vowels = set("aeiou") name_lower = name.lower() # Must have at least one vowel if not any(c in vowels for c in name_lower): return False # Check vowel distribution vowel_count = sum(1 for c in name_lower if c in vowels) vowel_ratio = vowel_count / len(name) if name else 0 return 0.2 <= vowel_ratio <= 0.6 def _is_common_word(self, name: str) -> bool: """Check if name is a common English word.""" # Simplified check - in production, use a dictionary API common_words = { "app", "web", "net", "dev", "code", "tech", "data", "cloud", "shop", "store", "buy", "sell", "pay", "cash", "money", "game", "play", "fun", "cool", "best", "top", "pro", "home", "life", "love", "care", "help", "work", "job", "news", "blog", "post", "chat", "talk", "meet", "link", "fast", "quick", "smart", "easy", "simple", "free", } return name.lower() in common_words def _calculate_confidence(self, *scores: int) -> str: """Calculate confidence level based on score consistency.""" avg = sum(scores) / len(scores) variance = sum((s - avg) ** 2 for s in scores) / len(scores) if variance < 100 and avg > 60: return "high" elif variance < 200 and avg > 40: return "medium" else: return "low" async def get_historical_valuations( self, domain: str, db: AsyncSession, limit: int = 10, ) -> list: """Get historical valuations for a domain.""" result = await db.execute( select(DomainValuation) .where(DomainValuation.domain == domain.lower()) .order_by(DomainValuation.created_at.desc()) .limit(limit) ) return result.scalars().all() # Singleton instance valuation_service = DomainValuationService()