""" 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 """ 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 import httpx import asyncio from app.database import get_db from app.api.deps import get_current_user, get_current_user_optional from app.models.user import User logger = logging.getLogger(__name__) router = APIRouter() # ============== Schemas ============== 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 class AuctionSearchResponse(BaseModel): """Response for auction search.""" auctions: List[AuctionListing] total: int platforms_searched: List[str] last_updated: datetime 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) ============== # In production, these would be real API calls or web 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, "") # In production, add affiliate tracking parameters return f"{base_url}{domain}" def _convert_to_listing(auction: dict) -> AuctionListing: """Convert raw auction data to AuctionListing.""" domain = auction["domain"] tld = domain.rsplit(".", 1)[-1] if "." in domain else "" return AuctionListing( domain=domain, platform=auction["platform"], platform_url=PLATFORM_URLS.get(auction["platform"], ""), current_bid=auction["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), ) # ============== 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"]), limit: int = Query(20, le=100), offset: int = Query(0, ge=0), current_user: Optional[User] = Depends(get_current_user_optional), ): """ 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 Smart Pounce Strategy: - Find undervalued domains ending soon - Track keywords you're interested in - Get notified when matching auctions appear """ # In production, this would call real scrapers/APIs 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 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) # Pagination total = len(auctions) auctions = auctions[offset:offset + limit] # Convert to response format listings = [_convert_to_listing(a) for a in auctions] 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), ): """ 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 """ 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] return [_convert_to_listing(a) for a in auctions] @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), ): """ Get hottest auctions by bidding activity. These auctions have the most competition - high demand indicators. """ auctions = sorted(MOCK_AUCTIONS, key=lambda x: x["num_bids"], reverse=True) auctions = auctions[:limit] return [_convert_to_listing(a) for a in auctions] @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. Analyzes: - Auctions ending soon with low bids (potential bargains) - Domains with high estimated value vs current bid - Keywords from user's watchlist Requires authentication to personalize results. """ from app.services.valuation import valuation_service opportunities = [] for auction in MOCK_AUCTIONS: # Get our valuation 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"] # Calculate opportunity score # Higher score = better opportunity value_ratio = estimated_value / current_bid if current_bid > 0 else 10 # Time factor - ending soon is more urgent 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 activity factor - less bids might mean overlooked bid_factor = 1.5 if auction["num_bids"] < 10 else 1.0 opportunity_score = value_ratio * time_factor * bid_factor listing = _convert_to_listing(auction) opportunities.append({ "auction": listing.model_dump(), "analysis": { "estimated_value": estimated_value, "current_bid": current_bid, "value_ratio": round(value_ratio, 2), "potential_profit": estimated_value - current_bid, "opportunity_score": round(opportunity_score, 2), "recommendation": ( "Strong buy" if opportunity_score > 5 else "Consider" if opportunity_score > 2 else "Monitor" ), } }) # Sort by opportunity score opportunities.sort(key=lambda x: x["analysis"]["opportunity_score"], reverse=True) return { "opportunities": opportunities[:10], "strategy_tips": [ "Focus on auctions ending in < 1 hour for best snipe opportunities", "Look for domains with value_ratio > 2.0 (undervalued)", "Low bid count often indicates overlooked gems", "Set alerts for keywords you're interested in", ], "generated_at": datetime.utcnow().isoformat(), }