pounce/backend/app/services/valuation.py
yves.gugger 6e84103a5b feat: Complete business model expansion
MAJOR FEATURES:
- New pricing tiers: Scout (Free), Trader (€19/mo), Tycoon (€49/mo)
- Portfolio management: Track owned domains with purchase price, value, ROI
- Domain valuation engine: Algorithmic estimates based on length, TLD, keywords, brandability
- Dashboard tabs: Watchlist + Portfolio views
- Valuation modal: Score breakdown with confidence level

BACKEND:
- New models: PortfolioDomain, DomainValuation
- New API routes: /portfolio/* with full CRUD
- Valuation service with multi-factor algorithm
- Database migration for portfolio tables

FRONTEND:
- Updated pricing page with comparison table and billing toggle
- Dashboard with Watchlist/Portfolio tabs
- Portfolio summary stats: Total value, invested, unrealized P/L, ROI
- Add portfolio domain modal with all fields
- Domain valuation modal with score visualization
- Updated landing page with new tier pricing
- Hero section with large puma logo

DESIGN:
- Consistent minimalist dark theme
- Responsive on all devices
- Professional animations and transitions
2025-12-08 11:08:18 +01:00

335 lines
10 KiB
Python

"""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()