pounce/backend/app/api/auctions.py
yves.gugger 6323671602 feat: Transparent auction valuations & comprehensive SEO optimization
AUCTION VALUATIONS (Transparent):
- All auctions now include real-time valuation from ValuationService
- Shows: estimated_value, value_ratio, potential_profit, confidence
- Displays exact formula: "$50 × Length × TLD × Keyword × Brand"
- value_ratio helps identify undervalued domains (> 1.0 = opportunity)
- Added valuation_note in API response explaining methodology

VALUATION FORMULA EXPLAINED:
  Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor

  Examples from API response:
  - healthtech.app: $50 × 1.0 × 0.45 × 1.3 × 1.32 = $40
  - blockchain.tech: $50 × 1.0 × 0.35 × 3.0 × 1.12 = $60
  - metaverse.ai: $50 × 1.6 × 1.2 × 3.0 × 1.1 = $315

SEO OPTIMIZATIONS:
- Root layout: Full Metadata with OpenGraph, Twitter Cards, JSON-LD
- JSON-LD Schema: Organization, WebSite, WebApplication with SearchAction
- robots.txt: Allows all crawlers including GPTBot, Claude, ChatGPT
- sitemap.ts: Dynamic sitemap with all pages + popular TLD pages
- Auctions layout: Page-specific meta + ItemList schema
- TLD Pricing layout: Product comparison schema

LLM OPTIMIZATION:
- robots.txt explicitly allows AI crawlers (GPTBot, Anthropic-AI, Claude-Web)
- Semantic HTML structure with proper headings
- JSON-LD structured data for rich snippets
- Descriptive meta descriptions optimized for AI summarization

FILES ADDED:
- frontend/src/lib/seo.ts - SEO configuration & helpers
- frontend/src/app/sitemap.ts - Dynamic sitemap generation
- frontend/src/app/auctions/layout.tsx - Auctions SEO
- frontend/src/app/tld-pricing/layout.tsx - TLD Pricing SEO
- frontend/public/robots.txt - Crawler directives
2025-12-08 13:57:06 +01:00

574 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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