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() {
) : (
-
+ >
Our Trader plan filters 99% of junk domains automatically. -
- + + > Upgrade