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
1954 lines
53 KiB
TypeScript
1954 lines
53 KiB
TypeScript
/**
|
|
* 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<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const url = `${getApiBaseUrl()}${endpoint}`
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers as Record<string, string>,
|
|
}
|
|
|
|
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<string, unknown>
|
|
}>
|
|
}>
|
|
}>(`/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<void>(`/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<any[]>('/listings')
|
|
}
|
|
|
|
// My Listings
|
|
async getMyListings() {
|
|
return this.request<any[]>('/listings/my')
|
|
}
|
|
|
|
async createListing(data: { domain: string; asking_price: number | null; currency: string; price_type: string }) {
|
|
return this.request<any>('/listings', { method: 'POST', body: JSON.stringify(data) })
|
|
}
|
|
|
|
async deleteListing(id: number) {
|
|
return this.request<void>(`/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<any>(`/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<any[]>(`/listings/${id}/inquiries`)
|
|
}
|
|
|
|
async updateListingInquiry(
|
|
listingId: number,
|
|
inquiryId: number,
|
|
data: { status: 'new' | 'read' | 'replied' | 'closed' | 'spam'; reason?: string | null }
|
|
) {
|
|
return this.request<any>(`/listings/${listingId}/inquiries/${inquiryId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async getMyInquiryThreads() {
|
|
return this.request<Array<{
|
|
id: number
|
|
listing_id: number
|
|
domain: string
|
|
slug: string
|
|
status: string
|
|
created_at: string
|
|
closed_at: string | null
|
|
closed_reason: string | null
|
|
}>>('/listings/inquiries/my')
|
|
}
|
|
|
|
async getInquiryMessagesAsBuyer(inquiryId: number) {
|
|
return this.request<any[]>(`/listings/inquiries/${inquiryId}/messages`)
|
|
}
|
|
|
|
async sendInquiryMessageAsBuyer(inquiryId: number, body: string) {
|
|
return this.request<any>(`/listings/inquiries/${inquiryId}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ body }),
|
|
})
|
|
}
|
|
|
|
async getInquiryMessagesAsSeller(listingId: number, inquiryId: number) {
|
|
return this.request<any[]>(`/listings/${listingId}/inquiries/${inquiryId}/messages`)
|
|
}
|
|
|
|
async sendInquiryMessageAsSeller(listingId: number, inquiryId: number, body: string) {
|
|
return this.request<any>(`/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<DomainHealthReport>(`/domains/${domainId}/health${refreshParam}`)
|
|
}
|
|
|
|
// Quick health check for any domain (premium)
|
|
async quickHealthCheck(domain: string) {
|
|
return this.request<DomainHealthReport>(`/domains/health-check?domain=${encodeURIComponent(domain)}`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
// Bulk cached health reports for watchlist UI (fast)
|
|
async getDomainsHealthCache() {
|
|
return this.request<{
|
|
reports: Record<string, DomainHealthReport>
|
|
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<PortfolioDomain[]>(`/portfolio?${params.toString()}`)
|
|
}
|
|
|
|
async getPortfolioSummary() {
|
|
return this.request<PortfolioSummary>('/portfolio/summary')
|
|
}
|
|
|
|
async addPortfolioDomain(data: PortfolioDomainCreate) {
|
|
return this.request<PortfolioDomain>('/portfolio', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async getPortfolioDomain(id: number) {
|
|
return this.request<PortfolioDomain>(`/portfolio/${id}`)
|
|
}
|
|
|
|
async updatePortfolioDomain(id: number, data: Partial<PortfolioDomainCreate>) {
|
|
return this.request<PortfolioDomain>(`/portfolio/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async deletePortfolioDomain(id: number) {
|
|
return this.request<void>(`/portfolio/${id}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
async markDomainSold(id: number, saleDate: string, salePrice: number) {
|
|
return this.request<PortfolioDomain>(`/portfolio/${id}/sell`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
sale_date: saleDate,
|
|
sale_price: salePrice,
|
|
}),
|
|
})
|
|
}
|
|
|
|
async refreshDomainValue(id: number) {
|
|
return this.request<PortfolioDomain>(`/portfolio/${id}/refresh-value`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
// ============== Portfolio DNS Verification ==============
|
|
|
|
async startPortfolioDnsVerification(id: number) {
|
|
return this.request<DNSVerificationStart>(`/portfolio/${id}/verify-dns`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async checkPortfolioDnsVerification(id: number) {
|
|
return this.request<DNSVerificationCheck>(`/portfolio/${id}/verify-dns/check`)
|
|
}
|
|
|
|
async getVerifiedPortfolioDomains() {
|
|
return this.request<PortfolioDomain[]>('/portfolio/verified')
|
|
}
|
|
|
|
async getDomainValuation(domain: string) {
|
|
return this.request<DomainValuation>(`/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<string, any>
|
|
}>(`/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<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
|
|
}>>(`/auctions/ending-soon?hours=${hours}&limit=${limit}`)
|
|
}
|
|
|
|
async getHotAuctions(limit = 10) {
|
|
return this.request<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
|
|
}>>(`/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<Array<{
|
|
platform: string
|
|
active_auctions: number
|
|
avg_bid: number
|
|
ending_soon: number
|
|
}>>('/auctions/stats')
|
|
}
|
|
|
|
// ============== Price Alerts ==============
|
|
|
|
async getPriceAlerts(activeOnly = false) {
|
|
const params = activeOnly ? '?active_only=true' : ''
|
|
return this.request<PriceAlert[]>(`/price-alerts${params}`)
|
|
}
|
|
|
|
async createPriceAlert(tld: string, targetPrice?: number, thresholdPercent = 5) {
|
|
return this.request<PriceAlert>('/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<PriceAlert>(`/price-alerts/${tld.replace(/^\./, '')}`)
|
|
}
|
|
|
|
async updatePriceAlert(tld: string, data: { is_active?: boolean; target_price?: number; threshold_percent?: number }) {
|
|
return this.request<PriceAlert>(`/price-alerts/${tld.replace(/^\./, '')}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async deletePriceAlert(tld: string) {
|
|
return this.request<void>(`/price-alerts/${tld.replace(/^\./, '')}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
async togglePriceAlert(tld: string) {
|
|
return this.request<PriceAlert>(`/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<any>('/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<any>(`/admin/users?${params}`)
|
|
}
|
|
|
|
async getAdminUser(userId: number) {
|
|
return this.request<any>(`/admin/users/${userId}`)
|
|
}
|
|
|
|
async updateAdminUser(userId: number, data: { name?: string; is_active?: boolean; is_verified?: boolean; is_admin?: boolean }) {
|
|
return this.request<any>(`/admin/users/${userId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async deleteAdminUser(userId: number) {
|
|
return this.request<any>(`/admin/users/${userId}`, { method: 'DELETE' })
|
|
}
|
|
|
|
async upgradeUser(userId: number, tier: string) {
|
|
return this.request<any>(`/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<any>(`/admin/newsletter?${params}`)
|
|
}
|
|
|
|
async exportNewsletterEmails() {
|
|
return this.request<{ emails: string[]; count: number }>('/admin/newsletter/export')
|
|
}
|
|
|
|
// TLD Scraping
|
|
async triggerTldScrape() {
|
|
return this.request<any>('/admin/scrape-tld-prices', { method: 'POST' })
|
|
}
|
|
|
|
async getTldPriceStats() {
|
|
return this.request<any>('/admin/tld-prices/stats')
|
|
}
|
|
|
|
// Auction Scraping
|
|
async triggerAuctionScrape() {
|
|
return this.request<any>('/auctions/admin/scrape', { method: 'POST' })
|
|
}
|
|
|
|
async getAuctionScrapeStatus() {
|
|
return this.request<any>('/auctions/admin/scrape-status')
|
|
}
|
|
|
|
// System
|
|
async getSystemHealth() {
|
|
return this.request<any>('/admin/system/health')
|
|
}
|
|
|
|
async makeUserAdmin(email: string) {
|
|
return this.request<any>(`/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<any>('/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<any>('/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<any>(`/blog/admin/posts/${postId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async deleteBlogPost(postId: number) {
|
|
return this.request<any>(`/blog/admin/posts/${postId}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
async publishBlogPost(postId: number) {
|
|
return this.request<any>(`/blog/admin/posts/${postId}/publish`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async unpublishBlogPost(postId: number) {
|
|
return this.request<any>(`/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<YieldDomain>(`/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<YieldDomain>(`/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<YieldPartner[]>(`/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<string, number>
|
|
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()
|
|
|