API reference

Every public HTTP route of the AiTrade backend.

Base URL and auth

Base URL https://aitrade.thundera.eu. Authenticated routes accept the thundera_token cookie or Authorization: Bearer <jwt>. Chat, Binance account helpers and the activity log are served by the assistant service under /api/chat, /api/binance and /api/system/activity-log; ThunderScript compile/backtest are reached through the backend.

1. Cross-cutting

1.1 Auth

  • Public (no auth middleware): GET /health, GET /api/plans, GET /api/leaderboard, Swagger UI GET /api/docs + GET /api/openapi.json (spec has empty paths/schemas, only title "AiTrade API" v0.1.0).
  • /metrics is not routed publicly (Prometheus scrapes the service in-cluster). GET /ws requires authentication (section 3).
  • Authenticated = every other route (layer require_auth over all protected routers). Token taken from cookie thundera_token, else header Authorization: Bearer <jwt>. Cookie wins if both present.
  • JWT: HS256, secret THUNDERA_JWT_SECRET, issuer must be https://auth.thundera.eu, audience *.thundera.eu, exp validated. Claims: sub (uuid), email, name, roles[], optional sid, gen, trusted, type, exp. A token with type other than access (a refresh token) → 401.
  • On each request the user is upserted into app_users (key thundera_id=sub; updates name, last_seen); plan is read from app_users.plan (NOT from the JWT claim). Internal user.id = app_users.id (all data scoped to it).
  • CSRF check (POST/PUT/PATCH/DELETE on protected routes): Origin (else Referer) must be an https:// URL whose host is thundera.eu or ends with .thundera.eu (parsed, not substring-matched: evilthundera.eu and aitrade.thundera.eu.attacker.com are rejected). If neither header is present, allowed only when an Authorization header exists. Otherwise → 401 {"error":"Unauthorized"}. (Runs before the token check.)
  • System admin (/api/system/*): roles claim must contain system-admin or system-super-admin (same rule as auth-service /system; a plain admin role is not enough). Else 403 {"error":"Forbidden"}.
  • Ownership: every id from the client (bot, wallet, BYO LLM, script, position, notification, strategy) is resolved against the caller; a foreign id and an unknown id both give 404 (never 200 and never 403, so existence is not revealed). A database trigger (bots_ownership_guard) additionally rejects a bot linked to another user's wallet, BYO LLM or script.

1.2 Error envelope

All AppErrors → {"error": "<message>"} with Content-Type: application/json.

Variant Status message
Unauthorized 401 Unauthorized
Forbidden 403 Forbidden
NotFound(m) 404 m
BadRequest(m) 400 m
Conflict(m) 409 m
Internal(m) 500 m
sqlx error 500 Database error (logged)
anyhow 500 Internal error

Not in the envelope: axum extractor rejections (malformed JSON → 400, missing/invalid field types → 422, wrong/missing Content-Type: application/json → 415, bad path UUID / bad query → 400) return axum's default plain-text body. /api/plans upstream failures use {"error":"upstream_bad_response"|"upstream_error"|"billing_unavailable"|"internal"}.

1.3 CORS

CorsLayer: allowed origin = any origin ending in .thundera.eu or exactly https://thundera.eu; allow_credentials(true); methods GET, POST, PATCH, DELETE, OPTIONS (note: PUT is not listed, so a cross-origin PUT /api/workspace-state/{device_type} would fail preflight; same-origin unaffected); headers Content-Type, Authorization, Cookie.

1.4 Rate limits

None implemented in the backend (no rate-limit layer/crate in code). Any throttling would be at ingress/edge (unclear). Related caps: bot limit (plan), 10 BYO LLMs per user, /api/plans 60 s cache, upstream timeouts (compile 10 s, backtest 120 s).

1.5 Plan limits

Only enforced in POST /api/bots (bot count). Limit resolution order: user_config_overrides.bot_limit → live GET {AUTH_INTERNAL_URL or http://auth-service.thundera.svc.cluster.local:3000}/api/v1/auth/billing/effective-plan/aitrade (Bearer = user JWT, 3 s) .limits.bot_limit → /billing/plans/by-tier/aitrade/{plan} → hardcoded free=1, pro=2, max=5, thunder=20, other=2. /api/chat additionally rejects plan pro. indicator_limit, instructions_limit, chat_enabled overrides can be set by admin but are not read by any handler in api/* (unclear whether engine uses them).

1.6 Conventions

Decimals (rust_decimal::Decimal) serialize per the crate default (JSON strings unless the serde-float feature is enabled — unclear from source). Timestamps RFC3339 UTC. IDs are UUIDs. Unknown JSON body fields are ignored.


2. Health / Metrics / Docs

Method Path Auth Response
GET /health public {"status":"healthy","service":"aitrade","version":"<CARGO_PKG_VERSION>"} (does not touch DB)
GET /metrics public Prometheus text (text/plain; version=0.0.4). Metrics: aitrade_requests_total, aitrade_active_bots, aitrade_trades_total, aitrade_active_connections (only requests counter + ws gauge are updated in these files)
GET /api/docs, /api/openapi.json public Swagger UI / near-empty OpenAPI doc

3. WebSocket GET /ws

  • Auth: required on the upgrade: thundera_token cookie (browsers) or Authorization: Bearer (native). No token → 401. A present Origin must be a Thundera origin (same rule as CSRF) → else 403; no Origin is accepted only with a Bearer token.
  • Server sends {"type":"hello"}. Client messages (JSON, max 4 KB): {"action":"subscribe","bot_id":…} → {"type":"subscribed","bot_id"} for the caller's own bot, else {"type":"error","error":"Bot not found"}; {"action":"unsubscribe","bot_id"}; {"action":"ping"} → {"type":"pong"}. Max 20 subscriptions. Every 5 s the server pushes {"type":"bot",bot_id,status,current_capital,total_pnl,total_trades,last_decision_at} per subscribed bot (re-scoped to the caller each time). Input is never echoed. aitrade_active_connections gauge inc/dec.

4. Wallets (auth: user)

Wallet types: spot_testnet→https://testnet.binance.vision (testnet), spot_live→https://api.binance.com, futures_testnet→https://testnet.binancefuture.com (testnet), futures_live→https://fapi.binance.com. Keys stored AES-256-GCM encrypted (ENCRYPTION_KEY); never returned.

GET /api/wallets

Response: array of {id, display_name, is_testnet, wallet_type, is_valid, api_key_hint, created_at}; api_key_hint is always •••• (no key material). Ordered newest first.

POST /api/wallets

Body: display_name string req (1..80); binance_api_key string req and binance_secret_key string req (16..256 alphanumeric); wallet_type string opt (one of 4 above); is_testnet bool opt default false (legacy; used only if wallet_type absent → spot_testnet/spot_live). The key pair is checked with a signed read-only account call against the wallet's own environment (spot /api/v3/account, futures /fapi/v2/account). Response: {"id","status":"created","wallet_type","is_valid":bool,"validation":"valid|invalid|unverified"}. is_valid is true only on a Binance 200; unverified (geo-block 451, network error, 5xx) is stored as false. Each secret is encrypted with its own nonce. Errors: 400 Invalid wallet_type '<t>'. Allowed: spot_testnet, spot_live, futures_testnet, futures_live; 500 encryption errors. No wallet-count limit.

PATCH /api/wallets/{id}

Body (all opt): display_name (trimmed non-empty, ≤80 else 400 display_name must be 1..80 chars), wallet_type (validated as above; updates base_url+is_testnet). Changing wallet_type resets is_valid to false. Response {"status":"updated"}; 404 Wallet not found for a foreign or unknown id.

PATCH /api/wallets/{id}/keys

Body: binance_api_key req, binance_secret_key req. Validated like POST against the wallet's own environment. Response {"status":"updated","is_valid":bool,"validation":"valid|invalid|unverified"}. Errors: 404 Wallet not found; 400 for malformed keys.

DELETE /api/wallets/{id}

Response {"status":"deleted"}. Errors: 409 Cannot delete: N bot(s) are using this wallet. Remove or reassign bots first.; 404 Wallet not found.


5. Bots (auth: user)

BotResponse fields: id, name, status, mode, ai_provider, ai_model, symbols[], indicators[]|null, initial_capital, current_capital, total_pnl, total_trades, winning_trades, last_decision_at, last_regime, last_indicators(json), is_paper (=wallet.is_testnet, default true), created_at, custom_instructions, risk_config(json), mode_config(json), leverage, margin_type, leverage_locked, decide_all_symbols, quote_currency, wallet_id, user_llm_id, supervisor_enabled, supervisor_auto_apply, supervisor_prompt, supervisor_last_run, supervisor_total_runs, supervisor_total_changes, margin_in_positions, unrealized_pnl, total_value (total_value = current_capital + open margin + open unrealized_pnl). Status values seen: active, paused, stopped, deleted.

GET /api/bots

array of BotResponse (newest first)

GET /api/bots/{id}

BotResponse; 404 Bot not found

POST /api/bots

field type req default / notes
name string yes no validation
wallet_id uuid yes not verified to belong to user
symbols string[] yes
initial_capital number yes also sets current_capital; unrepresentable → 0
mode string no autonomous (not validated on create)
ai_provider string no deepseek
ai_model string no deepseek-v4-flash
custom_instructions string no ""
refresh_interval_secs int no 300 (no minimum on create)
indicators string[] no []
indicator_mode string no selected
risk_config / mode_config object no {}
leverage int no 1 (max(1))
margin_type string no spot
decide_all_symbols bool no false
quote_currency string no USDT
user_llm_id uuid no BYO LLM
supervisor_enabled / supervisor_auto_apply bool no true / true
supervisor_prompt string no "" (no 500 limit on create)

Response {"id","status":"created"}. Validation: name 1..80, symbols ≤50 upper-case pairs, initial_capital > 0, leverage 1..125, margin_type spot|isolated|cross, mode one of the five (normalised), quote_currency 2-15 upper-case, custom_instructions ≤4000, refresh_interval_secs ≥30. wallet_id and user_llm_id must be the caller's → else 404 Wallet not found / LLM not found. Errors: 409 Bot limit reached (count/limit) (count excludes status='deleted'); 400 validation.

PATCH /api/bots/{id}

Body (all optional): name, mode, custom_instructions, indicators[], indicator_mode, risk_config, mode_config, leverage, margin_type, refresh_interval_secs, leaderboard_opt_in, decide_all_symbols, symbols[], ai_provider, ai_model, user_llm_id. - mode: normalized (trim, lowercase, spaces/-→_); must be autonomous|dca|grid|momentum|mean_reversion else 400 Invalid mode '<m>'. Valid modes: autonomous, dca, grid, momentum, mean_reversion. - indicators, indicator_mode, mode_config, refresh_interval_secs, symbols while status=='active' → 400 These fields require a paused bot (changing them invalidates indicator history or orphans positions). Pause first, then edit. - leverage/margin_type after first trade (leverage_locked) → 400 Leverage and margin type are locked after the bot's first trade. Create a new bot to change them.; leverage clamped max(1); refresh_interval_secs clamped max(30). - risk_config.max_position_size_pct is renamed to max_position_pct if the latter is absent. - The whole body is validated (including user_llm_id ownership → 404, paused-only fields, leverage lock) before anything is written. Response {"status":"updated"}; 404 Bot not found.

DELETE /api/bots/{id}

{"status":"deleted"} (hard DELETE); 404 Bot not found for a foreign or unknown id.

POST /api/bots/{id}/start

{"status":"started"} (sets status='active'; the engine picks it up next cycle); 404 Bot not found for a foreign or unknown id.

POST /api/bots/{id}/pause

{"status":"paused"}; 404 Bot not found for a foreign or unknown id.

POST /api/bots/kill-all

{"status":"all_killed"} (sets all user's active bots to stopped; positions NOT closed — TODO in code)

POST /api/bots/copy/{id}

Copies a leaderboard-opted-in bot (any user's, leaderboard_opt_in=true; no live/testnet filter) into the caller's first wallet (testnet wallets first) with name Copied Bot, initial/current capital 1000, copying mode, ai_provider, ai_model, symbols, indicators, indicator_mode, risk_config. Enforces the plan's bot limit (409). Response {"id","status":"copied","mode":"paper"} (mode:"paper" is hardcoded; actual paper/live depends on wallet). Errors: 404 Bot not found on leaderboard; 400 Add a wallet first.


6. Scripts & Supervisor (auth: user)

ScriptVersion: {id, bot_id, version, source, is_active, performance_snapshot(json), created_at, created_by, supervisor_reasoning, approved_at, approved_by, declined_at}. Compile/backtest are proxied to the assistant service ASSISTANT_URL (default http://aitrade-assistant:8090).

POST /api/scripts/compile

Body {source: string} req. Source ≤100 KB. Proxies POST {assistant}/api/thunderscript/compile (10 s) with the caller's JWT. Response: assistant JSON passed through verbatim (code reads errors[], ast; full shape unclear). Errors: 500 Compiler service unavailable: … / Compiler response invalid: ….

GET /api/bots/{id}/scripts

ScriptVersion[] ordered version DESC; 404 Bot not found for a foreign bot.

POST /api/bots/{id}/scripts

Body {source: string}. Verifies ownership (404 Bot not found), upgrades bot_type to thunderscript, compiles via assistant, stores new version (max+1), snapshots performance, makes it active. Response {"id","version","status":"compiled_and_active"}. Errors: 400 Script errors: [...] (compiler errors, Debug-formatted), 400 Compilation produced no AST, 500 Compiler: … / Compiler response: ….

POST /api/bots/{id}/scripts/{version}/activate (version int)

Response {"status":"activated","version":N}; 404 Script version not found.

POST /api/bots/{id}/scripts/{script_id}/approve

script_id must be a version of this bot, and the bot must be the caller's (404 Script not found otherwise). Deactivates all, activates script_id (sets approved_at/by), sets bot active_script_id, marks approval_request notifications for the script read. Response {"status":"approved"}.

POST /api/bots/{id}/scripts/{script_id}/decline

Sets declined_at; marks notification read. Response {"status":"declined"}; 404 Script not found (script must belong to the caller's bot).

PATCH /api/bots/{id}/supervisor

Body (opt): supervisor_enabled bool, supervisor_auto_apply bool, supervisor_prompt string (≤500 bytes else 400 supervisor_prompt cannot exceed 500 chars). Response {"status":"updated"}; 404 Bot not found.


7. Trades & Activity (auth: user)

GET /api/bots/{id}/trades

Query: limit int default 50 (clamped 1..500), offset int default 0 (≥0). Response array: {id, bot_id, symbol, side, quantity, price, pnl|null, is_paper, ai_reasoning|null, created_at, leverage|null}; newest first. 404 Bot not found for a foreign bot.

GET /api/bots/{id}/activity

Query: limit default 20 (clamped 1..500; offset ignored). 404 for a foreign bot. Response array: {action, symbol, price, quantity, reasoning, indicators_snapshot, market_snapshot, regime, is_actionable, ai_model_used, conditions_detail, created_at}.

GET /api/bots/{id}/positions

Open positions only: {id, symbol, side, quantity, entry_price, current_price, stop_loss, take_profit, unrealized_pnl, status, opened_at, leverage, margin_used, trailing_stop_enabled, trailing_stop_callback_pct, trailing_stop_activation_price, trailing_stop_highest_price}.

GET /api/bots/{id}/stats

404 Bot not found. Response:

{ account:{total_value, available_cash, margin_in_positions, unrealized_pnl, open_positions_count},
  totals:{total_pnl, total_fees, open_fees, close_fees, net_realized},   // net_realized = sum(sell pnl) - open_fees
  stats:{average_leverage, average_confidence, biggest_win, biggest_loss, trade_count},
  hold_times_seconds:{long, short},
  active_positions:[PositionResponse...],
  recent_trades:[{id,symbol,side,quantity,price,pnl,notional,fee_amount,leverage,confidence,fee_bnb_used,created_at}] (last 25) }

GET /api/bots/{id}/performance

Latest performance_snapshots row or zeros: {total_pnl, daily_pnl, win_rate, sharpe_ratio, max_drawdown, total_trades} (404 Bot not found for a foreign or unknown bot).

GET /api/trades/export

All user's trades, max 10 000, newest first. Response body CSV text (content-type not set explicitly → text/plain; charset=utf-8): header id,bot_id,symbol,side,quantity,price,pnl,is_paper,leverage,created_at.

POST /api/positions/{id}/close

Manually closes an open position at positions.current_price (fallback entry price); records a sell trade, closes position, updates bot capital/pnl, inserts activity. Does not place an exchange order (DB bookkeeping only, despite comments). Fee 0.05% if leverage>1 else 0.10%. Body {reason?: string} (body must be sent as JSON object; default reason Manual close (user-initiated)). Response {"status":"closed","pnl":number,"close_price":number}. Errors: 404 Position not found or already closed.

Blocked symbols

  • GET /api/blocked-symbols → [{symbol, note|null, blocked_at}] ordered by symbol.
  • POST /api/blocked-symbols body {symbol: string req, note?: string}; symbol trimmed + uppercased, 1..40 chars else 400 Invalid symbol; upsert. Response {"status":"blocked","symbol"}.
  • DELETE /api/blocked-symbols/{symbol} → {"status":"unblocked","symbol"} (idempotent).

8. Market (auth: user) — all stubs

  • GET /api/market/prices → [].
  • GET /api/market/indicators (query symbol, indicators, interval accepted, ignored) → {"indicators":[]}.
  • GET /api/market/regime → {"regime":"unknown","confidence":0}.

9. Sentiment (auth: user) — stub

GET /api/sentiment → hardcoded {fear_greed_index:50, fear_greed_label:"Neutral", btc_funding_rate:0.0001, eth_funding_rate:0.0001, btc_open_interest_change:0.0}.

10. Backtest (auth: user)

POST /api/backtest

Body (all opt): bot_id uuid (ownership check → 404 Bot not found), symbol string, symbols string[], timeframe string (default 1h), days int (default 42), initial_capital number (default 10000), leverage int, source string. Validated: days 1..365, initial_capital > 0, leverage 1..125, ≤10 symbols, source ≤100 KB. Proxied to {assistant}/api/thunderscript/backtest (120 s, synchronous) with the caller's JWT. Response: assistant JSON verbatim (shape unclear). Errors: 500 Backtest service unavailable: … / Backtest response invalid: …. Upstream non-2xx bodies are passed through as 200 if JSON.

11. Marketplace (auth: user for ALL, incl. GET)

MarketplaceStrategy: {id, user_id, author_email, name, description, source, tags[], downloads, upvotes, snapshot_total_pnl, snapshot_total_trades, snapshot_winning_trades, snapshot_window_days, strategy_family, leverage, regime_context, snapshot_win_rate, created_at, user_upvoted}. author_email is masked (a***@domain) except on the caller's own strategies. Full source of public strategies is visible.

GET /api/marketplace

Query: sort (upvotes|recent|performance, else downloads-then-upvotes), q (ILIKE on name/description), tag (exact tag), mine bool (own strategies incl. private; else public only), limit default 50, clamped 1..200. Empty tag/q are ignored; %/_ in q are literal.

GET /api/marketplace/{id}

public strategy or own; 404 Strategy not found.

POST /api/marketplace

Body: name string req (non-empty, ≤120), source string req (non-empty), description string opt (""), tags string[] opt ([]), is_public bool opt (true), bot_id uuid opt (must be the caller's → else 404; attaches snapshot pnl/trades/wins/days/family(mode)/leverage/regime + win_rate). Limits: description ≤4000, source ≤100 KB, ≤10 tags of 1-32 chars. Response {"id","status":"published"}. Errors 400 Name and source are required, Name must be ≤120 chars.

POST /api/marketplace/{id}/download

Increments counter; response {source, name, author_email}; 404 Strategy not found (also for others' private).

POST /api/marketplace/{id}/upvote

Toggle; response {"upvoted":bool,"upvotes":int}. 404 Strategy not found unless the strategy is public or the caller's.

DELETE /api/marketplace/{id}

{"status":"deleted"}; 404 Strategy not found.

12. Chat (auth: user)

POST /api/chat

Served by aitrade-assistant (the ingress routes /api/chat there); the backend has no chat route. See the AI Assistant page for the request/response contract.

13. LLMs — BYO (auth: user; max 10 per user)

Providers: openai, anthropic, google, deepseek, openrouter, openai_compatible, ollama, custom. Default base URLs for the first five; openai_compatible|ollama|custom require base_url.

GET /api/llms

[{id, provider, name, base_url, model, max_tokens, max_context, is_active, created_at, last_used_at, api_key_masked:"•••• stored"}]

POST /api/llms

Body: provider req, name req (non-blank), model req (non-blank), api_key req (trimmed len ≥10), base_url opt (must be https://, no credentials, and a public host: internal names such as localhost, *.svc, *.cluster.local, *.internal and private/loopback/link-local/CGNAT addresses — literal or resolved — are rejected; the engine re-checks at call time), max_tokens opt default 1500 clamped 100..32000, max_context opt default 8000 clamped 1000..1 000 000. Response {"id","status":"created","api_key_masked":"abc…wxyz"}. Errors 400: Unsupported provider: X, Name is required, Model is required, API key looks too short, base_url is required for this provider, base_url must use https://, base_url must be a public host, You can save at most 10 LLMs. Delete one first.

PATCH /api/llms/{id}

Body (opt): name (non-blank else 400 Name cannot be empty), model, max_tokens (clamped), max_context (clamped), is_active. Response {"status":"updated"}; 404 LLM not found.

PATCH /api/llms/{id}/key

Body {api_key} (≥10 chars else 400). Response {"status":"rotated","api_key_masked"}; 404 LLM not found.

DELETE /api/llms/{id}

{"status":"deleted"}; 404 LLM not found. (Bots referencing it get user_llm_id NULL via FK.)

14. Notifications (auth: user)

Notification: {id, type, title, body|null, read, created_at, metadata|null}. - GET /api/notifications → {notifications: Notification[] (latest 100), unread: int}. - POST /api/notifications body {type: string req, title: string req, body?: string} → created Notification. type 1-32 of [a-z_] (the server-only approval_request is rejected), title ≤200, body ≤2000. - POST /api/notifications/read-all → {"status":"all_read"}. - PATCH /api/notifications/{id}/read → {"status":"read"}; 404 if not the caller's. - DELETE /api/notifications/{id} → {"status":"deleted"}; 404 if not the caller's. - DELETE /api/notifications → {"status":"deleted_all"}.

15. Devices (auth: user)

  • POST /api/devices/tokens body {token: string req (non-blank, ≤500), platform: "ios"|"android" req, bundle_id?: string, device_name?: string}. Upsert on token (reassigns to caller). Response {"status":"registered"}. Errors 400 platform must be 'ios' or 'android', invalid token.
  • DELETE /api/devices/tokens/{token} → {"status":"removed"} (only deletes caller's token; idempotent).

16. Workspace state (auth: user)

{device_type} path: "mobile" → mobile; any other value → desktop. - GET /api/workspace-state/{device_type} → {state: <json>, updated_at: ts|null}; none stored → {state:{}, updated_at:null}. - PUT /api/workspace-state/{device_type} body {state: any JSON req} → {"status":"ok"}. No size limit in code (axum default body limit 2 MB applies). See CORS note about PUT.

17. Intelligence (auth: user)

Global anonymized strategy_edges aggregates, k-anonymity gated via system_config keys intel_kanon_min_occurrences (default 20) and intel_kanon_min_contributors (default 3). Query struct shared: regime, family, timeframe, limit (each endpoint uses a subset). - GET /api/intelligence/summary → {total_cells, mature_cells, total_observations, families, symbols} (all int). - GET /api/intelligence/best-setups query regime, family, timeframe, limit (default 50, max 200) → EdgeRow[] {asset_bucket, symbol(null), timeframe, regime, strategy_family, signature, n, win_rate, avg_ret_pct, contributors} sorted by avg_ret_pct desc. - GET /api/intelligence/coins query regime, family, limit (50/200; timeframe ignored as filter) → EdgeRow[] with symbol set and signature:"". - GET /api/intelligence/params query family, regime, limit (default 100, max 300) → [{strategy_family, regime, sl_bucket, tp_bucket, lev_bucket, n, win_rate, avg_ret_pct}].

18. Leaderboard (public)

GET /api/leaderboard — no params. Top 50 bots by latest snapshot total_pnl where leaderboard_opt_in, wallet is not testnet (live only), not deleted. Response [{bot_id, bot_name, ai_model, mode, total_pnl, win_rate, sharpe_ratio, max_drawdown, total_trades}]. DB errors are swallowed → [].

19. Plans (public)

GET /api/plans — proxies https://auth.thundera.eu/api/v1/auth/billing/plans?app=aitrade (5 s timeout), passes through the JSON ({"plans":[...]}; plan fields unclear), 60 s cache; serves last good cache if upstream fails. Errors: 502 {"error":"upstream_bad_response"} / {"error":"upstream_error"}; 503 {"error":"billing_unavailable"}; 500 {"error":"internal"}.

20. System admin (auth: system-admin or system-super-admin; else 403 Forbidden)

  • GET /api/system/config → [{key, value(string), description|null}].
  • PATCH /api/system/config body {key: string, value: string} both req. Updates an existing key only (unknown key → 404; value ≤2000).
  • GET /api/system/users → [{id, email, name, plan, created_at, bot_count}] (no pagination).
  • PATCH /api/system/users/{id}/config (id = app_users.id) body all opt: bot_limit int, indicator_limit int, instructions_limit int, chat_enabled bool; upsert, null/omitted keeps existing. → {"status":"updated"}. (Unknown user → 404.)
  • GET /api/system/ai/providers → [{id, name, display_name, provider_type, base_url, default_model, default_temperature, default_max_tokens, default_max_context, failover_chain, max_retries, is_active, sort_order, notes, has_api_key}] ordered sort_order, name.
  • POST /api/system/ai/providers body: name req, display_name req, provider_type req (openai_compatible|anthropic|google|meta else 400 provider_type must be openai_compatible|anthropic|google|meta), default_model req, base_url?, default_temperature? (0.10), default_max_tokens? (2500), default_max_context? (32000), failover_chain? string[] ([]), max_retries?, api_key? (blank ignored), is_active? (true), sort_order? (100), notes?. → {"id","status":"created"} (duplicate name → 500 DB error).
  • PATCH /api/system/ai/providers/{id} body all opt: display_name, base_url, default_model, default_temperature, default_max_tokens, default_max_context, failover_chain[], max_retries, is_active, sort_order, notes → {"status":"updated"} (no 404 on unknown id; name/provider_type not editable).
  • PATCH /api/system/ai/providers/{id}/key body {api_key: string}; blank → clears key → {"status":"cleared"}; else {"status":"rotated"}.
  • DELETE /api/system/ai/providers/{id} → {"status":"deleted"}; 404 Provider not found.
  • GET /api/system/ai/counters → up to 200 users: [{user_id, email, name, plan, byoai_calls, byoai_failures, byoai_stops, system_calls, system_failures, system_fallbacks, last_call_at}] ordered by total calls desc.
AiTrade docs