Files
trumpsignal-frontend/lib/api.ts
T
k 4c3c8c6f87 KOL count: 29 → 25 across marketing/SEO copy
Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists

Bundles other in-flight frontend work already in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:27 +08:00

624 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
TrumpPost, Candle, BotTrade, BotPerformance,
KolPostSummary, KolPostDetail, KolDigest,
KolWallet, KolHoldingChange, KolDivergence,
} from '@/types'
import type { SignedEnvelope } from './signedRequest'
function getServerApiBase() {
return (
process.env.API_BASE_URL ||
process.env.INTERNAL_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
'http://localhost:8000'
)
}
function getApiOrigin() {
return typeof window === 'undefined'
? `${getServerApiBase()}/api`
: '/api/proxy/api'
}
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
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<T>
}
export interface PostListResponse {
items: TrumpPost[]
total: number
page: number
limit: number
counts: {
all: number
actionable: number
buy: number
short: number
off_topic: number
}
source_counts: {
source: string
count: number
latest: string | null
}[]
}
export async function getPostsPage(
limit = 20,
page = 1,
source?: string,
filters?: {
sourceIn?: string[]
sourceNotIn?: string[]
archiveOnly?: boolean
sentiment?: 'bullish' | 'bearish' | 'neutral'
signal?: 'buy' | 'short' | 'actionable'
aiScoredOnly?: boolean
},
): Promise<PostListResponse> {
const params = new URLSearchParams({
limit: String(limit),
page: String(page),
})
if (source) params.set('source', source)
if (filters?.sourceIn?.length) params.set('source_in', filters.sourceIn.join(','))
if (filters?.sourceNotIn?.length) params.set('source_not_in', filters.sourceNotIn.join(','))
if (filters?.archiveOnly) params.set('archive_only', 'true')
if (filters?.sentiment) params.set('sentiment', filters.sentiment)
if (filters?.signal) params.set('signal', filters.signal)
if (filters?.aiScoredOnly) params.set('ai_scored_only', 'true')
const q = `/posts-paged?${params.toString()}`
return fetchJson<PostListResponse>(q)
}
export async function getPosts(limit = 20, page = 1, source?: string): Promise<TrumpPost[]> {
const q = `/posts?limit=${limit}&page=${page}` + (source ? `&source=${encodeURIComponent(source)}` : '')
return fetchJson<TrumpPost[]>(q)
}
export async function getPost(id: number): Promise<TrumpPost> {
return fetchJson<TrumpPost>(`/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<Candle[]> {
return fetchJson<Candle[]>(`/prices/${asset}?tf=${tf}`)
}
export async function getTrades(
wallet: string,
env: SignedEnvelope,
limit = 20,
page = 1,
): Promise<BotTrade[]> {
const qs = [
`wallet=${wallet.toLowerCase()}`,
`ts=${env.timestamp}`,
`sig=${encodeURIComponent(env.signature)}`,
`limit=${limit}`,
`page=${page}`,
].join('&')
return fetchJson<BotTrade[]>(`/trades?${qs}`)
}
export async function getPerformance(
wallet: string,
env: SignedEnvelope,
): Promise<BotPerformance> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<BotPerformance>(`/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, 110. 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<UserSettings> {
return fetchJson<UserSettings>(`/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
/** Per-system enable flags. bot_engine gates System-1 (Trump) on
* trump_enabled and System-2 (Macro) on macro_enabled. Auto-Trade ON
* with trump_enabled=false still does NOT open on a Trump signal. */
trump_enabled?: boolean
macro_enabled?: 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<OpenPositionsResponse> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<OpenPositionsResponse>(`/positions/open?${qs}`)
}
export async function getTodayStats(
wallet: string,
env: SignedEnvelope,
): Promise<TodayStats> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<TodayStats>(`/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<ScannersResponse> {
return fetchJson<ScannersResponse>(`/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<CloseTradeResponse> {
return fetchJson<CloseTradeResponse>(`/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<UserPublic> {
return fetchJson<UserPublic>(`/user/${wallet.toLowerCase()}/public`)
}
/** Full user data (trades, settings). Requires signed envelope. */
export async function getUser(
wallet: string,
env: SignedEnvelope,
): Promise<UserData> {
const qs = `ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<UserData>(`/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<string, SignalAccuracyEntry>
total_directional_signals: number
}
export async function getSignalAccuracy(): Promise<SignalAccuracy> {
return fetchJson<SignalAccuracy>(`/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; signalsOnly?: boolean; ticker?: string; days?: number; limit?: number; page?: number
} = {}): Promise<{ items: KolPostSummary[]; page: number; limit: number; total: number }> {
const p = new URLSearchParams()
if (opts.handle) p.set('handle', opts.handle)
if (opts.source) p.set('source', opts.source)
if (opts.signalsOnly) p.set('signals_only', 'true')
if (opts.ticker) p.set('ticker', opts.ticker)
if (opts.days) p.set('days', String(opts.days))
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<KolPostDetail> {
return fetchJson<KolPostDetail>(`/kol/posts/${id}`)
}
export async function getKolDigest(days = 7): Promise<KolDigest> {
return fetchJson<KolDigest>(`/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<KolWallet> {
return fetchJson<KolWallet>(`/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<FundingSnapshot> {
return fetchJson<FundingSnapshot>('/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<MacroSnapshot> {
return fetchJson<MacroSnapshot>('/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, env?: SignedEnvelope): Promise<TelegramStatus> {
// Pass signature when available so the backend returns full binding details.
// Without it the endpoint returns only `bound: boolean` to prevent third-party de-anonymisation.
if (env) {
const qs = new URLSearchParams({ timestamp: String(env.timestamp), signature: env.signature })
return fetchJson<TelegramStatus>(`/telegram/${wallet}/status?${qs}`)
}
return fetchJson<TelegramStatus>(`/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<TelegramInitResp> {
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<TelegramStatus> {
return fetchJson(`/telegram/${wallet}/preferences`, {
method: 'POST', body: JSON.stringify({ ...env, ...prefs }),
})
}