Skip to content

Web API Reference ​

40+ REST endpoints + SSE exposed by FastAPI. Same-origin deployment, CF Access edge authentication. This chapter is a quick reference for endpoints grouped by "Read / Write / Committee / System / Transparency" + the frontend type synchronization workflow.

← 05-data-model · Wiki Index · 07-extending →


1. Startup ​

bash
# Local development
uv run uvicorn connectors.web_api:app --host 127.0.0.1 --port 8765

# Swagger auto-generation
open http://127.0.0.1:8765/docs

# OpenAPI schema (pulled by frontend gen-types)
curl http://127.0.0.1:8765/openapi.json

Production deployment: see 08-deployment.md.


2. Read Endpoints (GET) ​

Holdings / Cash ​

MethodPathPurpose
GET/api/portfolioSimplified holdings snapshot (compatible with legacy frontend)
GET/api/holdingsComplete holdings list + cash dict (v2 main interface)
GET/api/portfolio/total_value?base=CNYMulti-currency converted total market value
GET/api/cashReturns only the cash dict
GET/api/goldGold holdings + real-time price + floating PnL (compatible with legacy interface)
GET/api/ndqNDQ.AX holdings + real-time quote (compatible with legacy interface)

/api/holdings response example:

json
{
  "cash": {"CNY": 50000, "AUD": 1000},
  "holdings": [
    {
      "symbol": "NDQ.AX",
      "kind": "etf",
      "units": 50,
      "unit_label": "shares",
      "avg_cost": 38.50,
      "cost_currency": "AUD",
      "channel": "CommSec",
      "display_name": "BetaShares Nasdaq 100 ETF",
      "quote": {
        "price": 42.30,
        "currency": "AUD",
        "is_stale": false,
        "extra": {"day_change_pct": 1.2}
      },
      "market_value": 2115.00,
      "pnl": 190.00,
      "is_tracking_only": false
    }
  ]
}

History / Transactions ​

MethodPathPurpose
GET/api/history?limit=200&symbol=NDQ.AXTransaction history
GET/api/daily?since=NDaily decision snapshots for the last N days
GET/api/pnl_history?since=NPnL time series points (written by jobs/pnl_snapshot)
GET/api/pnl_chart.svgBackend-generated SVG (viewable without JS)

Strategy / Configuration ​

MethodPathPurpose
GET/api/strategyComplete strategy + target_assets
GET/api/configCurrently effective values of API-settable tunables, whether overridden, and metadata (ADR-017)
GET/api/symbols/search?q=apple&limit=8yfinance Search for symbols (used by GUI for adding assets)
GET/api/regime/{symbol}Current regime for the symbol + algorithm inputs
GET/api/regime_rulesAll hard rules + full prompts for the 4 roles

3. Write Endpoints (POST/PUT/DELETE) ​

Cash ​

MethodPathBody
POST/api/cash/{currency}/deposit{amount, source?}
POST/api/cash/{currency}/withdraw{amount, source?}
bash
curl -X POST http://127.0.0.1:8765/api/cash/CNY/deposit \
  -H "Content-Type: application/json" \
  -d '{"amount": 1000, "source": "Salary"}'

Holdings CRUD (v2 Generic) ​

MethodPathPurpose
POST/api/holdingsAdd a holding (any yfinance symbol)
PUT/api/holdings/{symbol}Update specific fields
DELETE/api/holdings/{symbol}Delete (rejected if units > 0)
bash
# Add AAPL tracking position
curl -X POST http://127.0.0.1:8765/api/holdings -d '{
  "symbol": "AAPL",
  "kind": "stock",
  "units": 0,
  "unit_label": "shares",
  "avg_cost": 0,
  "cost_currency": "USD",
  "channel": "Robinhood",
  "is_tracking_only": true
}'

Smart Holdings Import (Free Text / CSV) ​

MethodPathPurpose
POST/api/holdings/import{content, commit} — Free text/CSV → backend LLM parses into structured holdings

commit:false only parses and returns a preview (parsed.{cash,holdings}), commit:true performs a non-destructive write: Only adds symbols not already in the portfolio; only fills cash currencies currently at 0; skips existing entries (summary.{added_holdings,skipped_holdings,cash_set,cash_skipped}). Returns 400 if no LLM key is configured. Single source of truth: services/holdings_import.py. Shared by GUI "Import Holdings", CLI skill.py import, and onboarding.

bash
curl -X POST http://127.0.0.1:8765/api/holdings/import \
  -d '{"content": "510300 ETF 3000 shares cost 4.2 yuan (Alipay)\nYu'ebao 50k", "commit": false}'

Legacy Dedicated Endpoints (Retained for Compatibility) ​

POST /api/deposit     POST /api/withdraw
POST /api/gold/buy    POST /api/gold/sell    POST /api/gold/set    POST /api/gold/offset

New code should use the generic holdings interface; dedicated endpoints are only for legacy frontend compatibility.

Strategy Write ​

MethodPathPurpose
PUT/api/strategy/allocationsModify stock/cash target ratios
POST/api/strategy/assetAdd a target_asset
PUT/api/strategy/asset/{symbol}Modify target_asset cap / fee rate
DELETE/api/strategy/asset/{symbol}Delete a target_asset
PUT/api/configSet a whitelisted config override (body {key, value}, persisted to disk, priority > env; ADR-017)
DELETE/api/config/{key}Delete a config override, fallback to env/yaml/default

config-via-API (ADR-017): The whitelist API_SETTABLE (in core/config/_loader.py) only contains user-safe behavioral switches (verdict.concentration_lens_enabled / verdict.risk_profile / verdict.gold_defense_dca_enabled / dreaming.llm_verify_enabled). Persisted to memory/.state/config_overrides.json, merged by load_config() on top of env. Read by web/cron/skill processes. Secrets + deployment guidance still only via env; locked.py is never exposed. CLI equivalent: skill config [--set K V] [--clear K].


4. Committee ​

Trigger + Status ​

MethodPathPurpose
POST/api/committee/runAsync trigger, immediately returns task_id (upon completion, result.by_asset[sym] contains verdict + cio_memo)
GET/api/committee/{task_id}Single status snapshot
GET/api/committee/live/{task_id}SSE stream (25s keepalive to prevent CF 5min idle timeout)
GET/api/committee_sessions?limit=NHistorical decision archive (reverse chronological)
GET/api/committee_sessions/{date}/{symbol}Complete markdown for a single decision
POST/api/committee/prepareCoordinator path RPC: returns a self-contained brief (6 inline prompts), used by remote clients to spawn subagents. Body {symbol}
POST/api/committee/saveCoordinator path RPC: transcript parsing + defensive degradation post-processing + persistence. Body {symbol, transcript}
bash
# Trigger
curl -X POST http://127.0.0.1:8765/api/committee/run \
  -d '{"symbols": ["NDQ.AX"], "max_debate_rounds": 4}'
# → {"task_id": "abc123...", "status": "queued", "poll_url": "..."}

# SSE stream
curl -N http://127.0.0.1:8765/api/committee/live/abc123
# event: progress
# data: {"phase": "round_1_done", "round": 1, ...}
# event: progress
# data: {"phase": "round_2_done", ...}
# event: done
# data: {"result": {...}}

POST body schema:

typescript
{
  symbols?: string[]  // If omitted, runs for all strategy.target_assets
  max_debate_rounds?: number  // Default 4, range 1-8
  note?: string
}

See 02-agents.md for the meaning of each phase.


5. Transparency Endpoints (v3) ​

LLM Telemetry ​

MethodPathPurpose
GET/api/llm/usage?since=NHistorical LLM call list (token/latency/cost)
GET/api/llm/summary?period=7dPeriod aggregation (total tokens / total cost / role distribution)

Tool Audit Trail ​

MethodPathPurpose
GET/api/agents/run/{run_id}/tool_callsAll tool calls for a single run + input/output/duration

Verdict Hit Rate ​

MethodPathPurpose
GET/api/verdict_review/summary1d / 7d / 30d hit rate × verdict type
GET/api/verdict_review/dataRaw data points
GET/api/verdict_review/reportFull markdown from docs/verdict_accuracy.md

Discipline Ledger (ADR-023) ​

MethodPathPurpose
GET/api/disciplineCommittee discipline quantification: inaction rate + intervention count + counterfactual PnL

Returns {summary:{inaction:{total_verdicts,by_verdict,hold,hold_rate}, interventions:{total,by_family,...}}, markdown}. Honest positioning (no alpha claims, quantifying "doing fewer wrong things"), see adr/023. Consumed by the GUI "Discipline" page.

Data Source Health ​

MethodPathPurpose
GET/api/data_sources/healthLast successful time + is_stale for all data sources (yfinance / DB / CommSec)

CommSec Manual Import (Replaces Legacy Cron) ​

MethodPathPurpose
GET/api/commsec/preview?lookback_days=180Preview trades fetched from emails (no write)
POST/api/commsec/applyWrite after user confirmation

See 05-data-model.md for why the mode was changed to manual.


6. System / Internal State ​

MethodPathPurpose
GET/api/healthContainer health check
GET/api/jobs/statusList of all cron jobs + next run time + enabled status
GET/api/insightsDreaming long-term insights
GET/api/dreams/stateDreaming short-term memory state
GET/api/dreams/bucketsRegime bucket group hit rates (v3)

7. Authentication Model ​

Browser → CF Access (email verification) → CF proxy → Caddy → Backend 127.0.0.1:8765

Backend does not perform auth:

  • Backend binds only to 127.0.0.1, unreachable from the public internet
  • Caddy only reverse-proxies /api/* to port 8765
  • CF Access verifies JWT at the edge (only authorized email passes through)
  • All paths are trusted → backend does not re-verify

→ Adding JWT validation is over-engineering, but only if no one can directly connect to the origin server IP. → Hardening plan: Add @cloudflare matcher in Caddy to only allow CF IP ranges (future).

Optional Application-Layer Token (2026-06, Remote Mode): When INVEST_API_TOKEN is set, non-loopback sources accessing /api/* (/api/health is exempt for health checks) must include Authorization: Bearer <token> (constant-time comparison via secrets.compare_digest). If not set, behavior is completely unchanged, and the edge authentication model above remains in effect. Loopback exemption ensures the Caddy→127.0.0.1 link and local GUI are unaffected; the token is for scenarios like "binding to 0.0.0.0 / direct intranet connection / LAN hub without CF". The token is never logged or included in response bodies.

See 08-deployment.md#cloudflare-access.


8. Frontend Type Synchronization ​

Auto-generation ​

bash
# In the invest-gui repository
pnpm gen-types
# = openapi-typescript http://127.0.0.1:8765/openapi.json -o src/lib/api-types.ts

→ Backend changes an endpoint / Pydantic model → Frontend runs pnpm gen-types to sync types in one line.

Workflow ​

  1. Backend modifies an endpoint / Pydantic schema
  2. Start uvicorn locally exposing port :8765
  3. Run pnpm gen-types in the invest-gui repository
  4. TS compilation errors indicate fields that need updating in the frontend code
  5. After fixing, commit the type artifact (src/lib/api-types.ts)
  6. CI does not force running gen-types (to avoid blocking builds when the backend is not running)

api-client.ts Wrapper ​

Wraps fetcher (for SWR) + postJSON (for mutations) + ApiError class. All routing code uses hooks for calls, not direct fetch().

typescript
import useSWR from "swr";
import { fetcher, type HoldingsListResponse } from "../lib/api-client";

const { data, error, isLoading } = useSWR<HoldingsListResponse>(
  "/api/holdings",
  fetcher,
  { refreshInterval: 30_000 },
);

9. Error Protocol ​

Backend raises HTTPException(status_code=N, detail=msg). Frontend ApiError:

typescript
class ApiError extends Error {
  status: number
  detail: string
}

try {
  await postJSON(...)
} catch (err) {
  setError(err instanceof ApiError ? err.detail : String(err))
}

Common error codes:

  • 400 Input validation failure (amount ≤ 0 / incorrect currency format)
  • 404 Symbol not found / task_id not found
  • 409 Holding symbol already exists (when POSTing holdings)
  • 503 External dependency failure (IMAP / yfinance)

10. CORS / Same-Origin ​

Production: Fully same-origin (/api/* and /* on the same host) → No CORS headers needed.

Development: Vite dev server on :5173 calls local :8765 → Backend uses INVEST_WEB_DEV_CORS=1 env to allow:

bash
INVEST_WEB_DEV_CORS=1 uv run uvicorn ...

→ Backend injects CORS middleware allowing only http://localhost:5173.


11. Skill-Parity Endpoints (Remote Mode Hub-and-Spoke) ​

When scripts/skill.py has INVEST_API_BASE set, CLI subcommands are forwarded to these endpoints via scripts/remote_dispatch.py. Output shape is identical to the local CLI (shares services/skill_views.py / PortfolioManager methods), so the agent protocol (SKILL.md) is completely unaware.

MethodPathCLI EquivalentDescription
GET/api/doctordoctorHub-perspective health self-check (memory/.env/LLM reachability)
GET/api/skill/statusstatusCash + holdings + real-time prices + total assets
GET/api/skill/strategystrategyStrategy + Dreaming insights
GET/api/skill/history?n=history -nTrades + debates (/api/history only has trades)
POST/api/skill/what_ifwhat_ifScenario simulation, body fields correspond one-to-one with CLI parameters
POST/api/skill/buy / /api/skill/sellbuy / sellWrites holdings via with_portfolio_tx, history source: skill_remote
POST/api/skill/deposit / /api/skill/withdrawdeposit / withdrawSame output as CLI (/api/cash/* returns WriteResponse shape, which doesn't match CLI, hence separate endpoints)
POST/api/skill/delete_holdingdelete_holdingSupports force (DELETE /api/holdings lacks this semantics)

Error semantics align with CLI: Domain errors (e.g., what_if symbol not in holdings) return 200 + {"status": "error", ...} (client prints as-is); invalid parameters return 400, uninitialized memory returns 503, wrong token returns 401—the remote dispatch maps these back to CLI-equivalent error JSON + exit 1.

Deployment topology is described in the hub-and-spoke section of 08-deployment.md.


Next Steps ​

→ 07-extending.md — What to modify to add a new endpoint

→ 08-deployment.md — Complete production deployment chain

→ 09-troubleshooting.md — How to debug endpoint failures