""" Smart Pounce - Domain Auction Aggregator This module aggregates domain auctions from multiple platforms: - GoDaddy Auctions - Sedo - NameJet - SnapNames - DropCatch IMPORTANT: This is a META-SEARCH feature. We don't host our own auctions - we aggregate and display auctions from other platforms, earning affiliate commissions on clicks. Legal Note (Switzerland): - No escrow/payment handling = no GwG/FINMA requirements - Users click through to external platforms - We only provide market intelligence VALUATION: All estimated values are calculated using our transparent algorithm: Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor See /api/v1/portfolio/valuation/{domain} for full calculation details. """ import logging from datetime import datetime, timedelta from typing import Optional, List from fastapi import APIRouter, Depends, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.api.deps import get_current_user, get_current_user_optional from app.models.user import User from app.services.valuation import valuation_service logger = logging.getLogger(__name__) router = APIRouter() # ============== Schemas ============== class AuctionValuation(BaseModel): """Valuation details for an auction.""" estimated_value: float value_ratio: float # estimated_value / current_bid potential_profit: float # estimated_value - current_bid confidence: str valuation_formula: str class AuctionListing(BaseModel): """A domain auction listing from any platform.""" domain: str platform: str platform_url: str current_bid: float currency: str num_bids: int end_time: datetime time_remaining: str buy_now_price: Optional[float] = None reserve_met: Optional[bool] = None traffic: Optional[int] = None age_years: Optional[int] = None tld: str affiliate_url: str # Valuation valuation: Optional[AuctionValuation] = None class AuctionSearchResponse(BaseModel): """Response for auction search.""" auctions: List[AuctionListing] total: int platforms_searched: List[str] last_updated: datetime valuation_note: str = ( "Values are estimated using our algorithm: " "$50 × Length × TLD × Keyword × Brand factors. " "See /portfolio/valuation/{domain} for detailed breakdown." ) class PlatformStats(BaseModel): """Statistics for an auction platform.""" platform: str active_auctions: int avg_bid: float ending_soon: int # ============== Mock Data (for demo - replace with real scrapers) ============== MOCK_AUCTIONS = [ { "domain": "cryptopay.io", "platform": "GoDaddy", "current_bid": 2500, "num_bids": 23, "end_time": datetime.utcnow() + timedelta(hours=2, minutes=34), "buy_now_price": 15000, "reserve_met": True, "traffic": 1200, "age_years": 8, }, { "domain": "aitools.com", "platform": "Sedo", "current_bid": 8750, "num_bids": 45, "end_time": datetime.utcnow() + timedelta(hours=5, minutes=12), "buy_now_price": None, "reserve_met": True, "traffic": 3400, "age_years": 12, }, { "domain": "nftmarket.co", "platform": "NameJet", "current_bid": 850, "num_bids": 12, "end_time": datetime.utcnow() + timedelta(hours=1, minutes=5), "buy_now_price": 5000, "reserve_met": False, "traffic": 500, "age_years": 4, }, { "domain": "cloudservices.net", "platform": "GoDaddy", "current_bid": 1200, "num_bids": 8, "end_time": datetime.utcnow() + timedelta(hours=12, minutes=45), "buy_now_price": 7500, "reserve_met": True, "traffic": 800, "age_years": 15, }, { "domain": "blockchain.tech", "platform": "Sedo", "current_bid": 3200, "num_bids": 31, "end_time": datetime.utcnow() + timedelta(hours=0, minutes=45), "buy_now_price": None, "reserve_met": True, "traffic": 2100, "age_years": 6, }, { "domain": "startupfund.io", "platform": "NameJet", "current_bid": 650, "num_bids": 5, "end_time": datetime.utcnow() + timedelta(hours=8, minutes=20), "buy_now_price": 3000, "reserve_met": False, "traffic": 150, "age_years": 3, }, { "domain": "metaverse.ai", "platform": "GoDaddy", "current_bid": 12500, "num_bids": 67, "end_time": datetime.utcnow() + timedelta(hours=3, minutes=15), "buy_now_price": 50000, "reserve_met": True, "traffic": 5000, "age_years": 2, }, { "domain": "defiswap.com", "platform": "Sedo", "current_bid": 4500, "num_bids": 28, "end_time": datetime.utcnow() + timedelta(hours=6, minutes=30), "buy_now_price": 20000, "reserve_met": True, "traffic": 1800, "age_years": 5, }, { "domain": "healthtech.app", "platform": "DropCatch", "current_bid": 420, "num_bids": 7, "end_time": datetime.utcnow() + timedelta(hours=0, minutes=15), "buy_now_price": None, "reserve_met": None, "traffic": 300, "age_years": 2, }, { "domain": "gameverse.io", "platform": "SnapNames", "current_bid": 1100, "num_bids": 15, "end_time": datetime.utcnow() + timedelta(hours=4, minutes=0), "buy_now_price": 5500, "reserve_met": True, "traffic": 900, "age_years": 4, }, ] # Platform affiliate URLs PLATFORM_URLS = { "GoDaddy": "https://auctions.godaddy.com/trpItemListing.aspx?miession=&domain=", "Sedo": "https://sedo.com/search/?keyword=", "NameJet": "https://www.namejet.com/Pages/Auctions/BackorderSearch.aspx?q=", "SnapNames": "https://www.snapnames.com/results.aspx?q=", "DropCatch": "https://www.dropcatch.com/domain/", } def _format_time_remaining(end_time: datetime) -> str: """Format time remaining in human-readable format.""" delta = end_time - datetime.utcnow() if delta.total_seconds() <= 0: return "Ended" hours = int(delta.total_seconds() // 3600) minutes = int((delta.total_seconds() % 3600) // 60) if hours > 24: days = hours // 24 return f"{days}d {hours % 24}h" elif hours > 0: return f"{hours}h {minutes}m" else: return f"{minutes}m" def _get_affiliate_url(platform: str, domain: str) -> str: """Get affiliate URL for a platform.""" base_url = PLATFORM_URLS.get(platform, "") return f"{base_url}{domain}" async def _convert_to_listing(auction: dict, db: AsyncSession, include_valuation: bool = True) -> AuctionListing: """Convert raw auction data to AuctionListing with valuation.""" domain = auction["domain"] tld = domain.rsplit(".", 1)[-1] if "." in domain else "" current_bid = auction["current_bid"] valuation_data = None if include_valuation: try: # Get real valuation from our service result = await valuation_service.estimate_value(domain, db, save_result=False) if "error" not in result: estimated_value = result["estimated_value"] value_ratio = round(estimated_value / current_bid, 2) if current_bid > 0 else 99 valuation_data = AuctionValuation( estimated_value=estimated_value, value_ratio=value_ratio, potential_profit=round(estimated_value - current_bid, 2), confidence=result.get("confidence", "medium"), valuation_formula=result.get("calculation", {}).get("formula", "N/A"), ) except Exception as e: logger.error(f"Valuation error for {domain}: {e}") return AuctionListing( domain=domain, platform=auction["platform"], platform_url=PLATFORM_URLS.get(auction["platform"], ""), current_bid=current_bid, currency="USD", num_bids=auction["num_bids"], end_time=auction["end_time"], time_remaining=_format_time_remaining(auction["end_time"]), buy_now_price=auction.get("buy_now_price"), reserve_met=auction.get("reserve_met"), traffic=auction.get("traffic"), age_years=auction.get("age_years"), tld=tld, affiliate_url=_get_affiliate_url(auction["platform"], domain), valuation=valuation_data, ) # ============== Endpoints ============== @router.get("", response_model=AuctionSearchResponse) async def search_auctions( keyword: Optional[str] = Query(None, description="Search keyword in domain names"), tld: Optional[str] = Query(None, description="Filter by TLD (e.g., 'com', 'io')"), platform: Optional[str] = Query(None, description="Filter by platform"), min_bid: Optional[float] = Query(None, ge=0, description="Minimum current bid"), max_bid: Optional[float] = Query(None, ge=0, description="Maximum current bid"), ending_soon: bool = Query(False, description="Only show auctions ending in < 1 hour"), sort_by: str = Query("ending", enum=["ending", "bid_asc", "bid_desc", "bids", "value_ratio"]), limit: int = Query(20, le=100), offset: int = Query(0, ge=0), current_user: Optional[User] = Depends(get_current_user_optional), db: AsyncSession = Depends(get_db), ): """ Search domain auctions across multiple platforms. This is a META-SEARCH feature: - We aggregate listings from GoDaddy, Sedo, NameJet, etc. - Clicking through uses affiliate links - We do NOT handle payments or transfers All auctions include estimated values calculated using our algorithm: Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor Smart Pounce Strategy: - Look for value_ratio > 1.0 (estimated value exceeds current bid) - Focus on auctions ending soon with low bid counts - Track keywords you're interested in """ auctions = MOCK_AUCTIONS.copy() # Apply filters if keyword: keyword_lower = keyword.lower() auctions = [a for a in auctions if keyword_lower in a["domain"].lower()] if tld: tld_clean = tld.lower().lstrip(".") auctions = [a for a in auctions if a["domain"].endswith(f".{tld_clean}")] if platform: auctions = [a for a in auctions if a["platform"].lower() == platform.lower()] if min_bid is not None: auctions = [a for a in auctions if a["current_bid"] >= min_bid] if max_bid is not None: auctions = [a for a in auctions if a["current_bid"] <= max_bid] if ending_soon: cutoff = datetime.utcnow() + timedelta(hours=1) auctions = [a for a in auctions if a["end_time"] <= cutoff] # Sort (before valuation for efficiency, except value_ratio) if sort_by == "ending": auctions.sort(key=lambda x: x["end_time"]) elif sort_by == "bid_asc": auctions.sort(key=lambda x: x["current_bid"]) elif sort_by == "bid_desc": auctions.sort(key=lambda x: x["current_bid"], reverse=True) elif sort_by == "bids": auctions.sort(key=lambda x: x["num_bids"], reverse=True) total = len(auctions) auctions = auctions[offset:offset + limit] # Convert to response format with valuations listings = [] for a in auctions: listing = await _convert_to_listing(a, db, include_valuation=True) listings.append(listing) # Sort by value_ratio if requested (after valuation) if sort_by == "value_ratio": listings.sort( key=lambda x: x.valuation.value_ratio if x.valuation else 0, reverse=True ) return AuctionSearchResponse( auctions=listings, total=total, platforms_searched=list(PLATFORM_URLS.keys()), last_updated=datetime.utcnow(), ) @router.get("/ending-soon", response_model=List[AuctionListing]) async def get_ending_soon( hours: int = Query(1, ge=1, le=24, description="Hours until end"), limit: int = Query(10, le=50), current_user: Optional[User] = Depends(get_current_user_optional), db: AsyncSession = Depends(get_db), ): """ Get auctions ending soon - best opportunities for sniping. Smart Pounce Tip: - Auctions ending in < 1 hour often have final bidding frenzy - Low-bid auctions ending soon can be bargains - Look for value_ratio > 1.0 (undervalued domains) """ cutoff = datetime.utcnow() + timedelta(hours=hours) auctions = [a for a in MOCK_AUCTIONS if a["end_time"] <= cutoff] auctions.sort(key=lambda x: x["end_time"]) auctions = auctions[:limit] listings = [] for a in auctions: listing = await _convert_to_listing(a, db, include_valuation=True) listings.append(listing) return listings @router.get("/hot", response_model=List[AuctionListing]) async def get_hot_auctions( limit: int = Query(10, le=50), current_user: Optional[User] = Depends(get_current_user_optional), db: AsyncSession = Depends(get_db), ): """ Get hottest auctions by bidding activity. These auctions have the most competition - high demand indicators. High demand often correlates with quality domains. """ auctions = sorted(MOCK_AUCTIONS, key=lambda x: x["num_bids"], reverse=True) auctions = auctions[:limit] listings = [] for a in auctions: listing = await _convert_to_listing(a, db, include_valuation=True) listings.append(listing) return listings @router.get("/stats", response_model=List[PlatformStats]) async def get_platform_stats( current_user: Optional[User] = Depends(get_current_user_optional), ): """ Get statistics for each auction platform. Useful for understanding where the best deals are. """ stats = {} for auction in MOCK_AUCTIONS: platform = auction["platform"] if platform not in stats: stats[platform] = { "platform": platform, "auctions": [], "ending_soon_count": 0, } stats[platform]["auctions"].append(auction) if auction["end_time"] <= datetime.utcnow() + timedelta(hours=1): stats[platform]["ending_soon_count"] += 1 result = [] for platform, data in stats.items(): auctions = data["auctions"] result.append(PlatformStats( platform=platform, active_auctions=len(auctions), avg_bid=round(sum(a["current_bid"] for a in auctions) / len(auctions), 2), ending_soon=data["ending_soon_count"], )) return sorted(result, key=lambda x: x.active_auctions, reverse=True) @router.get("/opportunities") async def get_smart_opportunities( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """ Smart Pounce Algorithm - Find the best auction opportunities. Our algorithm scores each auction based on: 1. Value Ratio: estimated_value / current_bid (higher = better deal) 2. Time Factor: Auctions ending soon get 2× boost 3. Bid Factor: Low bid count (< 10) gets 1.5× boost Opportunity Score = value_ratio × time_factor × bid_factor Recommendations: - "Strong buy": Score > 5 (significantly undervalued) - "Consider": Score 2-5 (potential opportunity) - "Monitor": Score < 2 (fairly priced) Requires authentication to personalize results. """ opportunities = [] for auction in MOCK_AUCTIONS: valuation = await valuation_service.estimate_value(auction["domain"], db, save_result=False) if "error" in valuation: continue estimated_value = valuation["estimated_value"] current_bid = auction["current_bid"] value_ratio = estimated_value / current_bid if current_bid > 0 else 10 hours_left = (auction["end_time"] - datetime.utcnow()).total_seconds() / 3600 time_factor = 2.0 if hours_left < 1 else (1.5 if hours_left < 4 else 1.0) bid_factor = 1.5 if auction["num_bids"] < 10 else 1.0 opportunity_score = value_ratio * time_factor * bid_factor listing = await _convert_to_listing(auction, db, include_valuation=True) opportunities.append({ "auction": listing.model_dump(), "analysis": { "estimated_value": estimated_value, "current_bid": current_bid, "value_ratio": round(value_ratio, 2), "potential_profit": round(estimated_value - current_bid, 2), "opportunity_score": round(opportunity_score, 2), "time_factor": time_factor, "bid_factor": bid_factor, "recommendation": ( "Strong buy" if opportunity_score > 5 else "Consider" if opportunity_score > 2 else "Monitor" ), "reasoning": _get_opportunity_reasoning( value_ratio, hours_left, auction["num_bids"], opportunity_score ), } }) opportunities.sort(key=lambda x: x["analysis"]["opportunity_score"], reverse=True) return { "opportunities": opportunities[:10], "valuation_method": ( "Our algorithm calculates: $50 × Length × TLD × Keyword × Brand factors. " "See /portfolio/valuation/{domain} for detailed breakdown of any domain." ), "strategy_tips": [ "🎯 Focus on value_ratio > 1.0 (estimated value exceeds current bid)", "⏰ Auctions ending in < 1 hour often have best snipe opportunities", "📉 Low bid count (< 10) might indicate overlooked gems", "💡 Premium TLDs (.com, .ai, .io) have highest aftermarket demand", ], "generated_at": datetime.utcnow().isoformat(), } def _get_opportunity_reasoning(value_ratio: float, hours_left: float, num_bids: int, score: float) -> str: """Generate human-readable reasoning for the opportunity.""" reasons = [] if value_ratio > 2: reasons.append(f"Significantly undervalued ({value_ratio:.1f}× estimated value)") elif value_ratio > 1: reasons.append(f"Undervalued ({value_ratio:.1f}× estimated value)") else: reasons.append(f"Current bid exceeds our estimate ({value_ratio:.2f}×)") if hours_left < 1: reasons.append("⚡ Ending very soon - final chance to bid") elif hours_left < 4: reasons.append("⏰ Ending soon - limited time remaining") if num_bids < 5: reasons.append("📉 Very low competition - potential overlooked opportunity") elif num_bids < 10: reasons.append("📊 Moderate competition") else: reasons.append(f"🔥 High demand ({num_bids} bids)") return " | ".join(reasons)