> ## Documentation Index
> Fetch the complete documentation index at: https://docs.simplefunctions.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Portfolio API

> Authenticated portfolio state, ticks, trades, views, strategies, credential connection, and run endpoints — the same data exposed by sf portfolio.

Portfolio endpoints are authenticated and scoped to the calling user. Every route requires `Authorization: Bearer sf_live_xxx`.

Use these when an external app or agent needs the same data exposed by `sf portfolio`.

## Start here

<CodeGroup>
  ```bash curl theme={null}
  curl -H "Authorization: Bearer $SF_API_KEY" \
    "https://simplefunctions.dev/api/portfolio/state"

  curl -H "Authorization: Bearer $SF_API_KEY" \
    "https://simplefunctions.dev/api/portfolio/ticks?limit=5&envelope=true"

  curl -H "Authorization: Bearer $SF_API_KEY" \
    "https://simplefunctions.dev/api/portfolio/trades?status=open&limit=20&envelope=true"
  ```

  ```bash CLI theme={null}
  sf portfolio status --json
  sf portfolio history --json --ticks 5 --trades 20
  sf portfolio config --json
  ```

  ```ts TypeScript theme={null}
  const res = await fetch('https://simplefunctions.dev/api/portfolio/state', {
    headers: { Authorization: `Bearer ${process.env.SF_API_KEY}` }
  })
  const state = await res.json()
  ```

  ```python Python theme={null}
  import os, requests
  state = requests.get(
      'https://simplefunctions.dev/api/portfolio/state',
      headers={'Authorization': f"Bearer {os.environ['SF_API_KEY']}"}
  ).json()
  ```
</CodeGroup>

All authenticated routes return `401 unauthorized` without a valid Bearer key.

## State

```http theme={null}
GET /api/portfolio/state
PUT /api/portfolio/state
```

`GET` returns the singleton state row for the user, or `null` if the autopilot has never written one.

<Expandable title="Response shape">
  ```json theme={null}
  {
    "userId": "uuid",
    "kalshiBalanceCents": 85100,
    "kalshiPortfolioValueCents": 199200,
    "totalExposureCents": 199200,
    "openPositionCount": 4,
    "dailyRealizedPnlCents": 0,
    "dailyPnlResetDate": "2026-05-05",
    "totalRealizedPnlCents": -31042,
    "totalUnrealizedPnlCents": 0,
    "highWaterMarkCents": 250000,
    "maxDrawdownCents": -89000,
    "lastTickAt": "2026-05-05T18:00:00.000Z",
    "lastReconcileAt": "2026-05-05T18:01:30.000Z",
    "lastReconcileStatus": "ok",
    "reconcileNotes": null,
    "createdAt": "2026-04-14T00:00:00.000Z"
  }
  ```

  All cent values are integers. `lastReconcile*` fields come from the SPEC-21 reconciler task.
</Expandable>

`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

```http theme={null}
GET /api/portfolio/config
PUT /api/portfolio/config
```

`GET` returns the config row, or platform defaults if the user hasn't saved one yet.

<Expandable title="Default config (returned when no row exists)">
  ```json theme={null}
  {
    "userId": "uuid",
    "enabled": false,
    "tickIntervalMinutes": 720,
    "maxTotalExposureCents": 300000,
    "maxPerMarketCents": 100000,
    "maxDailyLossCents": 15000,
    "maxPositions": 20,
    "minBalanceCents": 10000,
    "maxOrdersPerTick": 3,
    "cooldownAfterLossTicks": 4,
    "maxSingleOrderCents": 20000,
    "minEdgeCents": 5,
    "minThesisConfidence": "0.15",
    "maxSpreadCents": 5,
    "minTauDays": 3,
    "excludeCategories": ["sports", "esports"],
    "minAdjIy": "100",
    "maxLas": "0.15",
    "crossVenuePairsEnabled": false,
    "contagionEnabled": true,
    "primaryModel": "google/gemini-2.5-flash-lite",
    "decisionModel": "anthropic/claude-sonnet-4.6",
    "executionMode": "dry-run",
    "cronExpression": "0 7,19 * * *",
    "triggerScheduleId": null
  }
  ```
</Expandable>

`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

```http theme={null}
GET /api/portfolio/ticks
GET /api/portfolio/ticks/{id}
POST /api/portfolio/ticks
```

A **tick** is one autopilot evaluation cycle — actions taken, risk gates evaluated, balances at the time, total tick duration, and a free-text handoff note for the next tick to read.

| 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 }`. |

When `envelope=true`:

```json theme={null}
{
  "data": [/* tick rows */],
  "pageInfo": { "limit": 20, "nextCursor": "...", "hasMore": true }
}
```

Without `envelope`, the response is the raw array; the next cursor is returned in the `x-next-cursor` response header instead.

<Expandable title="Tick row">
  ```json theme={null}
  {
    "id": "uuid",
    "userId": "uuid",
    "tickAt": "2026-05-05T18:00:00.000Z",
    "balanceCents": 85100,
    "portfolioValueCents": 199200,
    "exposureCents": 199200,
    "openPositions": 4,
    "dailyPnlCents": 0,
    "totalPnlCents": -31042,
    "actionsTaken": [
      { "type": "buy", "ticker": "KXFEDDEC-26DEC10-T0", "qty": 50, "priceCents": 42 }
    ],
    "riskGates": { "maxDailyLossCents": "ok", "maxPositions": "ok" },
    "tickDurationMs": 184321,
    "handoffNote": "Held; spreads widened around CPI print, will re-evaluate next tick.",
    "traceId": "trace_...",
    "createdAt": "2026-05-05T18:03:04.321Z"
  }
  ```
</Expandable>

`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

```http theme={null}
GET /api/portfolio/trades
GET /api/portfolio/trades/{id}
POST /api/portfolio/trades
```

A **trade** is one entry (and optionally one exit) for P\&L attribution. The autopilot writes one row per fill and updates it on close.

| 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 }`.                                    |

<Expandable title="Trade row">
  ```json theme={null}
  {
    "id": "uuid",
    "userId": "uuid",
    "intentId": "uuid",
    "strategyId": "uuid",
    "thesisId": "uuid",
    "ticker": "KXFEDDEC-26DEC10-T0",
    "venue": "kalshi",
    "direction": "buy_yes",
    "entryPriceCents": 42,
    "exitPriceCents": 51,
    "quantity": 50,
    "costCents": 2100,
    "revenueCents": 2550,
    "pnlCents": 450,
    "exitReason": "take_profit",
    "openedAt": "2026-05-04T13:00:00.000Z",
    "closedAt": "2026-05-05T17:30:00.000Z",
    "settled": false,
    "ideaHeadline": "Fed will cut by Dec",
    "ideaEdgeCents": 8,
    "actualEdgeCents": 9,
    "indicatorSnapshot": {
      "iyYes": 320, "cri": 1.2, "tauDays": 219
    },
    "thesisConfidenceAtEntry": "0.62",
    "thesisConfidenceAtExit": "0.71",
    "notes": null,
    "traceId": "trace_...",
    "createdAt": "2026-05-04T13:00:01.234Z"
  }
  ```
</Expandable>

`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.

```http theme={null}
GET /api/portfolio/ledger
GET /api/portfolio/fills
GET /api/portfolio/positions
GET /api/portfolio/activity
GET /api/portfolio/attribution/daily
GET /api/portfolio/attribution/grouped
GET /api/portfolio/risk
```

`/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.

```bash curl theme={null}
curl -H "Authorization: Bearer $SF_API_KEY" \
  "https://simplefunctions.dev/api/portfolio/ledger?limit=20&eventType=fill&venue=kalshi"

curl -H "Authorization: Bearer $SF_API_KEY" \
  "https://simplefunctions.dev/api/portfolio/attribution/daily?from=2026-05-01&to=2026-05-21"

curl -H "Authorization: Bearer $SF_API_KEY" \
  "https://simplefunctions.dev/api/portfolio/risk"
```

These routes back the `/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 write `user_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.

```http theme={null}
POST /api/portfolio/ledger/import/kalshi
POST /api/portfolio/ledger/import/kalshi/pull
POST /api/portfolio/ledger/import/polymarket
```

### Client-supplied Kalshi rows

```http theme={null}
POST /api/portfolio/ledger/import/kalshi
```

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/ledger/import/kalshi" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fills": [{ "ticker": "KXTEST", "side": "yes", "action": "buy", "count": 5, "yes_price": 44, "created_time": "2026-05-21T12:00:00Z" }],
    "settlements": [],
    "dryRun": true
  }'
```

| 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.                                   |

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

```http theme={null}
POST /api/portfolio/ledger/import/kalshi/pull
```

The user must first connect a Kalshi BYOK keypair via `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.

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/ledger/import/kalshi/pull" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "include": ["fills", "settlements"], "limit": 2000, "dryRun": false }'
```

| 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. |

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

```http theme={null}
POST /api/portfolio/ledger/import/polymarket
```

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/ledger/import/polymarket" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trades": [{ "proxyWallet": "0x...", "transactionHash": "0xtx", "asset": "TOKEN", "conditionId": "0xcond", "side": "BUY", "outcome": "Yes", "size": 10, "price": 0.42, "timestamp": 1780000000 }],
    "redeems": [],
    "dryRun": true
  }'
```

| 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.                  |

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

```json theme={null}
{
  "ok": true,
  "scanned": { "fills": 5, "settlements": 1 },
  "inserted": 5,
  "duplicates": 1,
  "invalid": 0,
  "dryRun": false,
  "errors": []
}
```

`/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

```http theme={null}
GET /api/portfolio/views
POST /api/portfolio/views
PUT /api/portfolio/views
DELETE /api/portfolio/views
```

A **view** is a user-authored conviction or note that the LLM portfolio manager reads each tick. Views influence which ideas the manager prioritizes.

`GET` returns the user's views ordered by `conviction DESC`.

`POST` creates a new view:

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/views" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Rates cut view",
    "viewText": "Fed cut odds look too high into the next meeting.",
    "category": "macro",
    "tickers": ["KXRATECUT-26DEC31"],
    "conviction": 4
  }'
```

| 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

```http theme={null}
GET /api/portfolio/strategy
POST /api/portfolio/strategy
PUT /api/portfolio/strategy
DELETE /api/portfolio/strategy
```

Persistent instructions and constraints for the autopilot — read on every tick.

`GET` returns rows ordered by `priority` ASC.

`POST` body:

| Field         | Type    | Default  |
| ------------- | ------- | -------- |
| `name`        | string  | required |
| `description` | string  | required |
| `priority`    | integer | `0`      |
| `constraints` | object  | `{}`     |

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/strategy" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Rates discipline",
    "description": "Keep exposure small around FOMC.",
    "priority": 2,
    "constraints": { "maxPerCategoryCents": { "rates": 50000 } }
  }'
```

`PUT` updates by `id`; send `id` plus fields. `DELETE` takes `{ id }` in the body. Both return `{ ok: true }` or `404`.

## Credential connection

```http theme={null}
POST   /api/portfolio/secrets
DELETE /api/portfolio/secrets
```

Connects an encrypted Kalshi BYOK keypair so the cloud autopilot can submit orders on the user's behalf. The private key is encrypted before durable storage, plaintext is never returned by the API, and rotation uses the same endpoint as initial connection.

`POST` body:

| Field           | Type   | Required                                                  |
| --------------- | ------ | --------------------------------------------------------- |
| `kalshiKeyId`   | string | yes                                                       |
| `privateKeyPem` | string | yes — full PEM, newlines preserved and redacted from logs |

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/secrets" \
  -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kalshiKeyId": "key-id",
    "privateKeyPem": "-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----\n"
  }'
```

| Status | Body                                                  | Cause                                                         |
| ------ | ----------------------------------------------------- | ------------------------------------------------------------- |
| `400`  | `{ error: "privateKeyPem and kalshiKeyId required" }` | Missing field.                                                |
| `500`  | `{ error: string }`                                   | Encrypted credential storage is unavailable or misconfigured. |

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

```http theme={null}
POST /api/portfolio/trigger
```

Runs a cloud autopilot tick now, in addition to the user's configured schedule.

```bash theme={null}
curl -X POST "https://simplefunctions.dev/api/portfolio/trigger" \
  -H "Authorization: Bearer $SF_API_KEY"
```

<Expandable title="Response shape">
  ```json theme={null}
  {
    "triggered": true,
    "runId": "run_...",
    "publicAccessToken": "run_token_..."
  }
  ```

  `publicAccessToken` is a short-lived read-only token scoped to **this run only**. It can be used by a browser client to subscribe to run status without exposing account credentials. Ticks cap at roughly 8 minutes; the token TTL is longer than the run window.
</Expandable>

| Status | Body                | Cause                                              |
| ------ | ------------------- | -------------------------------------------------- |
| `500`  | `{ error: string }` | Cloud run scheduler unavailable or quota exceeded. |

## CLI equivalents

```bash theme={null}
sf portfolio status --json
sf portfolio config --json
sf portfolio config <key> <value>
sf portfolio history --json --ticks 5 --trades 10
sf portfolio tick <id> --json
sf portfolio trade <id> --json
sf portfolio view list --json
sf portfolio view add "..."
sf portfolio strategy list --json
sf portfolio strategy add <name> <description>
sf portfolio trigger
sf portfolio enable
sf portfolio disable
sf portfolio revoke         # DELETE /api/portfolio/secrets
```

See [CLI command reference — Portfolio](/cli/command-reference#portfolio) for every flag.

## See also

<CardGroup cols={2}>
  <Card title="Portfolio autopilot" href="/guides/portfolio-autopilot">
    Conceptual overview, risk gates, agent loop.
  </Card>

  <Card title="Risk gates" href="/concepts/risk-gates">
    The hard / soft gate model.
  </Card>

  <Card title="Trade intents" href="/api-reference/execution-intents">
    The execution gateway used by every tick that places orders.
  </Card>

  <Card title="Authentication" href="/guides/authentication">
    Bearer keys, BYOK, sandbox vs live.
  </Card>
</CardGroup>
