feat: integrate Pounce self-promotion & viral growth system
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

Pounce Eigenwerbung (from pounce_endgame.md):
- Add 'pounce_promo' as fallback partner for generic/unclear intent domains
- Create dedicated Pounce promo landing page with CTA to register
- Update footer on all yield pages: 'Monetized by Pounce • Own a domain? Start yielding'

Tech/Investment Domain Detection:
- Add 'investment_domains' category (invest, crypto, trading, domain, startup)
- Add 'tech_dev' category (developer, web3, fintech, proptech)
- Both categories have 'pounce_affinity' flag for higher Pounce conversion

Referral Tracking for Domain Owners:
- Add user fields: referred_by_user_id, referred_by_domain, referral_code
- Parse yield referral codes (yield_{user_id}_{domain_id}) on registration
- Domain owners earn lifetime commission when visitors sign up via their domain

DB Migrations:
- Add referral tracking columns to users table
This commit is contained in:
yves.gugger
2025-12-12 15:27:53 +01:00
parent dc12f14638
commit 58228e3d33
9 changed files with 713 additions and 12 deletions

View File

@ -100,6 +100,33 @@ async def register(
name=user_data.name,
)
# Process yield referral if present
# Format: yield_{user_id}_{domain_id}
if user_data.ref and user_data.ref.startswith("yield_"):
try:
parts = user_data.ref.split("_")
if len(parts) >= 3:
referrer_user_id = int(parts[1])
# Store referral info
user.referred_by_user_id = referrer_user_id
user.referral_code = user_data.ref
# Try to get domain name from yield_domain_id
try:
from app.models.yield_domain import YieldDomain
yield_domain_id = int(parts[2])
yield_domain = await db.execute(
select(YieldDomain).where(YieldDomain.id == yield_domain_id)
)
yd = yield_domain.scalar_one_or_none()
if yd:
user.referred_by_domain = yd.domain
except Exception:
pass
await db.commit()
logger.info(f"User {user.email} referred by user {referrer_user_id}")
except Exception as e:
logger.warning(f"Failed to process referral code: {user_data.ref}, error: {e}")
# Auto-admin for specific email
ADMIN_EMAILS = ["guggeryves@hotmail.com"]
if user.email.lower() in [e.lower() for e in ADMIN_EMAILS]:

View File

@ -82,8 +82,226 @@ def generate_tracking_url(
if partner.slug in network_urls:
return network_urls[partner.slug]
# Generic fallback - show Pounce marketplace
return f"{settings.site_url}/buy?ref={yield_domain.domain}&clickid={click_id}"
# Pounce self-promotion fallback with referral tracking
# Domain owner gets lifetime commission on signups via their domain
referral_code = f"yield_{yield_domain.user_id}_{yield_domain.id}"
return f"{settings.site_url}/register?ref={referral_code}&from={yield_domain.domain}&clickid={click_id}"
def is_pounce_affinity_domain(domain: str) -> bool:
"""
Check if a domain has high affinity for Pounce self-promotion.
Tech, investment, and domain-related domains convert better for Pounce.
"""
intent = detect_domain_intent(domain)
# Check if the matched category has pounce_affinity flag
if intent.category in ["investment", "tech"] or intent.subcategory in ["domains", "dev"]:
return True
# Check for specific keywords
pounce_keywords = {
"invest", "domain", "trading", "crypto", "asset", "portfolio",
"startup", "tech", "dev", "saas", "digital", "passive", "income"
}
domain_lower = domain.lower()
return any(kw in domain_lower for kw in pounce_keywords)
def generate_pounce_promo_page(
yield_domain: YieldDomain,
click_id: int,
) -> str:
"""
Generate Pounce self-promotion landing page.
Used as fallback when no high-value partner is available,
or when the domain has high Pounce affinity.
"""
referral_code = f"yield_{yield_domain.user_id}_{yield_domain.id}"
register_url = f"{settings.site_url}/register?ref={referral_code}&from={yield_domain.domain}&clickid={click_id}"
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{yield_domain.domain} - Powered by Pounce</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%);
color: #fff;
padding: 2rem;
}}
.container {{
text-align: center;
max-width: 600px;
}}
.badge {{
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 9999px;
color: #10b981;
font-size: 0.875rem;
margin-bottom: 2rem;
}}
.domain {{
font-size: 1.25rem;
color: #6b7280;
margin-bottom: 1rem;
}}
h1 {{
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
margin-bottom: 1.5rem;
}}
h1 span {{
background: linear-gradient(90deg, #10b981, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}}
.subtitle {{
font-size: 1.125rem;
color: #9ca3af;
margin-bottom: 2.5rem;
line-height: 1.6;
}}
.cta {{
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 2rem;
background: linear-gradient(90deg, #10b981, #059669);
color: #fff;
font-size: 1.125rem;
font-weight: 600;
border-radius: 0.75rem;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.3);
}}
.cta:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 30px rgba(16, 185, 129, 0.4);
}}
.features {{
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 3rem;
flex-wrap: wrap;
}}
.feature {{
display: flex;
align-items: center;
gap: 0.5rem;
color: #6b7280;
font-size: 0.875rem;
}}
.feature svg {{
color: #10b981;
}}
.footer {{
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
color: #4b5563;
}}
.footer a {{
color: #10b981;
text-decoration: none;
}}
.owner-note {{
margin-top: 3rem;
padding: 1rem;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 0.5rem;
font-size: 0.875rem;
color: #a78bfa;
}}
</style>
</head>
<body>
<div class="container">
<div class="badge">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
This domain is monetized by Pounce
</div>
<div class="domain">{yield_domain.domain}</div>
<h1>
Turn Your Domains Into<br>
<span>Passive Income</span>
</h1>
<p class="subtitle">
Stop paying renewal fees for idle domains.<br>
Let them earn money for you — automatically.
</p>
<a href="{register_url}" class="cta">
Start Earning Free
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
<div class="features">
<div class="feature">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
Free Forever
</div>
<div class="feature">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
70% Revenue Share
</div>
<div class="feature">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Swiss Quality
</div>
</div>
<div class="owner-note">
👋 The owner of this domain earns a commission when you sign up!
</div>
</div>
<div class="footer">
<a href="{settings.site_url}">pounce.ch</a> — Domain Intelligence Platform
</div>
</body>
</html>
"""
def generate_landing_page(
@ -98,18 +316,21 @@ def generate_landing_page(
1. Improve user experience
2. Allow for A/B testing
3. Comply with affiliate disclosure requirements
If no partner, shows Pounce self-promotion instead.
"""
# If no partner or partner is pounce_promo, show Pounce promo page
if partner is None or partner.slug == "pounce_promo":
return generate_pounce_promo_page(yield_domain, click_id)
intent = detect_domain_intent(yield_domain.domain)
# Partner info
partner_name = partner.name if partner else "Partner"
partner_desc = partner.description if partner else "Find the best offers"
partner_name = partner.name
partner_desc = partner.description or "Find the best offers"
# Generate redirect URL
redirect_url = (
generate_tracking_url(partner, yield_domain, click_id)
if partner else f"{settings.site_url}/buy"
)
redirect_url = generate_tracking_url(partner, yield_domain, click_id)
return f"""
<!DOCTYPE html>
@ -206,8 +427,8 @@ def generate_landing_page(
</div>
<div class="footer">
Powered by <a href="{settings.site_url}">Pounce</a> •
<a href="{settings.site_url}/privacy">Privacy</a>
Monetized by <a href="{settings.site_url}/yield?ref={yield_domain.domain}">Pounce</a> •
Own a domain? <a href="{settings.site_url}/yield?ref={yield_domain.domain}">Start yielding →</a>
</div>
<script>

View File

@ -166,6 +166,20 @@ async def apply_migrations(conn: AsyncConnection) -> None:
)
)
# ----------------------------------------------------
# 6) User referral tracking columns
# ----------------------------------------------------
if await _table_exists(conn, "users"):
if not await _has_column(conn, "users", "referred_by_user_id"):
logger.info("DB migrations: adding column users.referred_by_user_id")
await conn.execute(text("ALTER TABLE users ADD COLUMN referred_by_user_id INTEGER"))
if not await _has_column(conn, "users", "referred_by_domain"):
logger.info("DB migrations: adding column users.referred_by_domain")
await conn.execute(text("ALTER TABLE users ADD COLUMN referred_by_domain VARCHAR(255)"))
if not await _has_column(conn, "users", "referral_code"):
logger.info("DB migrations: adding column users.referral_code")
await conn.execute(text("ALTER TABLE users ADD COLUMN referral_code VARCHAR(100)"))
logger.info("DB migrations: done")

View File

@ -1,7 +1,7 @@
"""User model."""
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Boolean, DateTime
from sqlalchemy import String, Boolean, DateTime, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@ -40,6 +40,11 @@ class User(Base):
oauth_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
oauth_avatar: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
# Yield Referral Tracking (for viral growth)
referred_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # User who referred this user
referred_by_domain: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Domain that referred
referral_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # Original referral code
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(

View File

@ -10,6 +10,8 @@ class UserCreate(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8, max_length=100)
name: Optional[str] = Field(None, max_length=100)
# Yield referral tracking
ref: Optional[str] = Field(None, max_length=100, description="Referral code from yield domain")
class UserLogin(BaseModel):

View File

@ -388,6 +388,22 @@ PARTNER_SEED_DATA: list[dict[str, Any]] = [
"priority": 90,
},
# =========================================================================
# POUNCE SELF-PROMOTION (Viral Growth)
# =========================================================================
{
"name": "Pounce Promo",
"slug": "pounce_promo",
"network": "internal",
"intent_categories": "investment_domains,tech_dev,generic",
"geo_countries": "CH,DE,AT",
"payout_type": "cps",
"payout_amount": Decimal("0"), # 30% lifetime commission handled separately
"payout_currency": "CHF",
"description": "Pounce self-promotion. Domain owners earn 30% lifetime commission on referrals.",
"priority": 50, # Higher than generic but lower than high-value partners
},
# =========================================================================
# GENERIC FALLBACK
# =========================================================================

View File

@ -217,6 +217,33 @@ INTENT_PATTERNS = {
"partners": ["capterra", "g2"]
},
# Investment / Crypto / Finance Tech - HIGH POUNCE CONVERSION
"investment_domains": {
"keywords": [
"invest", "investment", "investor", "portfolio", "asset", "assets",
"trading", "trader", "crypto", "bitcoin", "blockchain", "nft",
"domain", "domains", "digital", "passive", "income", "yield",
"startup", "founder", "entrepreneur", "venture", "capital"
],
"patterns": [r"invest\w*", r"trad\w*", r"crypto\w*", r"domain\w*"],
"potential": "high",
"partners": ["pounce_promo"], # Pounce self-promotion
"pounce_affinity": True, # Flag for Pounce self-promotion
},
# Tech / Developer - GOOD POUNCE CONVERSION
"tech_dev": {
"keywords": [
"dev", "developer", "code", "coding", "tech", "technology",
"api", "sdk", "github", "git", "open-source", "opensource",
"web3", "defi", "fintech", "proptech", "saas"
],
"patterns": [r"dev\w*", r"tech\w*", r"web\d*"],
"potential": "medium",
"partners": ["pounce_promo"],
"pounce_affinity": True,
},
# Food / Restaurant
"food_restaurant": {
"keywords": [

View File

@ -0,0 +1,301 @@
'use client'
import { useEffect, useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useStore } from '@/lib/store'
import { Sidebar } from './Sidebar'
import { KeyboardShortcutsProvider, useUserShortcuts } from '@/hooks/useKeyboardShortcuts'
import { Bell, Search, X, Command } from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
interface CommandCenterLayoutProps {
children: React.ReactNode
title?: string
subtitle?: string
actions?: React.ReactNode
}
export function CommandCenterLayout({
children,
title,
subtitle,
actions
}: CommandCenterLayoutProps) {
const router = useRouter()
const { isAuthenticated, isLoading, checkAuth, domains } = useStore()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [mounted, setMounted] = useState(false)
const authCheckedRef = useRef(false)
// Ensure component is mounted before rendering
useEffect(() => {
setMounted(true)
}, [])
// Load sidebar state from localStorage
useEffect(() => {
if (mounted) {
const saved = localStorage.getItem('sidebar-collapsed')
if (saved) {
setSidebarCollapsed(saved === 'true')
}
}
}, [mounted])
// Check auth only once on mount
useEffect(() => {
if (!authCheckedRef.current) {
authCheckedRef.current = true
checkAuth()
}
}, [checkAuth])
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login')
}
}, [isLoading, isAuthenticated, router])
// Available domains for notifications
const availableDomains = domains?.filter(d => d.is_available) || []
const hasNotifications = availableDomains.length > 0
// Show loading only if we're still checking auth
if (!mounted || isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-foreground-muted">Loading Command Center...</p>
</div>
</div>
)
}
if (!isAuthenticated) {
return null
}
return (
<KeyboardShortcutsProvider>
<UserShortcutsWrapper />
<div className="min-h-screen bg-background">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.02] rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.015] rounded-full blur-[100px]" />
</div>
{/* Sidebar */}
<Sidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
/>
{/* Main Content Area */}
<div
className={clsx(
"relative min-h-screen transition-all duration-300",
// Desktop: adjust for sidebar
"lg:ml-[260px]",
sidebarCollapsed && "lg:ml-[72px]",
// Mobile: no margin, just padding for menu button
"ml-0 pt-16 lg:pt-0"
)}
>
{/* Top Bar */}
<header className="sticky top-0 z-30 bg-gradient-to-r from-background/95 via-background/90 to-background/95 backdrop-blur-xl border-b border-border/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-5 sm:py-6 flex items-center justify-between">
{/* Left: Title */}
<div className="ml-10 lg:ml-0 min-w-0 flex-1">
{title && (
<h1 className="text-xl sm:text-2xl font-semibold tracking-tight text-foreground truncate">{title}</h1>
)}
{subtitle && (
<p className="text-sm text-foreground-muted mt-0.5 hidden sm:block truncate">{subtitle}</p>
)}
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2 sm:gap-3 shrink-0 ml-4">
{/* Quick Search */}
<button
onClick={() => setSearchOpen(true)}
className="hidden md:flex items-center gap-2 h-9 px-3 bg-foreground/5 hover:bg-foreground/8
border border-border/40 rounded-lg text-sm text-foreground-muted
hover:text-foreground transition-all duration-200 hover:border-border/60"
>
<Search className="w-4 h-4" />
<span className="hidden lg:inline">Search</span>
<kbd className="hidden xl:inline-flex items-center h-5 px-1.5 bg-background border border-border/60
rounded text-[10px] text-foreground-subtle font-mono">K</kbd>
</button>
{/* Mobile Search */}
<button
onClick={() => setSearchOpen(true)}
className="md:hidden flex items-center justify-center w-9 h-9 text-foreground-muted
hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
>
<Search className="w-5 h-5" />
</button>
{/* Notifications */}
<div className="relative">
<button
onClick={() => setNotificationsOpen(!notificationsOpen)}
className={clsx(
"relative flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-200",
notificationsOpen
? "bg-foreground/10 text-foreground"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<Bell className="w-5 h-5" />
{hasNotifications && (
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-accent rounded-full">
<span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-50" />
</span>
)}
</button>
{/* Notifications Dropdown */}
{notificationsOpen && (
<div className="absolute right-0 top-full mt-2 w-80 bg-background-secondary border border-border
rounded-xl shadow-2xl overflow-hidden">
<div className="p-4 border-b border-border flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">Notifications</h3>
<button
onClick={() => setNotificationsOpen(false)}
className="text-foreground-muted hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="max-h-80 overflow-y-auto">
{availableDomains.length > 0 ? (
<div className="p-2">
{availableDomains.slice(0, 5).map((domain) => (
<Link
key={domain.id}
href="/command/watchlist"
onClick={() => setNotificationsOpen(false)}
className="flex items-start gap-3 p-3 hover:bg-foreground/5 rounded-lg transition-colors"
>
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center shrink-0">
<span className="w-2 h-2 bg-accent rounded-full animate-pulse" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{domain.name}</p>
<p className="text-xs text-accent">Available now!</p>
</div>
</Link>
))}
</div>
) : (
<div className="p-8 text-center">
<Bell className="w-8 h-8 text-foreground-subtle mx-auto mb-3" />
<p className="text-sm text-foreground-muted">No notifications</p>
<p className="text-xs text-foreground-subtle mt-1">
We'll notify you when domains become available
</p>
</div>
)}
</div>
</div>
)}
</div>
{/* Keyboard Shortcuts Hint */}
<button
onClick={() => {}}
className="hidden sm:flex items-center gap-1.5 px-2 py-1.5 text-xs text-foreground-subtle hover:text-foreground
bg-foreground/5 rounded-lg border border-border/40 hover:border-border/60 transition-all"
title="Keyboard shortcuts (?)"
>
<Command className="w-3.5 h-3.5" />
<span>?</span>
</button>
{/* Custom Actions */}
{actions}
</div>
</div>
</header>
{/* Page Content */}
<main className="relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
{children}
</div>
</main>
</div>
{/* Quick Search Modal */}
{searchOpen && (
<div
className="fixed inset-0 z-[60] bg-background/80 backdrop-blur-sm flex items-start justify-center pt-[15vh] sm:pt-[20vh] px-4"
onClick={() => setSearchOpen(false)}
>
<div
className="w-full max-w-xl bg-background-secondary border border-border rounded-2xl shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 p-4 border-b border-border">
<Search className="w-5 h-5 text-foreground-muted" />
<input
type="text"
placeholder="Search domains, TLDs, auctions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 bg-transparent text-foreground placeholder:text-foreground-subtle
outline-none text-lg"
autoFocus
/>
<button
onClick={() => setSearchOpen(false)}
className="flex items-center h-6 px-2 bg-background border border-border
rounded text-xs text-foreground-subtle font-mono hover:text-foreground transition-colors"
>
ESC
</button>
</div>
<div className="p-6 text-center text-foreground-muted text-sm">
Start typing to search...
</div>
</div>
</div>
)}
{/* Keyboard shortcut for search */}
<KeyboardShortcut onTrigger={() => setSearchOpen(true)} keys={['Meta', 'k']} />
</div>
</KeyboardShortcutsProvider>
)
}
// Keyboard shortcut component
function KeyboardShortcut({ onTrigger, keys }: { onTrigger: () => void, keys: string[] }) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (keys.includes('Meta') && e.metaKey && e.key === 'k') {
e.preventDefault()
onTrigger()
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [onTrigger, keys])
return null
}
// User shortcuts wrapper
function UserShortcutsWrapper() {
useUserShortcuts()
return null
}

View File

@ -111,4 +111,92 @@ Pounce becomes the **Asset Manager**.
Das passt perfekt. Es ist sauber, es ist automatisiert, und es ist extrem skalierbar.
Du baust keine Webseiten. Du baust **Wegweiser**. Und für jeden, der den Wegweiser nutzt, kassierst du Maut.
**Das ist das Unicorn-Modell.** 🦄
**Das ist das Unicorn-Modell.** 🦄
Das ist eine **geniale Growth-Hacking-Idee**. Du nutzt quasi das ungenutzte Inventar deiner User, um Pounce selbst zu bewerben. Das nennt man "Viral Loop" (wie bei Hotmail früher: "Sent with Hotmail").
Aber: Es gibt einen **entscheidenden Haken**, den wir beachten müssen, damit deine User nicht sauer werden.
Hier ist die Analyse, warum du das tun solltest aber **nicht ausschließlich**.
---
### Das Problem: Der "Intent Mismatch" (Warum es nicht immer klappt)
Stell dir vor, ein User besitzt die Domain `notfall-zahnarzt-bern.ch`.
* **Der Besucher:** Hat Zahnschmerzen. Er sucht Hilfe.
* **Deine Pounce-Werbung:** "Werde Domain-Investor und verdiene Geld!"
* **Das Ergebnis:** Der Besucher ist verwirrt und geht sofort weg (Bounce). Niemand verdient Geld. Weder du noch der Domain-Besitzer.
**Die Konsequenz:**
Wenn der Domain-Besitzer sieht, dass er 1.000 Besucher hatte, aber **0 CHF** verdient hat (weil niemand mit Zahnschmerzen ein Domain-Tool abonniert), wird er Pounce Yield wieder abschalten.
---
### Die Lösung: Das "Fallback"-Modell (Der Lückenfüller)
Wir machen die Pounce-Eigenwerbung nicht zur *einzigen* Option, sondern zur **Standard-Option (Default)**, wenn wir nichts Besseres haben.
So integrieren wir das perfekt:
#### 1. Priorität A: High-Value Intent (3rd Party)
Wenn wir wissen, was der User will, geben wir es ihm.
* Domain: `kredit-vergleich.ch`
* Route: Bank / Affiliate
* **Warum:** Das bringt dem User vielleicht 50 CHF pro Lead. Damit kannst du mit Eigenwerbung nicht mithalten. **Das macht den User reich.**
#### 2. Priorität B: Low-Value / Unclear Intent (Pounce Promo)
Wenn die Domain generisch ist oder wir keinen Partner haben.
* Domain: `coole-namen.net` oder `peter-müller.com`
* Route: **Pounce Landing Page**
* *Headline:* "This domain is powered by Pounce."
* *Subline:* "Make money with your domains. Start for free."
* *Action:* User klickt, registriert sich.
* *Provision:* Der Domain-Besitzer bekommt z.B. 30% Lifetime-Provision auf das Abo des Geworbenen.
* **Warum:** Hier ist die Chance auf ein Abo höher, und es ist besser als eine leere Seite. **Das macht Pounce groß.**
---
### Die Strategie: "Pounce as the Default Ad"
Wir bauen das System so auf:
1. **Das "Powered by Pounce" Badge (Immer da):**
Egal, wohin wir leiten (auch beim Zahnarzt), ganz unten im Footer steht immer klein:
* *"Monetized by Pounce. Own a domain? [Start yielding]"*
* Das ist dein kostenloses virales Marketing auf *jeder* Seite.
2. **Der "Empty State" Füller:**
Solange der User keine spezifische Route (z.B. zu Amazon) eingestellt hat, schalten wir **automatisch** die Pounce-Eigenwerbung.
* *Vorteil für dich:* Kostenloses Werbeinventar auf tausenden Domains.
* *Vorteil für User:* Besser als eine Fehlerseite ("404 Not Found"). Er hat zumindest die *Chance* auf eine Provision.
3. **Gezieltes Targeting für Tech-Domains:**
Bei Domains wie `invest-tech.io` oder `crypto-assets.xyz` ist die Zielgruppe (Investoren/Techies) perfekt für Pounce.
* Hier schlägt dein System dem User aktiv vor: *"Wir empfehlen für diese Domain die **Pounce-Affiliate-Route**. Die Conversion-Wahrscheinlichkeit für unser Tool ist hier sehr hoch."*
---
### Warum das für dein Unicorn-Ziel wichtig ist
Indem du Pounce selbst bewirbst, senkst du deine **CAC (Customer Acquisition Cost)** auf fast Null.
* Normalerweise musst du für einen neuen Kunden 50 CHF Werbung auf Google ausgeben.
* Mit diesem Modell bringen dir deine *bestehenden* Kunden über ihre Domains *neue* Kunden kostenlos.
**Das Rechenbeispiel:**
* Du hast 100 User.
* Jeder User hat 10 Domains auf "Pounce Yield" gestellt.
* = **1.000 digitale Plakatwände** im Internet, die für Pounce werben.
* Selbst wenn nur 1 Person pro Monat pro Domain klickt = 1.000 Klicks.
* Bei 1% Conversion = **10 neue Abos pro Monat geschenkt.**
### Fazit
Ja, wir machen das!
Aber wir machen es schlau:
1. **Footer-Link:** Überall. Immer.
2. **Full-Page Ad:** Nur, wenn wir keinen besseren (lukrativeren) Intent erkennen oder der User es als "Default" eingestellt hat.
Das schützt die Einnahmen deiner User (High Yield) und treibt gleichzeitig dein Wachstum (Viral Loop) voran. Das ist die perfekte Balance.