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.
| Field | Type | Range | Notes |
|---|---|---|---|
enabled | boolean | — | Master switch for the cloud autopilot. |
tickIntervalMinutes | integer | ≥ 1 | Informational; actual cadence is set by cronExpression. Default 720 (12h). |
maxTotalExposureCents | integer | — | Hard exposure cap. |
maxPerMarketCents | integer | — | Per-market exposure cap. |
maxDailyLossCents | integer | — | Halt threshold for daily realized loss. |
maxPositions | integer | — | Open-position cap. |
minBalanceCents | integer | — | Halt below this balance. |
maxOrdersPerTick | integer | — | Per-tick order budget. |
cooldownAfterLossTicks | integer | — | Skip N ticks after a stop-loss. |
maxSingleOrderCents | integer | — | Soft cap per order. |
minEdgeCents | integer | — | Skip ideas under this edge. |
minThesisConfidence | numeric (string) | 0.0–1.0 | Lower bound for thesis-derived ideas. |
maxSpreadCents | integer | — | Skip wide-spread markets. |
minTauDays | integer | — | Skip markets expiring within N days. |
maxDrawdownHaltCents | integer | — | Kill-switch threshold (SPEC-21). |
drawdownWarnCents | integer | — | Warning threshold. |
excludeCategories | string[] | — | Default ["sports", "esports"]. |
minAdjIy | numeric (string) | — | Adjusted implied yield floor. |
maxLas | numeric (string) | — | Max liquidity-adjusted spread. |
crossVenuePairsEnabled | boolean | — | Cross-venue arb scanning. |
contagionEnabled | boolean | — | Lagging-sibling pickups. |
primaryModel | string | — | Cheap pass model. |
decisionModel | string | — | Final-decision model. |
executionMode | string | dry-run, live | Default dry-run. |
cronExpression | string | 5-field cron, LA tz | Default 0 7,19 * * *. |
Ticks
| Query | Type | Default | Notes |
|---|---|---|---|
limit | integer | 20 | Clamped 1–100. |
since | ISO 8601 | — | Lower bound on tickAt. |
until | ISO 8601 | — | Upper bound on tickAt. |
cursor | string | — | Cursor returned by a prior call. |
envelope | true | — | Wrap the result in { data, pageInfo }. |
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
| Query | Type | Default | Notes |
|---|---|---|---|
limit | integer | 50 | Clamped 1–200. |
status | open | closed | — | Open = closedAt IS NULL. Invalid value → 400 invalid status. |
since / until | ISO 8601 | — | Bounds on openedAt (or closedAt when status=closed). |
cursor | string | — | From a prior page. |
ticker | string | — | Filter by ticker. |
thesisId | uuid | — | Filter by thesis. |
envelope | true | — | Wrap in { data, pageInfo }. |
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
| Field | Type | Notes |
|---|---|---|
fills | unknown[] | Kalshi fill rows; server normalizes by ticker, side, action, count, side-aware price, fee, created time. |
settlements | unknown[] | Kalshi settlement rows; server computes realized PnL from revenue minus yes/no total cost. |
dryRun | boolean | Validate and report scanned/invalid counts without writing the ledger. |
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.
| Field | Type | Notes |
|---|---|---|
include | Array<"fills" | "settlements"> | Defaults to both. Unknown values return 400 invalid include value. |
ticker | string | Optional Kalshi ticker filter. |
limit | integer | Max rows per source, clamped 1–2000. |
dryRun | boolean | Validate and report fetched/scanned counts without writing. |
| Status | Cause |
|---|---|
400 | Missing portfolio Kalshi credentials or PORTFOLIO_ENCRYPTION_KEY. |
502 | Kalshi venue pull failed; retry with a smaller limit if rate-limited. |
portfolio-venue-import-scheduled Trigger task, which enumerates enabled portfolio_config users and imports automatically.
Client-supplied Polymarket rows
| Field | Type | Notes |
|---|---|---|
trades | unknown[] | Polymarket Data API trade rows. |
redeems | unknown[] | Polymarket REDEEM activity rows; recorded as cash/revenue settlement events. |
dryRun | boolean | Validate and report scanned/invalid counts without writing. |
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:
| Field | Type | Default | Notes |
|---|---|---|---|
title | string | required | Short display title. |
viewText | string | required | The conviction in plain language. |
category | string | macro | E.g. macro, geopolitics, crypto. |
tickers | string[] | [] | Markets the view applies to. |
conviction | integer | 3 | 1–5. Higher = more weight in PM context. |
timeHorizon | ISO date | null | Optional expiry. |
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:
| Field | Type | Default |
|---|---|---|
name | string | required |
description | string | required |
priority | integer | 0 |
constraints | object | {} |
PUT updates by id; send id plus fields. DELETE takes { id } in the body. Both return { ok: true } or 404.
Credential connection
POST body:
| Field | Type | Required |
|---|---|---|
kalshiKeyId | string | yes |
privateKeyPem | string | yes — full PEM, newlines preserved and redacted from logs |
| Status | Body | Cause |
|---|---|---|
400 | { error: "privateKeyPem and kalshiKeyId required" } | Missing field. |
500 | { error: string } | Encrypted credential storage is unavailable or misconfigured. |
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
| Status | Body | Cause |
|---|---|---|
500 | { error: string } | Cloud run scheduler unavailable or quota exceeded. |
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.