/** * API client for pounce backend * * API URL is determined dynamically based on the current hostname: * - localhost/127.0.0.1 → http://localhost:8000/api/v1 * - Local network IPs (10.x, 192.168.x) → http://{hostname}:8000/api/v1 * - Production (any other domain) → https://{hostname}/api/v1 (requires reverse proxy) */ const getApiBase = (): string => { // Server-side rendering: use environment variable or localhost if (typeof window === 'undefined') { // Prefer internal backend URL (server-only) when running in Docker/SSR const backendUrl = process.env.BACKEND_URL?.replace(/\/$/, '') const configured = (backendUrl ? `${backendUrl}/api/v1` : process.env.NEXT_PUBLIC_API_URL) || 'http://localhost:8000/api/v1' const normalized = configured.replace(/\/$/, '') return normalized.endsWith('/api/v1') ? normalized : `${normalized}/api/v1` } const { protocol, hostname, port } = window.location // Localhost development if (hostname === 'localhost' || hostname === '127.0.0.1') { return 'http://localhost:8000/api/v1' } // Local network (10.x.x.x, 192.168.x.x, 172.16-31.x.x) if (/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(hostname)) { return `http://${hostname}:8000/api/v1` } // Production: use same protocol and domain with /api/v1 path // This requires a reverse proxy (nginx/caddy) to route /api/v1 to the backend return `${protocol}//${hostname}/api/v1` } // Lazy-evaluated to ensure window is available on client let _apiBase: string | null = null const getApiBaseUrl = (): string => { if (_apiBase === null) { _apiBase = getApiBase() } return _apiBase } interface ApiError { detail: string } class ApiClient { get baseUrl(): string { return getApiBaseUrl().replace('/api/v1', '') } async request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${getApiBaseUrl()}${endpoint}` const headers: Record = { 'Content-Type': 'application/json', ...options.headers as Record, } const response = await fetch(url, { ...options, headers, credentials: 'include', // send HttpOnly auth cookie }) if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'An error occurred' })) const errorMessage = typeof errorData.detail === 'string' ? errorData.detail : (errorData.message || JSON.stringify(errorData.detail) || 'An error occurred') throw new Error(errorMessage) } if (response.status === 204) { return {} as T } return response.json() } // Auth async register(email: string, password: string, name?: string, ref?: string) { return this.request<{ id: number; email: string }>('/auth/register', { method: 'POST', body: JSON.stringify({ email, password, name, ref }), }) } async getReferralLink() { return this.request<{ invite_code: string url: string stats?: { window_days: number referred_users_total: number qualified_referrals_total: number referral_link_views_window: number bonus_domains: number next_reward_at: number badge: string | null cooldown_days?: number disqualified_cooldown_total?: number disqualified_missing_ip_total?: number disqualified_shared_ip_total?: number disqualified_duplicate_ip_total?: number } }>('/auth/referral') } async login(email: string, password: string) { const response = await this.request<{ expires_in: number }>( '/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), } ) return response } async logout() { // Clears auth cookie on the backend return this.request<{ message: string; success: boolean }>('/auth/logout', { method: 'POST', }) } async getMe() { return this.request<{ id: number email: string name: string | null is_active: boolean is_admin: boolean is_verified: boolean created_at: string }>('/auth/me') } // Dashboard (Terminal Radar) - single call payload async getDashboardSummary() { return this.request<{ market: { total_auctions: number ending_soon: number ending_soon_preview: Array<{ domain: string current_bid: number time_remaining: string platform: string affiliate_url?: string }> } listings: { active: number; sold: number; draft: number; total: number } tlds: { trending: Array<{ tld: string; reason: string; price_change: number; current_price: number }> } timestamp: string }>('/dashboard/summary') } // Analyze (Alpha Terminal - Diligence) async analyzeDomain(domain: string, opts?: { fast?: boolean; refresh?: boolean }) { const params = new URLSearchParams() if (opts?.fast) params.set('fast', 'true') if (opts?.refresh) params.set('refresh', 'true') const qs = params.toString() return this.request<{ domain: string computed_at: string cached: boolean sections: Array<{ key: string title: string items: Array<{ key: string label: string value: unknown | null status: string source: string details: Record }> }> }>(`/analyze/${encodeURIComponent(domain)}${qs ? `?${qs}` : ''}`) } // HUNT (Alpha Terminal - Discovery) async getHuntBargainBin(limit: number = 100) { const qs = new URLSearchParams({ limit: String(limit) }) return this.request<{ items: Array<{ domain: string platform: string auction_url: string current_bid: number currency: string end_time: string age_years: number | null backlinks: number | null pounce_score: number | null }> total: number filtered_out_missing_data: number last_updated: string | null }>(`/hunt/bargain-bin?${qs}`) } async getHuntTrends(geo: string = 'US') { const qs = new URLSearchParams({ geo }) return this.request<{ geo: string items: Array<{ title: string; approx_traffic?: string | null; published_at?: string | null; link?: string | null }> fetched_at: string }>(`/hunt/trends?${qs}`) } async huntKeywords(payload: { keywords: string[]; tlds?: string[] }) { return this.request<{ items: Array<{ keyword: string; domain: string; tld: string; is_available: boolean | null; status: string }> }>(`/hunt/keywords`, { method: 'POST', body: JSON.stringify(payload), }) } async huntTypos(payload: { brand: string; tlds?: string[]; limit?: number }) { return this.request<{ brand: string items: Array<{ domain: string; is_available: boolean | null; status: string }> }>(`/hunt/typos`, { method: 'POST', body: JSON.stringify(payload), }) } async huntBrandables(payload: { pattern: string; tlds?: string[]; limit?: number; max_checks?: number }) { return this.request<{ pattern: string items: Array<{ domain: string; is_available: boolean | null; status: string }> }>(`/hunt/brandables`, { method: 'POST', body: JSON.stringify(payload), }) } // CFO (Alpha Terminal - Management) async getCfoSummary() { return this.request<{ computed_at: string upcoming_30d_total_usd: number upcoming_30d_rows: Array<{ domain_id: number domain: string renewal_date: string | null renewal_cost_usd: number | null cost_source: string is_sold: boolean }> monthly: Array<{ month: string; total_cost_usd: number; domains: number }> kill_list: Array<{ domain_id: number domain: string renewal_date: string | null renewal_cost_usd: number | null cost_source: string auto_renew: boolean is_dns_verified: boolean yield_net_60d: number yield_clicks_60d: number reason: string }> }>(`/cfo/summary`) } async cfoSetToDrop(domainId: number) { return this.request<{ domain_id: number; auto_renew: boolean; updated_at: string }>(`/cfo/domains/${domainId}/set-to-drop`, { method: 'POST', }) } async updateMe(data: { name?: string }) { return this.request<{ id: number email: string name: string | null is_active: boolean created_at: string }>('/auth/me', { method: 'PUT', body: JSON.stringify(data), }) } // Password Reset async forgotPassword(email: string) { return this.request<{ message: string; success: boolean }>('/auth/forgot-password', { method: 'POST', body: JSON.stringify({ email }), }) } async resetPassword(token: string, newPassword: string) { return this.request<{ message: string; success: boolean }>('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, new_password: newPassword }), }) } // Email Verification async verifyEmail(token: string) { return this.request<{ message: string; success: boolean }>('/auth/verify-email', { method: 'POST', body: JSON.stringify({ token }), }) } async resendVerification(email: string) { return this.request<{ message: string; success: boolean }>('/auth/resend-verification', { method: 'POST', body: JSON.stringify({ email }), }) } // OAuth async getOAuthProviders() { return this.request<{ google_enabled: boolean; github_enabled: boolean }>('/oauth/providers') } getGoogleLoginUrl(redirect?: string) { const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : '' return `${getApiBaseUrl()}/oauth/google/login${params}` } getGitHubLoginUrl(redirect?: string) { const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : '' return `${getApiBaseUrl()}/oauth/github/login${params}` } // Contact Form async submitContact(name: string, email: string, subject: string, message: string) { return this.request<{ message: string; success: boolean }>('/contact', { method: 'POST', body: JSON.stringify({ name, email, subject, message }), }) } // Newsletter async subscribeNewsletter(email: string) { return this.request<{ message: string; success: boolean }>('/contact/newsletter/subscribe', { method: 'POST', body: JSON.stringify({ email }), }) } async unsubscribeNewsletter(email: string, token?: string) { return this.request<{ message: string; success: boolean }>('/contact/newsletter/unsubscribe', { method: 'POST', body: JSON.stringify({ email, token }), }) } // Subscription - Stripe Integration async createCheckoutSession(plan: string, successUrl?: string, cancelUrl?: string) { return this.request<{ checkout_url: string; session_id: string }>('/subscription/checkout', { method: 'POST', body: JSON.stringify({ plan, success_url: successUrl, cancel_url: cancelUrl, }), }) } async createPortalSession() { return this.request<{ portal_url: string }>('/subscription/portal', { method: 'POST', }) } async cancelSubscription() { return this.request<{ status: string; message: string; new_tier: string }>('/subscription/cancel', { method: 'POST', }) } // Domain Check (public) async checkDomain(domain: string, quick = false) { return this.request<{ domain: string status: string is_available: boolean registrar: string | null expiration_date: string | null creation_date: string | null name_servers: string[] | null error_message: string | null checked_at: string }>('/check', { method: 'POST', body: JSON.stringify({ domain, quick }), }) } // Domains (authenticated) async getDomains(page = 1, perPage = 20) { return this.request<{ domains: Array<{ id: number name: string status: string is_available: boolean registrar: string | null expiration_date: string | null notify_on_available: boolean created_at: string last_checked: string | null }> total: number page: number per_page: number pages: number }>(`/domains?page=${page}&per_page=${perPage}`) } async addDomain(name: string, notify = true) { return this.request<{ id: number name: string status: string is_available: boolean }>('/domains', { method: 'POST', body: JSON.stringify({ name, notify_on_available: notify }), }) } async deleteDomain(id: number) { return this.request(`/domains/${id}`, { method: 'DELETE', }) } async refreshDomain(id: number) { return this.request<{ id: number name: string status: string is_available: boolean }>(`/domains/${id}/refresh`, { method: 'POST', }) } async updateDomainNotify(id: number, notify: boolean) { return this.request<{ id: number name: string notify: boolean }>(`/domains/${id}/notify`, { method: 'PATCH', body: JSON.stringify({ notify }), }) } async updateDomainExpiry(id: number, expirationDate: string | null) { return this.request<{ id: number name: string expiration_date: string | null }>(`/domains/${id}/expiry`, { method: 'PATCH', body: JSON.stringify({ expiration_date: expirationDate }), }) } // Marketplace Listings (Pounce Direct) async getMarketplaceListings() { return this.request('/listings') } // My Listings async getMyListings() { return this.request('/listings/my') } async createListing(data: { domain: string; asking_price: number | null; currency: string; price_type: string }) { return this.request('/listings', { method: 'POST', body: JSON.stringify(data) }) } async deleteListing(id: number) { return this.request(`/listings/${id}`, { method: 'DELETE' }) } async updateListing( id: number, data: { status?: string title?: string description?: string asking_price?: number sold_reason?: string | null sold_price?: number | null sold_currency?: string | null } ) { return this.request(`/listings/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } async startDnsVerification(id: number) { return this.request<{ verification_code: string dns_record_type: string dns_record_name: string dns_record_value: string instructions: string status: string }>(`/listings/${id}/verify-dns`, { method: 'POST' }) } async checkDnsVerification(id: number) { return this.request<{ verified: boolean status: string message: string }>(`/listings/${id}/verify-dns/check`) } async getListingInquiries(id: number) { return this.request(`/listings/${id}/inquiries`) } async updateListingInquiry( listingId: number, inquiryId: number, data: { status: 'new' | 'read' | 'replied' | 'closed' | 'spam'; reason?: string | null } ) { return this.request(`/listings/${listingId}/inquiries/${inquiryId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async getMyInquiryThreads() { return this.request>('/listings/inquiries/my') } async getInquiryMessagesAsBuyer(inquiryId: number) { return this.request(`/listings/inquiries/${inquiryId}/messages`) } async sendInquiryMessageAsBuyer(inquiryId: number, body: string) { return this.request(`/listings/inquiries/${inquiryId}/messages`, { method: 'POST', body: JSON.stringify({ body }), }) } async getInquiryMessagesAsSeller(listingId: number, inquiryId: number) { return this.request(`/listings/${listingId}/inquiries/${inquiryId}/messages`) } async sendInquiryMessageAsSeller(listingId: number, inquiryId: number, body: string) { return this.request(`/listings/${listingId}/inquiries/${inquiryId}/messages`, { method: 'POST', body: JSON.stringify({ body }), }) } // Subscription async getSubscription() { return this.request<{ id: number tier: string tier_name: string status: string domain_limit: number domains_used: number portfolio_limit: number check_frequency: string history_days: number features: { email_alerts: boolean priority_alerts: boolean full_whois: boolean expiration_tracking: boolean domain_valuation: boolean market_insights: boolean api_access: boolean webhooks: boolean bulk_tools: boolean seo_metrics: boolean } started_at: string expires_at: string | null }>('/subscription') } async getTiers() { return this.request<{ tiers: Array<{ id: string name: string domain_limit: number price: number features: string[] }> }>('/subscription/tiers') } // Domain History (Professional/Enterprise) async getDomainHistory(domainId: number, limit = 30) { return this.request<{ domain: string total_checks: number history: Array<{ id: number status: string is_available: boolean checked_at: string }> }>(`/domains/${domainId}/history?limit=${limit}`) } // Domain Health Check - 4-layer analysis (DNS, HTTP, SSL, WHOIS) async getDomainHealth(domainId: number, options?: { refresh?: boolean }) { const refreshParam = options?.refresh ? '?refresh=true' : '' return this.request(`/domains/${domainId}/health${refreshParam}`) } // Quick health check for any domain (premium) async quickHealthCheck(domain: string) { return this.request(`/domains/health-check?domain=${encodeURIComponent(domain)}`, { method: 'POST', }) } // Bulk cached health reports for watchlist UI (fast) async getDomainsHealthCache() { return this.request<{ reports: Record total_domains: number cached_domains: number timestamp: string }>('/domains/health-cache') } // TLD Pricing async getTldOverview( limit = 25, offset = 0, sortBy: 'popularity' | 'price_asc' | 'price_desc' | 'name' = 'popularity', search?: string ) { const params = new URLSearchParams({ limit: limit.toString(), offset: offset.toString(), sort_by: sortBy, }) if (search) { params.append('search', search) } return this.request<{ tlds: Array<{ tld: string type: string description: string avg_registration_price: number min_registration_price: number max_registration_price: number min_renewal_price: number avg_renewal_price: number registrar_count: number trend: string price_change_7d: number price_change_1y: number price_change_3y: number risk_level: 'low' | 'medium' | 'high' risk_reason: string popularity_rank?: number }> total: number limit: number offset: number has_more: boolean source: string }>(`/tld-prices/overview?${params.toString()}`) } async getTldHistory(tld: string, days = 90) { return this.request<{ tld: string type: string description: string registry: string current_price: number price_change_7d: number price_change_30d: number price_change_90d: number trend: string trend_reason: string history: Array<{ date: string price: number }> source: string }>(`/tld-prices/${tld}/history?days=${days}`) } async getTldCompare(tld: string) { return this.request<{ tld: string type: string description: string registry: string introduced: number | null registrars: Array<{ name: string registration_price: number renewal_price: number transfer_price: number }> cheapest_registrar: string cheapest_price: number price_range: { min: number max: number avg: number } }>(`/tld-prices/${tld}/compare`) } async getTrendingTlds() { return this.request<{ trending: Array<{ tld: string reason: string price_change: number current_price: number }> }>('/tld-prices/trending') } // Get TLD info for SEO pages async getTldInfo(tld: string) { try { const compare = await this.getTldCompare(tld) return { tld: compare.tld, type: compare.type, registration_price: compare.cheapest_price, renewal_price: compare.registrars?.[0]?.renewal_price || null, registrar_count: compare.registrars?.length || 0, description: compare.description, introduced: compare.introduced?.toString() || null, registry: compare.registry, } } catch { return null } } // Get TLD prices for SEO pages async getTldPrices(tld: string) { try { const [compare, history] = await Promise.all([ this.getTldCompare(tld), this.getTldHistory(tld, 365), ]) return { registrars: compare.registrars?.map(r => ({ registrar: r.name, registration: r.registration_price, renewal: r.renewal_price, transfer: r.transfer_price, })) || [], history: history.history || [], } } catch { return { registrars: [], history: [] } } } // ============== Portfolio ============== async getPortfolio( status?: string, sortBy = 'created_at', sortOrder = 'desc', limit = 100, offset = 0 ) { const params = new URLSearchParams({ sort_by: sortBy, sort_order: sortOrder, limit: limit.toString(), offset: offset.toString(), }) if (status) { params.append('status', status) } return this.request(`/portfolio?${params.toString()}`) } async getPortfolioSummary() { return this.request('/portfolio/summary') } async addPortfolioDomain(data: PortfolioDomainCreate) { return this.request('/portfolio', { method: 'POST', body: JSON.stringify(data), }) } async getPortfolioDomain(id: number) { return this.request(`/portfolio/${id}`) } async updatePortfolioDomain(id: number, data: Partial) { return this.request(`/portfolio/${id}`, { method: 'PUT', body: JSON.stringify(data), }) } async deletePortfolioDomain(id: number) { return this.request(`/portfolio/${id}`, { method: 'DELETE', }) } async markDomainSold(id: number, saleDate: string, salePrice: number) { return this.request(`/portfolio/${id}/sell`, { method: 'POST', body: JSON.stringify({ sale_date: saleDate, sale_price: salePrice, }), }) } async refreshDomainValue(id: number) { return this.request(`/portfolio/${id}/refresh-value`, { method: 'POST', }) } // ============== Portfolio DNS Verification ============== async startPortfolioDnsVerification(id: number) { return this.request(`/portfolio/${id}/verify-dns`, { method: 'POST', }) } async checkPortfolioDnsVerification(id: number) { return this.request(`/portfolio/${id}/verify-dns/check`) } async getVerifiedPortfolioDomains() { return this.request('/portfolio/verified') } async getDomainValuation(domain: string) { return this.request(`/portfolio/valuation/${domain}`) } // ============== Market Feed (Unified) ============== /** * Get unified market feed combining Pounce Direct listings + external auctions. * This is the main feed for the Market page. */ async getMarketFeed(options: { source?: 'all' | 'pounce' | 'external' keyword?: string tld?: string minPrice?: number maxPrice?: number minScore?: number endingWithin?: number verifiedOnly?: boolean sortBy?: 'score' | 'price_asc' | 'price_desc' | 'time' | 'newest' limit?: number offset?: number } = {}) { const params = new URLSearchParams() if (options.source) params.append('source', options.source) if (options.keyword) params.append('keyword', options.keyword) if (options.tld) params.append('tld', options.tld) if (options.minPrice !== undefined) params.append('min_price', options.minPrice.toString()) if (options.maxPrice !== undefined) params.append('max_price', options.maxPrice.toString()) if (options.minScore !== undefined) params.append('min_score', options.minScore.toString()) if (options.endingWithin !== undefined) params.append('ending_within', options.endingWithin.toString()) if (options.verifiedOnly) params.append('verified_only', 'true') if (options.sortBy) params.append('sort_by', options.sortBy) if (options.limit !== undefined) params.append('limit', options.limit.toString()) if (options.offset !== undefined) params.append('offset', options.offset.toString()) return this.request<{ items: Array<{ id: string domain: string tld: string price: number currency: string price_type: 'bid' | 'fixed' | 'negotiable' status: 'auction' | 'instant' source: string is_pounce: boolean verified: boolean time_remaining?: string end_time?: string num_bids?: number slug?: string seller_verified: boolean url: string is_external: boolean pounce_score: number }> total: number pounce_direct_count: number auction_count: number sources: string[] last_updated: string filters_applied: Record }>(`/auctions/feed?${params.toString()}`) } // ============== Auctions (Smart Pounce) ============== async getAuctions( keyword?: string, tld?: string, platform?: string, minBid?: number, maxBid?: number, endingSoon = false, sortBy = 'ending', limit = 100, offset = 0 ) { const params = new URLSearchParams({ sort_by: sortBy, limit: limit.toString(), offset: offset.toString(), ending_soon: endingSoon.toString(), }) if (keyword) params.append('keyword', keyword) if (tld) params.append('tld', tld) if (platform) params.append('platform', platform) if (minBid !== undefined) params.append('min_bid', minBid.toString()) if (maxBid !== undefined) params.append('max_bid', maxBid.toString()) return this.request<{ auctions: Array<{ domain: string platform: string platform_url: string current_bid: number currency: string num_bids: number end_time: string time_remaining: string buy_now_price: number | null reserve_met: boolean | null traffic: number | null age_years: number | null tld: string affiliate_url: string }> total: number platforms_searched: string[] last_updated: string }>(`/auctions?${params.toString()}`) } async getEndingSoonAuctions(hours = 1, limit = 10) { return this.request>(`/auctions/ending-soon?hours=${hours}&limit=${limit}`) } async getHotAuctions(limit = 10) { return this.request>(`/auctions/hot?limit=${limit}`) } async getAuctionOpportunities() { return this.request<{ opportunities: Array<{ auction: { domain: string platform: string platform_url: string current_bid: number currency: string num_bids: number end_time: string time_remaining: string buy_now_price: number | null reserve_met: boolean | null traffic: number | null age_years: number | null tld: string affiliate_url: string } analysis: { estimated_value: number current_bid: number value_ratio: number potential_profit: number opportunity_score: number recommendation: string } }> strategy_tips: string[] generated_at: string }>('/auctions/opportunities') } async getAuctionPlatformStats() { return this.request>('/auctions/stats') } // ============== Price Alerts ============== async getPriceAlerts(activeOnly = false) { const params = activeOnly ? '?active_only=true' : '' return this.request(`/price-alerts${params}`) } async createPriceAlert(tld: string, targetPrice?: number, thresholdPercent = 5) { return this.request('/price-alerts', { method: 'POST', body: JSON.stringify({ tld: tld.replace(/^\./, ''), target_price: targetPrice, threshold_percent: thresholdPercent, }), }) } async getPriceAlertStatus(tld: string) { return this.request<{ tld: string; has_alert: boolean; is_active: boolean }>( `/price-alerts/status/${tld.replace(/^\./, '')}` ) } async getPriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`) } async updatePriceAlert(tld: string, data: { is_active?: boolean; target_price?: number; threshold_percent?: number }) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`, { method: 'PUT', body: JSON.stringify(data), }) } async deletePriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`, { method: 'DELETE', }) } async togglePriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}/toggle`, { method: 'POST', }) } } // ============== Types ============== export interface PortfolioDomain { id: number domain: string purchase_date: string | null purchase_price: number | null purchase_registrar: string | null registrar: string | null renewal_date: string | null renewal_cost: number | null auto_renew: boolean estimated_value: number | null value_updated_at: string | null is_sold: boolean sale_date: string | null sale_price: number | null status: string notes: string | null tags: string | null roi: number | null // DNS Verification fields is_dns_verified: boolean verification_status: string // 'unverified' | 'pending' | 'verified' | 'failed' verification_code: string | null verified_at: string | null created_at: string updated_at: string } export interface DNSVerificationStart { domain_id: number domain: string verification_code: string dns_record_type: string dns_record_name: string dns_record_value: string instructions: string status: string } export interface DNSVerificationCheck { verified: boolean status: string message: string } export interface PortfolioSummary { total_domains: number active_domains: number sold_domains: number total_invested: number total_value: number total_sold_value: number unrealized_profit: number realized_profit: number overall_roi: number } export interface PortfolioDomainCreate { domain: string purchase_date?: string purchase_price?: number purchase_registrar?: string registrar?: string renewal_date?: string renewal_cost?: number auto_renew?: boolean notes?: string tags?: string } export interface DomainValuation { domain: string estimated_value: number currency: string scores: { length: number tld: number keyword: number brandability: number overall: number } factors: { length: number tld: string has_numbers: boolean has_hyphens: boolean is_dictionary_word: boolean } confidence: string source: string calculated_at: string valuation_formula?: string } export interface PriceAlert { id: number tld: string is_active: boolean target_price: number | null threshold_percent: number last_notified_at: string | null last_notified_price: number | null created_at: string } // Domain Health Check Types export type HealthStatus = 'healthy' | 'weakening' | 'parked' | 'critical' | 'unknown' export interface DomainHealthReport { domain: string status: HealthStatus score: number // 0-100 checked_at: string signals: string[] recommendations: string[] dns: { has_ns: boolean has_a: boolean has_mx: boolean nameservers: string[] is_parked: boolean parking_provider?: string error?: string } http: { is_reachable: boolean status_code?: number is_parked: boolean parking_keywords?: string[] content_length?: number error?: string } ssl: { has_certificate: boolean is_valid: boolean expires_at?: string days_until_expiry?: number issuer?: string error?: string } } // ============== Admin API Extension ============== class AdminApiClient extends ApiClient { // Admin Stats async getAdminStats() { return this.request('/admin/stats') } // Admin Users async getAdminUsers(limit: number = 50, offset: number = 0, search?: string) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }) if (search) params.append('search', search) return this.request(`/admin/users?${params}`) } async getAdminUser(userId: number) { return this.request(`/admin/users/${userId}`) } async updateAdminUser(userId: number, data: { name?: string; is_active?: boolean; is_verified?: boolean; is_admin?: boolean }) { return this.request(`/admin/users/${userId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async deleteAdminUser(userId: number) { return this.request(`/admin/users/${userId}`, { method: 'DELETE' }) } async upgradeUser(userId: number, tier: string) { return this.request(`/admin/users/${userId}/upgrade?tier=${tier}`, { method: 'POST' }) } // Newsletter async getAdminNewsletter(limit: number = 100, offset: number = 0, activeOnly: boolean = true) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset), active_only: String(activeOnly), }) return this.request(`/admin/newsletter?${params}`) } async exportNewsletterEmails() { return this.request<{ emails: string[]; count: number }>('/admin/newsletter/export') } // TLD Scraping async triggerTldScrape() { return this.request('/admin/scrape-tld-prices', { method: 'POST' }) } async getTldPriceStats() { return this.request('/admin/tld-prices/stats') } // Auction Scraping async triggerAuctionScrape() { return this.request('/auctions/admin/scrape', { method: 'POST' }) } async getAuctionScrapeStatus() { return this.request('/auctions/admin/scrape-status') } // System async getSystemHealth() { return this.request('/admin/system/health') } async makeUserAdmin(email: string) { return this.request(`/admin/system/make-admin?email=${encodeURIComponent(email)}`, { method: 'POST' }) } // Price Alerts async getAdminPriceAlerts(limit = 100, offset = 0) { return this.request<{ alerts: Array<{ id: number tld: string target_price: number | null alert_type: string created_at: string user: { id: number; email: string; name: string | null } }> total: number }>(`/admin/price-alerts?limit=${limit}&offset=${offset}`) } // Domain Health Check async triggerDomainChecks() { return this.request<{ message: string domains_queued: number started_at: string }>('/admin/domains/check-all', { method: 'POST' }) } // Email Test async sendTestEmail() { return this.request<{ message: string sent_to: string timestamp: string }>('/admin/system/test-email', { method: 'POST' }) } // Scheduler Status async getSchedulerStatus() { return this.request<{ scheduler_running: boolean jobs: Array<{ id: string name: string next_run: string | null trigger: string }> last_runs: { tld_scrape: string | null auction_scrape: string | null domain_check: string | null } timestamp: string }>('/admin/system/scheduler') } async listDbBackups(limit: number = 20) { const query = new URLSearchParams({ limit: String(limit) }) return this.request<{ backups: Array<{ name: string; path: string; size_bytes: number; modified_at: string }> }>( `/admin/system/backups?${query}` ) } async createDbBackup(verify: boolean = true) { const query = new URLSearchParams({ verify: String(verify) }) return this.request<{ status: string backup: { path: string; size_bytes: number; created_at: string; verified: boolean; verification_detail: string | null } }>(`/admin/system/backups?${query}`, { method: 'POST' }) } async runOpsAlertsNow() { return this.request('/admin/system/ops-alerts/run', { method: 'POST' }) } async getOpsAlertsHistory(limit: number = 100) { const query = new URLSearchParams({ limit: String(limit) }) return this.request<{ events: any[] }>(`/admin/system/ops-alerts/history?${query}`) } // User Export async exportUsersCSV() { return this.request<{ csv: string count: number exported_at: string }>('/admin/users/export') } // Bulk Upgrade async bulkUpgradeUsers(userIds: number[], tier: string) { return this.request<{ message: string tier: string upgraded: Array<{ user_id: number; email: string }> failed: Array<{ user_id: number; reason: string }> total_upgraded: number total_failed: number }>('/admin/users/bulk-upgrade', { method: 'POST', body: JSON.stringify({ user_ids: userIds, tier }), }) } // Activity Log async getActivityLog(limit = 50, offset = 0) { return this.request<{ logs: Array<{ id: number action: string details: string created_at: string admin: { id: number; email: string; name: string | null } }> total: number }>(`/admin/activity-log?limit=${limit}&offset=${offset}`) } // ============== Blog ============== async getBlogPosts(limit = 10, offset = 0, category?: string, tag?: string) { let url = `/blog/posts?limit=${limit}&offset=${offset}` if (category) url += `&category=${encodeURIComponent(category)}` if (tag) url += `&tag=${encodeURIComponent(tag)}` return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null tags: string[] is_published: boolean published_at: string | null created_at: string view_count: number author: { id: number; name: string | null } }> total: number limit: number offset: number }>(url) } async getBlogPost(slug: string) { return this.request<{ id: number title: string slug: string excerpt: string | null content: string cover_image: string | null category: string | null tags: string[] meta_title: string | null meta_description: string | null is_published: boolean published_at: string | null created_at: string updated_at: string view_count: number author: { id: number; name: string | null } }>(`/blog/posts/${slug}`) } async getBlogPostMeta(slug: string) { return this.request<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null tags: string[] meta_title: string | null meta_description: string | null is_published: boolean published_at: string | null created_at: string updated_at: string view_count: number author: { id: number; name: string | null } }>(`/blog/posts/${slug}/meta`) } async getBlogCategories() { return this.request<{ categories: Array<{ name: string; count: number }> }>('/blog/posts/categories') } async getFeaturedPosts(limit = 3) { return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null published_at: string | null }> }>(`/blog/posts/featured?limit=${limit}`) } // Admin Blog async getAdminBlogPosts(limit = 50, offset = 0, status?: 'published' | 'draft') { let url = `/blog/admin/posts?limit=${limit}&offset=${offset}` if (status) url += `&status_filter=${status}` return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null tags: string[] is_published: boolean published_at: string | null created_at: string view_count: number author: { id: number; name: string | null } }> total: number }>(url) } async createBlogPost(data: { title: string content: string excerpt?: string cover_image?: string category?: string tags?: string[] meta_title?: string meta_description?: string is_published?: boolean }) { return this.request('/blog/admin/posts', { method: 'POST', body: JSON.stringify(data), }) } async updateBlogPost(postId: number, data: { title?: string content?: string excerpt?: string cover_image?: string category?: string tags?: string[] meta_title?: string meta_description?: string is_published?: boolean }) { return this.request(`/blog/admin/posts/${postId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async deleteBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}`, { method: 'DELETE', }) } async publishBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}/publish`, { method: 'POST', }) } async unpublishBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}/unpublish`, { method: 'POST', }) } // ========================================================================= // YIELD / Intent Routing API // ========================================================================= async analyzeYieldDomain(domain: string) { return this.request<{ domain: string intent: { category: string subcategory: string | null confidence: number keywords_matched: string[] suggested_partners: string[] monetization_potential: 'high' | 'medium' | 'low' } value: { estimated_monthly_min: number estimated_monthly_max: number currency: string potential: string confidence: number geo: string | null } partners: string[] monetization_potential: string }>(`/yield/analyze?domain=${encodeURIComponent(domain)}`, { method: 'POST', }) } async getYieldDashboard() { return this.request<{ stats: { total_domains: number active_domains: number pending_domains: number monthly_revenue: number monthly_clicks: number monthly_conversions: number lifetime_revenue: number lifetime_clicks: number lifetime_conversions: number pending_payout: number next_payout_date: string | null currency: string } domains: YieldDomain[] recent_transactions: YieldTransaction[] top_domains: YieldDomain[] }>('/yield/dashboard') } async getYieldDomains(params?: { status?: string; limit?: number; offset?: number }) { const queryParams = new URLSearchParams() if (params?.status) queryParams.set('status', params.status) if (params?.limit) queryParams.set('limit', params.limit.toString()) if (params?.offset) queryParams.set('offset', params.offset.toString()) return this.request<{ domains: YieldDomain[] total: number total_active: number total_revenue: number total_clicks: number }>(`/yield/domains?${queryParams}`) } async getYieldDomain(domainId: number) { return this.request(`/yield/domains/${domainId}`) } async activateYieldDomain(domain: string, acceptTerms: boolean = true) { return this.request<{ domain_id: number domain: string status: string intent: { category: string subcategory: string | null confidence: number keywords_matched: string[] suggested_partners: string[] monetization_potential: string } value_estimate: { estimated_monthly_min: number estimated_monthly_max: number currency: string potential: string confidence: number geo: string | null } dns_instructions: { domain: string nameservers: string[] cname_host: string cname_target: string verification_url: string } message: string }>('/yield/activate', { method: 'POST', body: JSON.stringify({ domain, accept_terms: acceptTerms }), }) } async verifyYieldDomainDNS(domainId: number) { return this.request<{ domain: string verified: boolean expected_ns: string[] actual_ns: string[] cname_ok: boolean error: string | null checked_at: string }>(`/yield/domains/${domainId}/verify`, { method: 'POST', }) } async updateYieldDomain(domainId: number, data: { active_route?: string landing_page_url?: string status?: string }) { return this.request(`/yield/domains/${domainId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async deleteYieldDomain(domainId: number) { return this.request<{ message: string }>(`/yield/domains/${domainId}`, { method: 'DELETE', }) } async getYieldTransactions(params?: { domain_id?: number status?: string limit?: number offset?: number }) { const queryParams = new URLSearchParams() if (params?.domain_id) queryParams.set('domain_id', params.domain_id.toString()) if (params?.status) queryParams.set('status', params.status) if (params?.limit) queryParams.set('limit', params.limit.toString()) if (params?.offset) queryParams.set('offset', params.offset.toString()) return this.request<{ transactions: YieldTransaction[] total: number total_gross: number total_net: number }>(`/yield/transactions?${queryParams}`) } async getYieldPayouts(params?: { status?: string; limit?: number; offset?: number }) { const queryParams = new URLSearchParams() if (params?.status) queryParams.set('status', params.status) if (params?.limit) queryParams.set('limit', params.limit.toString()) if (params?.offset) queryParams.set('offset', params.offset.toString()) return this.request<{ payouts: YieldPayout[] total: number total_paid: number total_pending: number }>(`/yield/payouts?${queryParams}`) } async getYieldPartners(category?: string) { const params = category ? `?category=${encodeURIComponent(category)}` : '' return this.request(`/yield/partners${params}`) } async getTelemetryKpis(days: number = 30) { const query = new URLSearchParams({ days: String(days) }) return this.request<{ window: { days: number; start: string; end: string } deal: { listing_views: number inquiries_created: number seller_replied_inquiries: number inquiry_reply_rate: number listings_with_inquiries: number listings_sold: number inquiry_to_sold_listing_rate: number median_reply_seconds: number | null median_time_to_sold_seconds: number | null } yield: { connected_domains: number clicks: number conversions: number conversion_rate: number payouts_paid: number payouts_paid_amount_total: number } }>(`/telemetry/kpis?${query}`) } async getReferralKpis(days: number = 30, limit: number = 200, offset: number = 0) { const query = new URLSearchParams({ days: String(days), limit: String(limit), offset: String(offset) }) return this.request<{ window: { days: number; start: string; end: string } totals: Record referrers: Array<{ user_id: number email: string invite_code: string | null created_at: string referred_users_total: number referred_users_window: number referral_link_views_window: number }> }>(`/telemetry/referrals?${query}`) } // ============================================================================ // DROPS - Zone File Analysis (.ch / .li) // ============================================================================ async getDropsStats() { return this.request<{ ch: { domain_count: number; last_sync: string | null } li: { domain_count: number; last_sync: string | null } weekly_drops: number }>('/drops/stats') } async getDrops(params?: { tld?: 'ch' | 'li' | 'xyz' | 'org' | 'online' | 'info' | 'dev' | 'app' days?: number min_length?: number max_length?: number exclude_numeric?: boolean exclude_hyphen?: boolean keyword?: string limit?: number offset?: number }) { const query = new URLSearchParams() if (params?.tld) query.set('tld', params.tld) if (params?.days) query.set('days', params.days.toString()) if (params?.min_length) query.set('min_length', params.min_length.toString()) if (params?.max_length) query.set('max_length', params.max_length.toString()) if (params?.exclude_numeric) query.set('exclude_numeric', 'true') if (params?.exclude_hyphen) query.set('exclude_hyphen', 'true') if (params?.keyword) query.set('keyword', params.keyword) if (params?.limit) query.set('limit', params.limit.toString()) if (params?.offset) query.set('offset', params.offset.toString()) return this.request<{ total: number items: Array<{ domain: string tld: string dropped_date: string length: number is_numeric: boolean has_hyphen: boolean }> }>(`/drops?${query}`) } async getDropsTlds() { return this.request<{ tlds: Array<{ tld: string name: string flag: string registry: string }> }>('/drops/tlds') } } // Yield Types export interface YieldDomain { id: number domain: string status: 'pending' | 'verifying' | 'active' | 'paused' | 'inactive' | 'error' detected_intent: string | null intent_confidence: number active_route: string | null partner_name: string | null dns_verified: boolean dns_verified_at: string | null connected_at: string | null total_clicks: number total_conversions: number total_revenue: number currency: string activated_at: string | null created_at: string } export interface YieldTransaction { id: number event_type: 'click' | 'lead' | 'sale' partner_slug: string click_id?: string | null gross_amount: number net_amount: number currency: string status: 'pending' | 'confirmed' | 'paid' | 'rejected' geo_country: string | null created_at: string confirmed_at: string | null } export interface YieldPayout { id: number amount: number currency: string period_start: string period_end: string transaction_count: number status: 'pending' | 'processing' | 'completed' | 'failed' payment_method: string | null payment_reference: string | null created_at: string completed_at: string | null } export interface YieldPartner { slug: string name: string network: string intent_categories: string[] geo_countries: string[] payout_type: string description: string | null logo_url: string | null } export const api = new AdminApiClient()