From 6d7db540215f741af796566678f89e4bfb672722 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Mon, 15 Dec 2025 08:04:03 +0100 Subject: [PATCH] feat: Portfolio improvements, listing/yield integration, UNICORN_PLAN --- UNICORN_PLAN.md | 230 +++++++++++++++++++++ backend/app/api/listings.py | 68 ++++++ backend/app/api/yield_domains.py | 32 +-- frontend/src/app/acquire/page.tsx | 112 +++++----- frontend/src/app/buy/[slug]/page.tsx | 86 ++++---- frontend/src/app/buy/page.tsx | 6 +- frontend/src/app/terminal/listing/page.tsx | 224 +++++++++++++++++++- frontend/src/components/Footer.tsx | 4 +- frontend/src/lib/api.ts | 7 + 9 files changed, 643 insertions(+), 126 deletions(-) create mode 100644 UNICORN_PLAN.md diff --git a/UNICORN_PLAN.md b/UNICORN_PLAN.md new file mode 100644 index 0000000..8010e96 --- /dev/null +++ b/UNICORN_PLAN.md @@ -0,0 +1,230 @@ +## Pounce Unicorn Plan (integriert) + +Ziel: Pounce von einem starken Produkt (Trust + Inventory + Lead Capture) zu einem skalierbaren System mit Moat + Flywheel entwickeln. + +--- + +## Absicht & holistisches Konzept + +### Absicht (warum es Pounce gibt) + +Pounce existiert, um Domains von „toten Namen“ (nur Renewal-Kosten, keine Nutzung) zu **messbaren, handelbaren digitalen Assets** zu machen. +Wir bauen nicht nur einen Feed oder einen Marktplatz, sondern eine **Lifecycle Engine**: entdecken → erwerben → monetarisieren → liquidieren. + +### Für wen (Zielgruppe) + +- **Domain Investors / Operators**: brauchen sauberes Inventory, schnelle Entscheidungen, klare Workflows. +- **Builders / Entrepreneurs**: wollen gute Assets finden und sofort nutzen/monetarisieren. +- **Portfolio Owner** (ab 10+ Domains): wollen Governance (Health, Renewal, Cashflow) statt Chaos. + +### Positionierung (klarer Satz) + +**Pounce ist das Operating System für Domains**: ein Clean Market Feed + Verified Direct Deals + Yield Routing – mit Messbarkeit vom ersten View bis zum Exit. + +### Das Gesamtmodell (4 Module) + +1. **Discover (Intelligence)** + Findet Assets: Clean Feed, Scores, TLD Intel, Filter, Alerts. + +2. **Acquire (Marketplace / Liquidity)** + Sichert Assets: externe Auktionen + **Pounce Direct** (DNS-verified Owner). + +3. **Yield (Intent Routing)** + Monetarisiert Assets: Domain-Traffic → Intent → Partner → Revenue Share. + +4. **Trade (Exit / Outcomes)** + Liquidität und Bewertung: Domains werden nach **Cashflow** bepreist (Multiple), nicht nur nach „Vibe“. + +### Warum das Unicorn-Potenzial hat (Moat + Flywheel) + +- **Moat**: Proprietäre Daten über Intent, Traffic, Conversion und Cashflow auf Domain-Level (schwer kopierbar). +- **Flywheel**: mehr Domains → mehr Routing/Conversions → mehr Daten → bessere Scores/Routing → mehr Deals → mehr Domains. + +--- + +## 0) Leitprinzipien + +- **Moat entsteht dort, wo proprietäre Daten entstehen**: Yield/Intent + Deal Outcomes. +- **Trust ist ein Feature**: alles, was Spam/Scam senkt, steigert Conversion. +- **Telemetry ist nicht „später“**: jede neue Funktion erzeugt Events + messbare KPIs. + +--- + +## 1) Deal‑System (Liquidity Loop fertig machen) + +### 1A — Inbox Workflow (Woche 1) + +**Ziel**: Seller können Leads zuverlässig triagieren und messen. + +- **Inquiry Status Workflow komplett**: `new → read → replied → closed` + `spam` + - Backend PATCH Endpoint + UI Actions + - „Close“ inkl. Grund (z.B. sold elsewhere / low offer / no fit) +- **Audit Trail (minimal)** + - jede Statusänderung speichert: `who/when/old/new` + +**KPIs** +- inquiry→read rate +- inquiry→replied rate +- median reply time + +### 1B — Threading/Negotiation (Woche 2–3) + +**Ziel**: Verhandlung im Produkt, nicht off-platform. + +- **Threading**: Buyer ↔ Seller Messages als Conversation pro Listing +- **Notifications**: E‑Mail „New message“ + Login‑Gate +- **Audit Trail (voll)**: message events + status events +- **Security**: rate limits (buyer + seller), keyword checks, link safety + +**KPIs** +- inquiry→first message +- messages/thread +- reply rate + +### 1C — Deal Closure + GMV (Woche 3–4) + +**Ziel**: echte Conversion/GMV messbar machen. + +- **“Mark as Sold”** auf Listing + - Gründe: sold on Pounce / sold off‑platform / removed + - optional: **deal_value** + currency +- optional sauberer **Deal-Record** + - `deal_id`, `listing_id`, `buyer_user_id(optional)`, `final_price`, `closed_at` + +**KPIs** +- inquiry→sold +- close rate +- time-to-close +- GMV + +### 1D — Anti‑Abuse (laufend ab Woche 1) + +- **Rate limit** pro IP + pro User (inquire + message + status flips) +- **Spam flagging** (Heuristiken + manuell) +- **Blocklist** (buyer account/email/domain-level) + +**KPIs** +- spam rate +- blocked attempts +- false positive rate + +--- + +## 2) Yield als Burggraben (Moat) + +### 2A — Connect/Nameserver Flow (Woche 2–4) + +**Ziel**: Domains „unter Kontrolle“ bringen (Connect Layer). + +- **Connect Wizard** (Portfolio → Yield) + - Anleitung: NS/TXT Setup + - Status: pending/verified/active +- **Backend checks** (NS/TXT) + Speicherung: `connected_at` +- **Routing Entry** (Edge/Web): Request → route decision + +**KPIs** +- connect attempts→verified +- connected domains + +### 2B — Intent → Routing → Tracking (Monat 2) + +**Ziel**: Intent Routing MVP für 1 Vertical. + +- **Intent detection** (MVP) +- **Routing** zu Partnern + Fallbacks +- **Tracking**: click_id, domain_id, partner_id +- **Attribution**: conversion mapping + payout status + +**KPIs** +- clicks/domain +- conversion rate +- revenue/domain + +### 2C — Payout + Revenue Share (Monat 2–3) + +- Ledger: pending → confirmed → paid +- payout schedule (monatlich) + export/reports + +**KPIs** +- payout accuracy +- disputes +- net margin + +### 2D — Portfolio Cashflow Dashboard (Monat 3) + +- Portfolio zeigt: **MRR, last 30d revenue, ROI**, top routes +- Domains werden „yield-bearing assets“ → später handelbar nach Multiple + +**KPIs** +- MRR +- retention/churn +- expansion + +--- + +## 3) Flywheel / Distribution + +### 3A — Programmatic SEO maximal (Monat 1–2) + +- Templates skalieren (TLD/Intel/Price) +- klare CTA‑Pfade: „Track this TLD“, „Enter Terminal“, „View Direct Deals“ + +**KPIs** +- organic sessions +- signup conversion + +### 3B — Public Deal Surface + Login Gate (Monat 1) + +- Public Acquire + /buy als Conversion‑Engine +- “contact requires login” überall konsistent + +**KPIs** +- view→login +- login→inquiry + +### 3C — Viral Loop „Powered by Pounce“ (Monat 2–3) + +- nur wenn intent passt / low intent fallback +- referral link + revenue share + +**KPIs** +- referral signups +- CAC ~0 + +--- + +## 4) Skalierung / Telemetry + +### 4A — Events (Woche 1–2) + +Definiere & logge Events: +- `listing_view` +- `inquiry_created` +- `inquiry_status_changed` +- `message_sent` +- `listing_marked_sold` +- `yield_connected` +- `yield_click` +- `yield_conversion` +- `payout_paid` + +**KPIs** +- funnel conversion +- time metrics + +### 4B — Ops (Monat 1) + +- Monitoring/alerts (Errors + Business KPIs) +- Backups (DB daily + restore drill) +- Deliverability (SPF/DKIM/DMARC, bounce handling) +- Abuse monitoring dashboards + +--- + +## Empfohlene Reihenfolge (damit es schnell „unfair“ wird) + +1. **Deal-System 1A–1C** (GMV & close-rate messbar) +2. **Yield 2A** (Connect Layer) parallel starten +3. **Events 4A** sofort mitziehen +4. **Yield 2B–2C** (Moat) sobald Connect stabil +5. Flywheel 3A–3C kontinuierlich diff --git a/backend/app/api/listings.py b/backend/app/api/listings.py index be83e83..eaabf95 100644 --- a/backend/app/api/listings.py +++ b/backend/app/api/listings.py @@ -188,6 +188,11 @@ class InquiryResponse(BaseModel): from_attributes = True +class InquiryUpdate(BaseModel): + """Update inquiry status for listing owner.""" + status: str = Field(..., min_length=1, max_length=20) # new, read, replied, spam + + class VerificationResponse(BaseModel): """DNS verification response.""" verification_code: str @@ -716,6 +721,69 @@ async def get_listing_inquiries( ] +@router.patch("/{id}/inquiries/{inquiry_id}", response_model=InquiryResponse) +async def update_listing_inquiry( + id: int, + inquiry_id: int, + data: InquiryUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Update an inquiry status (listing owner only).""" + allowed = {"new", "read", "replied", "spam"} + status_clean = (data.status or "").strip().lower() + if status_clean not in allowed: + raise HTTPException(status_code=400, detail="Invalid status") + + # Verify listing ownership + listing_result = await db.execute( + select(DomainListing).where( + and_( + DomainListing.id == id, + DomainListing.user_id == current_user.id, + ) + ) + ) + listing = listing_result.scalar_one_or_none() + if not listing: + raise HTTPException(status_code=404, detail="Listing not found") + + inquiry_result = await db.execute( + select(ListingInquiry).where( + and_( + ListingInquiry.id == inquiry_id, + ListingInquiry.listing_id == id, + ) + ) + ) + inquiry = inquiry_result.scalar_one_or_none() + if not inquiry: + raise HTTPException(status_code=404, detail="Inquiry not found") + + now = datetime.utcnow() + inquiry.status = status_clean + if status_clean == "read" and inquiry.read_at is None: + inquiry.read_at = now + if status_clean == "replied": + inquiry.replied_at = now + + await db.commit() + await db.refresh(inquiry) + + return InquiryResponse( + id=inquiry.id, + name=inquiry.name, + email=inquiry.email, + phone=inquiry.phone, + company=inquiry.company, + message=inquiry.message, + offer_amount=inquiry.offer_amount, + status=inquiry.status, + created_at=inquiry.created_at, + read_at=inquiry.read_at, + ) + + @router.put("/{id}", response_model=ListingResponse) async def update_listing( id: int, diff --git a/backend/app/api/yield_domains.py b/backend/app/api/yield_domains.py index 9b8b856..0a50be0 100644 --- a/backend/app/api/yield_domains.py +++ b/backend/app/api/yield_domains.py @@ -124,13 +124,13 @@ async def get_yield_dashboard( domain_ids = [d.id for d in domains] monthly_result = await db.execute( select( - func.count(YieldTransaction.id).label("count"), + func.count(YieldTransaction.id).label("count"), func.coalesce(func.sum(YieldTransaction.net_amount), 0).label("revenue"), func.sum(case((YieldTransaction.event_type == "click", 1), else_=0)).label("clicks"), func.sum(case((YieldTransaction.event_type.in_(["lead", "sale"]), 1), else_=0)).label("conversions"), ).where( YieldTransaction.yield_domain_id.in_(domain_ids), - YieldTransaction.created_at >= month_start, + YieldTransaction.created_at >= month_start, ) ) monthly_stats = monthly_result.first() @@ -153,8 +153,8 @@ async def get_yield_dashboard( pending_result = await db.execute( select(func.coalesce(func.sum(YieldTransaction.net_amount), 0)).where( YieldTransaction.yield_domain_id.in_(domain_ids), - YieldTransaction.status == "confirmed", - YieldTransaction.paid_at.is_(None), + YieldTransaction.status == "confirmed", + YieldTransaction.paid_at.is_(None), ) ) pending_payout = pending_result.scalar() or Decimal("0") @@ -258,8 +258,8 @@ async def get_yield_domain( """ result = await db.execute( select(YieldDomain).where( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, ) ) domain = result.scalar_one_or_none() @@ -350,8 +350,8 @@ async def activate_domain_for_yield( if intent_result.suggested_partners: partner_result = await db.execute( select(AffiliatePartner).where( - AffiliatePartner.slug == intent_result.suggested_partners[0], - AffiliatePartner.is_active == True, + AffiliatePartner.slug == intent_result.suggested_partners[0], + AffiliatePartner.is_active == True, ) ) partner = partner_result.scalar_one_or_none() @@ -408,8 +408,8 @@ async def verify_domain_dns( """ result = await db.execute( select(YieldDomain).where( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, ) ) domain = result.scalar_one_or_none() @@ -487,8 +487,8 @@ async def update_yield_domain( """ result = await db.execute( select(YieldDomain).where( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, ) ) domain = result.scalar_one_or_none() @@ -501,8 +501,8 @@ async def update_yield_domain( # Validate partner exists partner_result = await db.execute( select(AffiliatePartner).where( - AffiliatePartner.slug == update.active_route, - AffiliatePartner.is_active == True, + AffiliatePartner.slug == update.active_route, + AffiliatePartner.is_active == True, ) ) partner = partner_result.scalar_one_or_none() @@ -539,8 +539,8 @@ async def delete_yield_domain( """ result = await db.execute( select(YieldDomain).where( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, ) ) domain = result.scalar_one_or_none() diff --git a/frontend/src/app/acquire/page.tsx b/frontend/src/app/acquire/page.tsx index 55cf2cf..dcbc0c7 100644 --- a/frontend/src/app/acquire/page.tsx +++ b/frontend/src/app/acquire/page.tsx @@ -552,21 +552,21 @@ export default function AcquirePage() { ) : ( - + >
- {auction.domain} + {auction.domain}
@@ -589,7 +589,7 @@ export default function AcquirePage() { )}
-
+ ) ))}
@@ -660,50 +660,50 @@ export default function AcquirePage() { {/* Search & Filters Bar */}
- {/* Search */} + {/* Search */}
- setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-12 pr-4 py-3 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-sm focus:outline-none focus:border-accent transition-all" - /> -
- - {/* Filters */} + /> +
+ + {/* Filters */}
- setSelectedPlatform(e.target.value)} className="appearance-none px-4 py-3 bg-[#0A0A0A] border border-white/10 text-white font-mono text-sm focus:outline-none focus:border-accent cursor-pointer" - > - {PLATFORMS.map(p => )} - - -
- {[ + > + {PLATFORMS.map(p => )} + + +
+ {[ { id: 'all' as const, label: 'All', icon: Gavel }, { id: 'ending' as const, label: 'Ending', icon: Timer }, { id: 'hot' as const, label: 'Hot', icon: Flame }, - ].map((tab) => ( - - ))} -
+ {tab.label} + + ))}
+
@@ -719,18 +719,18 @@ export default function AcquirePage() { {/* Table Body */} - {loading ? ( + {loading ? (
) : filteredAuctions.length === 0 ? (
No assets found
) : ( -
+
{filteredAuctions.map((auction, i) => ( auction.is_pounce ? (
- {auction.domain} + {auction.domain}
@@ -798,14 +798,14 @@ export default function AcquirePage() { "w-8 h-8 border flex items-center justify-center transition-all", "border-white/10 text-white/30 group-hover:bg-white group-hover:text-black" )}> - -
+ - + + ) ))} - - )} + + )} {/* Stats Footer */} @@ -822,18 +822,18 @@ export default function AcquirePage() {
-
+

Eliminate Noise.

Our Trader plan filters 99% of junk domains automatically. -

- + + > Upgrade - - + + )} diff --git a/frontend/src/app/buy/[slug]/page.tsx b/frontend/src/app/buy/[slug]/page.tsx index e748b1b..888296b 100644 --- a/frontend/src/app/buy/[slug]/page.tsx +++ b/frontend/src/app/buy/[slug]/page.tsx @@ -382,36 +382,36 @@ export default function BuyDomainPage() { ) : ( -
-
-

- {listing.asking_price ? 'Purchase Inquiry' : 'Contact Seller'} -

-
+ +
+

+ {listing.asking_price ? 'Purchase Inquiry' : 'Contact Seller'} +

+
-
+
- setFormData({ ...formData, name: e.target.value })} - className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" - /> - setFormData({ ...formData, email: e.target.value })} + setFormData({ ...formData, name: e.target.value })} + className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" + /> + setFormData({ ...formData, email: e.target.value })} readOnly={isAuthenticated} title={isAuthenticated ? 'Uses your account email' : undefined} className={clsx( "w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all", isAuthenticated && "opacity-70 cursor-not-allowed" )} - /> + />
{isAuthenticated && (

@@ -419,13 +419,13 @@ export default function BuyDomainPage() {

)} setFormData({ ...formData, phone: e.target.value })} - className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" + type="text" + placeholder="Phone (Optional)" + value={formData.phone} + onChange={(e) => setFormData({ ...formData, phone: e.target.value })} + className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" /> - {listing.allow_offers && ( + {listing.allow_offers && (
$ )}