import type { TrumpPost, Candle, BotTrade, BotPerformance, KolPostSummary, KolPostDetail, KolDigest, KolWallet, KolHoldingChange, KolDivergence, } from '@/types' import type { SignedEnvelope } from './signedRequest' const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000' function getApiOrigin() { return typeof window === 'undefined' ? `${BASE_URL}/api` : '/api/proxy/api' } async function fetchJson(path: string, init?: RequestInit): Promise { const res = await fetch(`${getApiOrigin()}${path}`, { headers: { 'Content-Type': 'application/json' }, ...init, }) if (!res.ok) { let detail = `API error ${res.status}` try { const j = await res.json() if (j.detail) detail = `${res.status}: ${j.detail}` } catch {} throw new Error(detail) } return res.json() as Promise } export async function getPosts(limit = 20, page = 1, source?: string): Promise { const q = `/posts?limit=${limit}&page=${page}` + (source ? `&source=${encodeURIComponent(source)}` : '') return fetchJson(q) } export async function getPost(id: number): Promise { return fetchJson(`/posts/${id}`) } // asset widened from 'BTC' | 'ETH' to string — chart will eventually support // SOL/TRUMP etc. once the backend /prices/{asset} endpoint covers them. export async function getPrices(asset: string, tf: string): Promise { return fetchJson(`/prices/${asset}?tf=${tf}`) } export async function getTrades( wallet: string, env: SignedEnvelope, limit = 20, page = 1, ): Promise { const qs = [ `wallet=${wallet.toLowerCase()}`, `ts=${env.timestamp}`, `sig=${encodeURIComponent(env.signature)}`, `limit=${limit}`, `page=${page}`, ].join('&') return fetchJson(`/trades?${qs}`) } export async function getPerformance( wallet: string, env: SignedEnvelope, ): Promise { const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}` return fetchJson(`/performance?${qs}`) } export async function subscribe( env: SignedEnvelope, options: { paper_mode?: boolean } = {}, ): Promise<{ status: string; wallet: string; paper_mode: boolean }> { // paper_mode is a top-level body field (not nested) so the canonical // signed body matches the backend's expectation: { paper_mode: true } | null. const payload = options.paper_mode ? { ...env, paper_mode: true } : env return fetchJson<{ status: string; wallet: string; paper_mode: boolean }>(`/subscribe`, { method: 'POST', body: JSON.stringify(payload), }) } export interface UserSettings { leverage: number position_size_usd: number take_profit_pct: number | null stop_loss_pct: number | null min_confidence: number daily_budget_usd: number | null /** System-2 (bottom reversal) leverage, 1–10. null = platform default. * Independent of `leverage` (Trump). The protective stop auto-scales to * this so the position is never exchange-liquidated. */ sys2_leverage: number | null /** System-2 risk mode: 'standard' (cycle-rider) or 'aggressive' * (separately-funded high-risk/high-explosiveness sleeve). */ sys2_mode?: 'standard' | 'aggressive' | null active_from: string | null // ISO UTC active_until: string | null // ISO UTC /** Master on/off switches for each module. false = module is disabled; * bot ignores all signals for that module even if subscribed. */ trump_enabled?: boolean | null macro_enabled?: boolean | null } export interface UserData { wallet_address: string active: boolean subscribed_at: string | null hl_api_key_set: boolean hl_api_key_masked: string | null paper_mode: boolean trades: BotTrade[] settings: UserSettings /** ISO-UTC timestamp until which the bot is manually armed (null = use schedule). */ manual_window_until: string | null } export async function setUserSettings( env: SignedEnvelope, settings: UserSettings, ): Promise { return fetchJson(`/user/${env.wallet}/settings`, { method: 'PUT', body: JSON.stringify({ ...env, settings }), }) } export interface UserPublic { wallet_address: string active: boolean hl_api_key_set: boolean /** Phase 1 operational state — surfaced on /signals page. */ paper_mode?: boolean manual_window_until?: string | null circuit_breaker_tripped_at?: string | null circuit_breaker_reason?: string | null /** Master Auto-Trade gate. false (default) = signals shown, not traded. */ auto_trade?: boolean } export interface SignalSource { source: string count: number latest: string | null } export async function getSignalSources(): Promise<{ sources: SignalSource[] }> { return fetchJson<{ sources: SignalSource[] }>(`/signals/sources`) } /** * Toggle paper mode for a wallet. Hits the dev endpoint — no signature * required. Only available in development environments where the dev router * is mounted; in production this will 404. */ export async function setPaperMode( wallet: string, enabled: boolean, ): Promise<{ status: string; paper_mode: boolean }> { return fetchJson<{ status: string; paper_mode: boolean }>( `/dev/paper-mode?wallet=${wallet.toLowerCase()}&enabled=${enabled}`, { method: 'POST' }, ) } // ─── Open positions + today stats ─────────────────────────────────────────── export interface OpenPosition { trade_id: number asset: string side: 'long' | 'short' entry_price: number current_price: number | null size_usd: number | null leverage: number | null opened_at: string hold_minutes: number unrealized_pct: number | null unrealized_usd: number | null is_paper: boolean trigger_post_id: number | null /** System-2 staged lifecycle. size_usd above is the OPEN notional after * de-risk; realized_usd is PnL already banked by partial de-risk. */ realized_usd?: number | null derisk_steps?: number addon_steps?: number /** Per-trade pyramiding switch. */ grow_mode?: boolean } export interface OpenPositionsResponse { wallet: string count: number positions: OpenPosition[] } export interface TodayStats { wallet: string realized_pnl_usd: number trades_closed: number wins: number losses: number open_count: number /** Cumulative PnL banked by staged de-risk on still-open positions. * Shown separately — not part of realized_pnl_usd (closed trades only). */ open_realized_usd?: number } export async function getOpenPositions( wallet: string, env: SignedEnvelope, ): Promise { const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}` return fetchJson(`/positions/open?${qs}`) } export async function getTodayStats( wallet: string, env: SignedEnvelope, ): Promise { const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}` return fetchJson(`/positions/today?${qs}`) } // ─── Scanner control (module 2) ───────────────────────────────────────────── export interface ScannerSummary { name: string enabled: boolean last_run_at: string | null last_status: string last_message: string | null last_fired_at: string | null total_runs: number total_fires: number consecutive_errors: number } export interface ScannersResponse { count: number enabled: number scanners: ScannerSummary[] } export async function getScanners(): Promise { return fetchJson(`/scanners`) } /** * Toggle ONE scanner. Signed + subscriber-gated on the backend: the wallet * must own the signature AND already be a subscriber. The signed body is * { enabled, name } — must match the backend's canonical hash exactly. */ export async function toggleScanner( env: SignedEnvelope, name: string, enabled: boolean, ): Promise<{ status: string; name: string; enabled: boolean }> { return fetchJson<{ status: string; name: string; enabled: boolean }>( `/scanners/${name}/toggle`, { method: 'POST', body: JSON.stringify({ ...env, name, enabled }), }, ) } export async function killAllScanners( env: SignedEnvelope, ): Promise<{ status: string; count: number }> { return fetchJson<{ status: string; count: number }>( `/scanners/all/disable`, { method: 'POST', body: JSON.stringify(env) }, ) } export async function reviveAllScanners( env: SignedEnvelope, ): Promise<{ status: string; count: number }> { return fetchJson<{ status: string; count: number }>( `/scanners/all/enable`, { method: 'POST', body: JSON.stringify(env) }, ) } export interface CloseTradeResponse { status: string trade_id: number exit_price: number | null pnl_usd: number | null reason: string note: string | null } /** * Manual emergency close. Requires a signed envelope produced for the action * "close_trade" with body { trade_id }. The backend re-checks wallet ownership. */ export async function manualCloseTrade( env: SignedEnvelope, trade_id: number, ): Promise { return fetchJson(`/positions/${trade_id}/close`, { method: 'POST', body: JSON.stringify({ ...env, trade_id }), }) } /** No-auth boolean state. Safe to call on every page load. */ export async function getUserPublic(wallet: string): Promise { return fetchJson(`/user/${wallet.toLowerCase()}/public`) } /** Full user data (trades, settings). Requires signed envelope. */ export async function getUser( wallet: string, env: SignedEnvelope, ): Promise { const qs = `ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}` return fetchJson(`/user/${wallet.toLowerCase()}?${qs}`) } export interface WindowAccuracy { checked: number correct: number accuracy_pct: number } export interface SignalAccuracyEntry { count: number m5: WindowAccuracy m15: WindowAccuracy m1h: WindowAccuracy } export interface SignalAccuracy { overall: { m5: WindowAccuracy; m15: WindowAccuracy; m1h: WindowAccuracy } by_signal: Record total_directional_signals: number } export async function getSignalAccuracy(): Promise { return fetchJson(`/signals/accuracy`) } export async function setHlApiKey( env: SignedEnvelope, api_key: string, ): Promise<{ status: string; masked_key: string }> { return fetchJson<{ status: string; masked_key: string }>( `/user/${env.wallet}/hl-api-key`, { method: 'PUT', body: JSON.stringify({ ...env, api_key }), }, ) } /** * Arm (or clear) the manual-window override. Pass `hours = 0` to disarm. * Backend stores `manual_window_until = now + hours`; when in the future the * bot trades regardless of the schedule. */ /** Master Auto-Trade switch (signed). enabled=true also acknowledges + * clears a tripped circuit breaker. */ export async function setAutoTrade( env: SignedEnvelope, enabled: boolean, ): Promise<{ auto_trade: boolean; circuit_breaker_cleared: boolean }> { return fetchJson<{ auto_trade: boolean; circuit_breaker_cleared: boolean }>( `/user/${env.wallet}/auto-trade`, { method: 'POST', body: JSON.stringify({ ...env, enabled }) }, ) } /** Per-trade Grow (pyramiding) switch (signed). */ export async function setTradeGrow( env: SignedEnvelope, trade_id: number, enabled: boolean, ): Promise<{ trade_id: number; grow_mode: boolean }> { return fetchJson<{ trade_id: number; grow_mode: boolean }>( `/positions/${trade_id}/grow`, { method: 'POST', body: JSON.stringify({ ...env, trade_id, enabled }) }, ) } export async function setManualWindow( env: SignedEnvelope, hours: number, ): Promise<{ manual_window_until: string | null }> { return fetchJson<{ manual_window_until: string | null }>( `/user/${env.wallet}/manual-window`, { method: 'POST', body: JSON.stringify({ ...env, hours }), }, ) } // ── KOL module ──────────────────────────────────────────────────── export async function getKolPosts(opts: { handle?: string; source?: string; limit?: number; page?: number } = {}): Promise<{ items: KolPostSummary[]; page: number; limit: number }> { const p = new URLSearchParams() if (opts.handle) p.set('handle', opts.handle) if (opts.source) p.set('source', opts.source) p.set('limit', String(opts.limit ?? 50)) p.set('page', String(opts.page ?? 1)) return fetchJson(`/kol/posts?${p.toString()}`) } export async function getKolPost(id: number): Promise { return fetchJson(`/kol/posts/${id}`) } export async function getKolDigest(days = 7): Promise { return fetchJson(`/kol/digest?days=${days}`) } export async function getKolWallets(handle?: string): Promise<{ wallets: KolWallet[] }> { const p = handle ? `?handle=${handle}` : '' return fetchJson<{ wallets: KolWallet[] }>(`/kol/wallets${p}`) } export async function addKolWallet(body: { handle: string; chain: string; address: string; label?: string; source_url?: string }): Promise { return fetchJson(`/kol/wallets`, { method: 'POST', body: JSON.stringify(body), }) } export async function getKolChanges(opts: { handle?: string; days?: number } = {}): Promise<{ window_days: number; since: string; count: number; changes: KolHoldingChange[] }> { const p = new URLSearchParams() if (opts.handle) p.set('handle', opts.handle) p.set('days', String(opts.days ?? 7)) return fetchJson(`/kol/changes?${p.toString()}`) } export async function getKolDivergence(opts: { handle?: string; ticker?: string; signal_type?: 'divergence' | 'alignment'; days?: number } = {}): Promise<{ window_days: number; since: string; count: number; items: KolDivergence[] }> { const p = new URLSearchParams() if (opts.handle) p.set('handle', opts.handle) if (opts.ticker) p.set('ticker', opts.ticker) if (opts.signal_type) p.set('signal_type', opts.signal_type) p.set('days', String(opts.days ?? 30)) return fetchJson(`/kol/divergence?${p.toString()}`) } // ── Funding rate reversal (BTC tab) ───────────────────────────────────────── export interface FundingSnapshot { ok: boolean error?: string asset?: string venue?: string cadence_hours?: number coverage_days?: number latest_rate_pct?: number last_24h_avg_pct?: number cum_30d_pct?: number extreme_threshold_pct?: number signal_fired?: boolean debug?: { reason?: string; direction?: string; [k: string]: unknown } history?: { t: number; rate_pct: number }[] } export async function getFundingSnapshot(): Promise { return fetchJson('/funding/snapshot') } // ── Macro snapshot (BTC page Macro Bottom tab) ────────────────────────────── export interface MacroSnapshot { ok: boolean error?: string date?: string captured_at?: string indicators?: { ahr999: number | null altcoin_season_index: number | null fear_greed: number | null fear_greed_label: string | null btc_dominance_pct: number | null eth_btc_ratio: number | null stablecoin_supply_usd: number | null etf_flow_net_usd_1d: number | null btc_open_interest_usd: number | null } composite_score?: number | null regime_label?: string | null } export async function getMacroSnapshot(): Promise { return fetchJson('/macro/snapshot') } // ── Telegram alerts ───────────────────────────────────────────────────────── export interface TelegramStatus { configured: boolean bot_username: string | null bound: boolean wallet_address?: string | null tg_username?: string | null chat_id?: number | null alerts_enabled?: boolean | null alert_trump?: boolean | null alert_btc_bottom?: boolean | null alert_funding?: boolean | null alert_kol_divergence?: boolean | null min_confidence?: number | null mute_from_hour?: number | null mute_until_hour?: number | null total_alerts_sent?: number | null } export interface TelegramInitResp { code: string; deep_link: string; expires_in_seconds: number } export async function getTelegramStatus(wallet: string): Promise { return fetchJson(`/telegram/${wallet}/status`) } // SignedEnvelope is already imported at the top of this file. Below we // reuse it directly for the Telegram helpers — no separate alias needed. export async function tgInit(wallet: string, env: SignedEnvelope): Promise { return fetchJson(`/telegram/${wallet}/init`, { method: 'POST', body: JSON.stringify(env), }) } export async function tgUnbind(wallet: string, env: SignedEnvelope): Promise<{ removed: number }> { return fetchJson(`/telegram/${wallet}/unbind`, { method: 'POST', body: JSON.stringify(env), }) } export async function tgTest(wallet: string, env: SignedEnvelope): Promise<{ sent: boolean }> { return fetchJson(`/telegram/${wallet}/test`, { method: 'POST', body: JSON.stringify(env), }) } export async function tgPreferences( wallet: string, env: SignedEnvelope, prefs: Partial<{ alerts_enabled: boolean; alert_trump: boolean; alert_btc_bottom: boolean alert_funding: boolean; alert_kol_divergence: boolean min_confidence: number; mute_from_hour: number; mute_until_hour: number }>, ): Promise { return fetchJson(`/telegram/${wallet}/preferences`, { method: 'POST', body: JSON.stringify({ ...env, ...prefs }), }) }