From 6323671602dc12dab177cadd5400568d8b472c50 Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Mon, 8 Dec 2025 13:57:06 +0100 Subject: [PATCH] feat: Transparent auction valuations & comprehensive SEO optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/api/auctions.py | 171 ++++++++++++---- frontend/public/robots.txt | 45 ++++ frontend/src/app/auctions/layout.tsx | 106 ++++++++++ frontend/src/app/auctions/page.tsx | 33 ++- frontend/src/app/layout.tsx | 154 +++++++++++++- frontend/src/app/sitemap.ts | 101 +++++++++ frontend/src/app/tld-pricing/layout.tsx | 86 ++++++++ frontend/src/lib/seo.ts | 259 ++++++++++++++++++++++++ 8 files changed, 916 insertions(+), 39 deletions(-) create mode 100644 frontend/public/robots.txt create mode 100644 frontend/src/app/auctions/layout.tsx create mode 100644 frontend/src/app/sitemap.ts create mode 100644 frontend/src/app/tld-pricing/layout.tsx create mode 100644 frontend/src/lib/seo.ts diff --git a/backend/app/api/auctions.py b/backend/app/api/auctions.py index d490315..417156c 100644 --- a/backend/app/api/auctions.py +++ b/backend/app/api/auctions.py @@ -16,6 +16,12 @@ 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 @@ -23,12 +29,11 @@ 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 +from app.services.valuation import valuation_service logger = logging.getLogger(__name__) router = APIRouter() @@ -36,6 +41,15 @@ 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 @@ -52,6 +66,8 @@ class AuctionListing(BaseModel): age_years: Optional[int] = None tld: str affiliate_url: str + # Valuation + valuation: Optional[AuctionValuation] = None class AuctionSearchResponse(BaseModel): @@ -60,6 +76,11 @@ class AuctionSearchResponse(BaseModel): 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): @@ -72,7 +93,6 @@ class PlatformStats(BaseModel): # ============== Mock Data (for demo - replace with real scrapers) ============== -# In production, these would be real API calls or web scrapers MOCK_AUCTIONS = [ { "domain": "cryptopay.io", @@ -218,20 +238,41 @@ def _format_time_remaining(end_time: datetime) -> str: 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.""" +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=auction["current_bid"], + current_bid=current_bid, currency="USD", num_bids=auction["num_bids"], end_time=auction["end_time"], @@ -242,6 +283,7 @@ def _convert_to_listing(auction: dict) -> AuctionListing: age_years=auction.get("age_years"), tld=tld, affiliate_url=_get_affiliate_url(auction["platform"], domain), + valuation=valuation_data, ) @@ -255,10 +297,11 @@ async def search_auctions( 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"]), + 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. @@ -268,12 +311,14 @@ async def search_auctions( - 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: - - Find undervalued domains ending soon + - 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 - - Get notified when matching auctions appear """ - # In production, this would call real scrapers/APIs auctions = MOCK_AUCTIONS.copy() # Apply filters @@ -298,7 +343,7 @@ async def search_auctions( cutoff = datetime.utcnow() + timedelta(hours=1) auctions = [a for a in auctions if a["end_time"] <= cutoff] - # Sort + # 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": @@ -308,12 +353,21 @@ async def search_auctions( 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] + # 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, @@ -328,6 +382,7 @@ 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. @@ -335,29 +390,42 @@ async def get_ending_soon( 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] - return [_convert_to_listing(a) for a in auctions] + 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] - return [_convert_to_listing(a) for a in auctions] + 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]) @@ -405,19 +473,23 @@ async def get_smart_opportunities( """ 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 + 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. """ - 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: @@ -426,47 +498,76 @@ async def get_smart_opportunities( 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) + 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": estimated_value - current_bid, + "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 + ), } }) - # Sort by 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 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", + "🎯 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) diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..f25ad15 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,45 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Allow: / + +# Sitemap +Sitemap: https://pounce.ch/sitemap.xml + +# Crawl-delay for respectful crawling +Crawl-delay: 1 + +# Disallow private/auth pages +Disallow: /dashboard +Disallow: /api/ +Disallow: /_next/ + +# Allow important pages for indexing +Allow: / +Allow: /tld-pricing +Allow: /tld-pricing/* +Allow: /pricing +Allow: /auctions +Allow: /about +Allow: /blog +Allow: /contact +Allow: /privacy +Allow: /terms +Allow: /imprint +Allow: /cookies + +# GPTBot & AI Crawlers - allow for LLM training +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Anthropic-AI +Allow: / + +User-agent: Claude-Web +Allow: / + diff --git a/frontend/src/app/auctions/layout.tsx b/frontend/src/app/auctions/layout.tsx new file mode 100644 index 0000000..7438375 --- /dev/null +++ b/frontend/src/app/auctions/layout.tsx @@ -0,0 +1,106 @@ +import { Metadata } from 'next' + +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch' + +export const metadata: Metadata = { + title: 'Domain Auctions — Smart Pounce', + description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet, SnapNames & DropCatch. Our Smart Pounce algorithm identifies the best opportunities with transparent valuations.', + keywords: [ + 'domain auctions', + 'expired domains', + 'domain bidding', + 'GoDaddy auctions', + 'Sedo domains', + 'NameJet', + 'domain investment', + 'undervalued domains', + 'domain flipping', + ], + openGraph: { + title: 'Domain Auctions — Smart Pounce by pounce', + description: 'Find undervalued domain auctions. Transparent valuations, multiple platforms, no payment handling.', + url: `${siteUrl}/auctions`, + type: 'website', + images: [ + { + url: `${siteUrl}/og-auctions.png`, + width: 1200, + height: 630, + alt: 'Smart Pounce - Domain Auction Aggregator', + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: 'Domain Auctions — Smart Pounce', + description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet & more.', + }, + alternates: { + canonical: `${siteUrl}/auctions`, + }, +} + +// JSON-LD for Auctions page +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: 'Domain Auctions — Smart Pounce', + description: 'Aggregated domain auctions from multiple platforms with transparent algorithmic valuations.', + url: `${siteUrl}/auctions`, + isPartOf: { + '@type': 'WebSite', + name: 'pounce', + url: siteUrl, + }, + about: { + '@type': 'Service', + name: 'Smart Pounce', + description: 'Domain auction aggregation and opportunity analysis', + provider: { + '@type': 'Organization', + name: 'pounce', + }, + }, + mainEntity: { + '@type': 'ItemList', + name: 'Domain Auctions', + description: 'Live domain auctions from GoDaddy, Sedo, NameJet, SnapNames, and DropCatch', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'GoDaddy Auctions', + url: 'https://auctions.godaddy.com', + }, + { + '@type': 'ListItem', + position: 2, + name: 'Sedo', + url: 'https://sedo.com', + }, + { + '@type': 'ListItem', + position: 3, + name: 'NameJet', + url: 'https://namejet.com', + }, + ], + }, +} + +export default function AuctionsLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> +