Authorization: Bearer sf_live_xxx.
Use these when an external app or agent needs the same data exposed by sf portfolio.
Start here
401 unauthorized without a valid Bearer key.
State
GET returns the singleton state row for the user, or null if the autopilot has never written one.
PUT is a fire-and-forget upsert — the autopilot writes here on every tick. End-user code rarely needs to call it; if you do, send a partial body and the upsert sets lastTickAt = now().
Config
GET returns the config row, or platform defaults if the user hasn’t saved one yet.
PUT is a partial upsert. Fields you omit are left unchanged on an existing row, or fall back to defaults on a fresh row. userId, createdAt, and schedule identifiers are server-managed and stripped from the body.
When enabled flips false → true, the platform creates a managed cloud schedule using cronExpression (default 0 7,19 * * *, America/Los_Angeles). When cronExpression changes while enabled, the schedule is updated. When enabled flips true → false, the schedule is deactivated. Schedule lifecycle is best-effort; config saves still succeed if the scheduler is temporarily unavailable.
Ticks
When
envelope=true:
envelope, the response is the raw array; the next cursor is returned in the x-next-cursor response header instead.
GET /api/portfolio/ticks/{id} returns one row by id, or 404 not found if it doesn’t belong to the caller.
POST /api/portfolio/ticks is the writer the cloud tick uses to log itself; external integrations rarely need it. Body is a tick row minus id and userId. Server stamps tickAt, attributes the trace, and returns { ok: true }.
Trades
direction is one of buy_yes, buy_no. exitReason is one of take_profit, stop_loss, thesis_exit, settlement, manual, or null for open trades.
POST /api/portfolio/trades is the writer the cloud tick uses; the body should match the trade row minus id, userId, createdAt. Server stamps openedAt = now() if not provided. Returns { ok: true }.
Ledger reads
The portfolio ledger is the canonical append-only event source for fills, settlements, cancellations, and agent decisions. Every read route returns a paginated envelope and accepts the same filter set:limit, since, until, cursor, eventType, venue, marketId, source, thesisId, confidence. Daily and grouped attribution accept from / to instead of since / until and add groupBy for grouped reads.
/ledger, /fills, /activity return SfPage<PortfolioLedgerEntry>. /positions returns SfPage<PortfolioPositionSnapshot> snapshot rows derived from the ledger. /attribution/daily and /attribution/grouped return materialized PnL/cash/fee/slippage/position deltas keyed by day, source, venue, market, thesis, strategy, view, and confidence. /risk returns a single composite snapshot: balance, exposure utilization, daily loss utilization, drawdown utilization, position utilization, stale-data flag, last reconcile status, and execution mode.
confidence values: exact, low, unknown. unknown rows are always counted but never silently collapsed into a thesis or strategy — they are surfaced explicitly so attribution does not invent linkage that the ledger could not prove.
curl
/dashboard2/portfolio cockpit and the SDK sf.portfolio.* resource tree. They are read-only and sideEffect: none.
Ledger imports
Authenticated import routes ingest historical venue rows into the portfolio ledger. They writeuser_write ledger events with attribution confidence unknown until thesis or strategy linkage is proven. Each route supports dryRun: true for validation-only runs and rejects payloads larger than 2000 rows per source.
Client-supplied Kalshi rows
Idempotency is enforced server-side via stable per-row keys derived from
trade_id / order_id / event_ticker and timestamps. Replays do not double-count PnL, cash, fees, slippage, or position deltas. Order/cancel rows with no PnL/cash/position/fee/slippage measures are not materialized as zero-measure attribution noise.
Server-side Kalshi pull
POST /api/portfolio/secrets. The server then signs Kalshi user-API requests locally, pulls fills and settlements, and runs them through the same idempotent import path as the client-supplied route. Global KALSHI_API_KEY_ID / KALSHI_PRIVATE_KEY_PEM environment variables are not mutated.
This route also powers the hourly
portfolio-venue-import-scheduled Trigger task, which enumerates enabled portfolio_config users and imports automatically.
Client-supplied Polymarket rows
Polymarket redeem rows do not invent realized PnL when cost basis is missing — they record
cashDeltaCents and settlementRevenueCents only. Server-side scheduled Polymarket import is a follow-up; today this route requires the client to supply rows.
Response shape
/kalshi/pull adds fetched: { fills, settlements } reflecting how many rows the server actually pulled before normalization. errors lists per-row reasons ({ kind: "fill" | "settlement", index: number, reason: string }) for rows that failed validation. The remaining successful rows still insert.
Views
GET returns the user’s views ordered by conviction DESC.
POST creates a new view:
PUT updates by id; send id plus the fields to change. userId and createdAt are stripped before write. 404 not found if the row isn’t owned. Server stamps updatedAt.
DELETE takes { id } in the body; returns { ok: true } or 404 not found.
Strategy
GET returns rows ordered by priority ASC.
POST body:
PUT updates by id; send id plus fields. DELETE takes { id } in the body. Both return { ok: true } or 404.
Credential connection
POST body:
There is no
GET /api/portfolio/secrets — the API never returns plaintext or ciphertext over the wire. To rotate, POST again (upsert). To revoke, DELETE.
DELETE returns { ok: true } or 404 no secrets found.
Run now
CLI equivalents
See also
Portfolio autopilot
Conceptual overview, risk gates, agent loop.
Risk gates
The hard / soft gate model.
Trade intents
The execution gateway used by every tick that places orders.
Authentication
Bearer keys, BYOK, sandbox vs live.