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

# tRPC v2 Portfolio API

> Canonical portfolio-first tRPC surface for CLI and agent integrations

The `v2` tRPC namespace is the canonical API surface for agent and CLI consumers.

* Base endpoint: `POST /v1/trpc`
* Namespace: `v2.*`
* Auth:
  * Wallet session for `v2.portfolio.create.*` and `v2.portfolio.permission.refresh.*`
  * Wallet session for `v2.portfolio.builder.getProfile`, `saveProfile`, `getDraft`, `saveDraft`, `recommend`, and `seedCopilot`
  * Wallet session or API key for `v2.portfolio.builder.generateDraft` and `remixDraft`
  * Wallet session for `v2.portfolio.scheduledFunctions.*`
  * Wallet session or API key for other `v2.portfolio.*`, `v2.executions.*`, `v2.automationRuns.*`, and deprecated `v2.operations.*`
  * For API key calls, owner is resolved from canonical API key metadata/KMS linkage. `x-glider-owner-address` is optional and must match canonical owner when provided.
  * Public rate-limited access for `v2.public.*` non-owner read surfaces

## Procedure Groups

* `v2.portfolio.*`
  * `list`, `listByOwnerDbV2`, `get`, `status`
  * `vaults.list`
  * `performance.get`, `performance.series`, `performance.assetMetrics`
  * `activity.list`, `activity.allocationHistory.list`, `activity.allocationHistory.chart`
  * `recipients.list`
  * `deposit.instructions`
  * `builder.getProfile`, `builder.saveProfile`, `builder.getDraft`, `builder.saveDraft`, `builder.recommend`, `builder.generateDraft`, `builder.remixDraft`, `builder.seedCopilot`
  * `create.prepare`, `create.confirm`
  * `permission.status`, `permission.refresh.prepare`, `permission.refresh.confirm`
  * `archive`, `unarchive`
  * `updates.list`, `updates.get`, `updates.preview`, `updates.create`, `updates.submit`, `updates.supersede`
  * `approvals.list`, `approvals.get`, `approvals.decide`
  * `runs.list`, `runs.get`
  * `events.list`, `events.stream`
  * `context.list`, `context.get`
  * `agents.list`, `agents.get`
  * `actions.submit`
  * `policy.evaluate`, `policy.scheduleAdvisory`
  * `policy.config.get`, `policy.config.update`
  * `schedule.get`, `schedule.create`, `schedule.update`, `schedule.setFromText`, `schedule.pause`, `schedule.archive`, `schedule.resume`, `schedule.runNow`
  * `scheduledFunctions.list`, `scheduledFunctions.create`, `scheduledFunctions.delete` (beta/private rollout; exposed in the webapp only behind the recurring-swaps and recurring-transfers feature flags)
* `v2.executions.*`
  * `get`, `detail`, `result`, `list`, `stream`
* `v2.automationRuns.*`
  * `get`, `events`, `stream`
* `v2.workflows.*` (feature-gated beta/private rollout)
  * `list`, `get`
  * `drafts.list`, `drafts.start`, `drafts.startFromWorkflow`, `drafts.get`, `drafts.answer`, `drafts.cancel`
  * `authoring.validate`, `authoring.preview`
* `v2.operations.*` (deprecated)
  * legacy compatibility reads
* `v2.agentAuth.*`
  * `createApiKey`
* `v2.public.*`
  * `portfolio.list`, `portfolio.listByIds`, `portfolio.get`
  * `portfolio.vaults.list`
  * `portfolio.schedule.get`
  * `portfolio.activity.list`, `portfolio.activity.allocationHistory`, `portfolio.activity.allocationHistoryChart`
  * `account.profile.get`
  * `account.assets.list`
  * `account.performance.series`
  * `account.performance.overview`

## `actions.submit` Withdrawal Modes

For `v2.portfolio.actions.submit` with `kind: "withdraw"`:

* `mode: "withdraw"` transfers the selected assets out directly.
* `mode: "transfer"` requires `recipient` and is limited to the owner address or
  an owned vault.
* `mode: "external"` requires `recipient` for external wallet withdrawals.
* `mode: "withdraw_as_usdc"` requires `recipient` and lets the backend either:
  * transfer already-USDC assets directly, or
  * smart-route one-or-many non-USDC assets through backend swap execution and
    send the resulting USDC to that recipient.

## `actions.submit` Swap Constraints

For `v2.portfolio.actions.submit` with `kind: "swap"`:

* `params.chainId` remains the execution chain for the submitted LiFi route.
* `params.executionConstraints` is optional and additive. When present, the
  backend enforces stricter settlement rules without changing existing generic
  swap callers.
* `params.executionConstraints.sameChainOnly: true` rejects multi-chain routes.
* `params.executionConstraints.expectedVaultChainId` requires the route to stay
  on that chain and settle into the authenticated portfolio's exact vault on
  that chain.

This is used by Investing Account buy/sell flows to keep Base, Mainnet, and
Arbitrum cash balances chain-local and to fail closed when the requested-chain
vault is missing.

## `actions.submit` Yield Actions

For `v2.portfolio.actions.submit` with `kind: "yield"`:

```json theme={null}
{
  "portfolioId": "<portfolio-id>",
  "kind": "yield",
  "params": {
    "mode": "deposit",
    "chainId": 42161,
    "protocolId": "aave:v3:ausdc:42161",
    "amount": "1000000",
    "assetId": "0xaf88d065e77c8cc2239327c5edb3a432268e5831:42161"
  }
}
```

* `mode: "deposit"` requires `assetId` and submits a manual engine
  `yield-deposit` operation.
* `mode: "withdraw"` requires `assetId` and submits a manual engine
  `yield-withdraw` operation for a USDC-denominated exit amount.
* `mode: "redeem"` requires `shareAssetId` and submits a manual engine
  `yield-redeem` operation for an exact share-token amount.
* The contract is generic enough to carry any validated protocol/source/chain,
  but current product scope is limited to Arbitrum (`42161`) USDC targets for
  Aave V3, Morpho Steakhouse High Yield, and Fluid.
* Submitted yield actions return an execution handle with `kind: "execution"`;
  clients should poll `v2.executions.get` or stream `v2.executions.stream`.

## Performance Series Response Shape

`v2.portfolio.performance.series` and `v2.public.account.performance.series` return a unified response containing historical TWR series, pre-computed stats per timeframe, and a live value snapshot:

```json theme={null}
{
  "series": [
    {
      "ts": "2026-03-24T00:00:00.000Z",
      "eventType": null,
      "tvlUsd": "1234.56",
      "amount": "0.00",
      "growthFactor": "1.05",
      "cumulativeTwrPct": "5.00",
      "isLive": false
    }
  ],
  "stats": {
    "DAY": { "absoluteChangeUsd": "12.34", "twrPct": "1.05" },
    "WEEK": { "absoluteChangeUsd": "45.67", "twrPct": "3.21" },
    "MONTH": { "absoluteChangeUsd": "123.45", "twrPct": "8.90" },
    "YEAR": { "absoluteChangeUsd": "456.78", "twrPct": "25.00" },
    "ALL": { "absoluteChangeUsd": "789.01", "twrPct": "42.00" }
  },
  "live": {
    "value": "1234.56",
    "assets": [
      {
        "assetId": "asset-db-id",
        "symbol": "USDC",
        "valueUsd": "500.00"
      }
    ]
  }
}
```

* `series[]` may include a trailing `isLive: true` point representing the current live valuation.
* `stats` is keyed by `PerformanceChartResolution` (`DAY`, `WEEK`, `MONTH`, `YEAR`, `ALL`).
* `live` contains the real-time portfolio value and per-asset breakdown. `live.assets` is present on `v2.portfolio.performance.series` but omitted from the account-level endpoint.
* Responses are cached in Redis with a 30s TTL.

## Execution Handle

Long-running actions return a canonical execution envelope:

```json theme={null}
{
  "operationId": "rebalance:<portfolioId>:<runId>",
  "portfolioId": "<portfolio-id>",
  "kind": "rebalance",
  "state": "accepted",
  "createdAt": "2026-02-27T00:00:00.000Z",
  "updatedAt": "2026-02-27T00:00:00.000Z",
  "refs": {
    "workflowId": "<workflow-id>",
    "runId": "<run-id>",
    "requestId": "<request-id>",
    "executionSource": "manual",
    "scheduleId": "<rebalance-schedule-id>",
    "scheduledFunctionId": "42",
    "scheduledRunAt": "2026-02-27T00:00:00.000Z"
  }
}
```

`v2.portfolio.actions.submit` also includes additive execution guidance:

* `nextSteps.poll` -> `v2.executions.get`
* `nextSteps.stream` -> `v2.executions.stream`
* `nextRecommendedCommands` with ready-to-copy polling/stream commands
* `refs.executionSource` distinguishes manual and automated starts (`manual`, `scheduled`, `event`, `scheduled_function`)
* `refs.requestId`, `refs.scheduleId`, `refs.scheduledFunctionId`, and `refs.scheduledRunAt` are additive correlation fields when the upstream flow can provide them

## Execution Detail

Use `v2.executions.get` for a lightweight summary handle,
`v2.executions.detail` for the persisted execution narrative, and
`v2.executions.result` for raw terminal machine output.

`v2.executions.detail` returns:

* `operation`: the canonical execution handle
* `summary`: the current headline, phase, and terminal reason when available
* `timeline`: curated execution events suitable for user-facing progress UIs or agents
* `graph`: optional rebalance observer graph metadata for richer visualizations

## Execution Stream

`v2.executions.stream` is the canonical typed realtime surface for first-party
web clients. It replays ordered canonical execution events and then switches to
live tail delivery.

Each event uses this envelope:

```json theme={null}
{
  "streamId": "execution:op_123",
  "eventId": "1743163201000-0",
  "sequence": 17,
  "ts": "2026-03-28T15:10:00.000Z",
  "kind": "execution.timeline_appended",
  "operationId": "op_123",
  "cursor": "1743163201000-0",
  "payload": {}
}
```

* Resume is supported with `cursor`. SSE reconnects also use the same opaque
  cursor via `Last-Event-ID`.
* `v2.executions.get` and `v2.executions.detail` remain the snapshot and
  hydration APIs.
* `GET /v1/executions/:operationId/stream` exposes the same canonical event log
  over SSE for CLIs, API integrators, and future chat surfaces.
* `GET /v1/ai/executions/:operationId/ag-ui-stream` exposes an AG-UI projection
  of that same execution log for TanStack AI-compatible clients.

## Automation Run Detail

Use `v2.automationRuns.get` for the current canonical authored-run snapshot and
`v2.automationRuns.events` for ordered timeline pagination.

`v2.automationRuns.get` returns:

* `run`: the current automation run handle
* `latestSequence`: the latest persisted timeline sequence

`v2.automationRuns.events` returns:

* `runId`
* `items`: canonical automation timeline items
* `nextSequence`: the last delivered sequence for resume/polling

## Automation Run Stream

`v2.automationRuns.stream` is the typed realtime surface for authored
automation-engine runs. It follows the same resume model as
`v2.executions.stream`.

Each event uses this envelope:

```json theme={null}
{
  "streamId": "automation:auto_run_123",
  "eventId": "timeline:auto_run_123:4",
  "sequence": 4,
  "ts": "2026-04-19T16:00:00.000Z",
  "kind": "automation.timeline_appended",
  "runId": "auto_run_123",
  "cursor": "sequence:4",
  "payload": {}
}
```

* Resume is supported with `cursor`.
* Direct HTTP SSE for automation runs has been retired. HTTP consumers can read
  snapshots and event pages through `GET /v1/automation-runs/:runId` and
  `GET /v1/automation-runs/:runId/events`.

## Retired/Legacy Mapping

* `sessionKeys.getPortfolioSignableMessage` -> `v2.portfolio.create.prepare`
* `sessionKeys.createPortfolioWithSignature` -> `v2.portfolio.create.confirm`
* `sessionKeys.refreshSessionKeyMessage*` -> `v2.portfolio.permission.refresh.prepare`
* `sessionKeys.refreshSessionKeyWithSignature` -> `v2.portfolio.permission.refresh.confirm`
* `strategyInstances.getStrategyInstancesOwnedByAddress` -> `v2.portfolio.list`
* authenticated dashboard owner-list hydration -> `v2.portfolio.listByOwnerDbV2`
* `strategyInstances.getStrategyInstance` -> `v2.portfolio.get`
* `strategyInstances.archiveStrategyInstance` -> `v2.portfolio.archive`
* `strategyInstances.unarchiveStrategyInstance` -> `v2.portfolio.unarchive`
* `strategyInstances.getUnifiedPortfolioHistory` -> `v2.portfolio.activity.list` (`kind="history"`)
* derived allocation history feed -> `v2.portfolio.activity.allocationHistory.list`
  * `items` remains the cursor-paginated newest-first activity feed for the History table.
  * The response still includes `checkpoints` for backward compatibility when `includeCheckpoints=true`; chart consumers should pass `includeCheckpoints=false` and use the fixed chart route instead.
* fixed allocation history chart -> `v2.portfolio.activity.allocationHistory.chart`
  * `checkpoints` is the 90-day chart-ready sampled allocation series. It includes daily balance-history snapshots, including days without events, so charts should not treat event rows as the only drawable points.
  * Event feed pagination remains on `allocationHistory.list`; chart checkpoints do not use the event cursor.
* `strategyInstances.getStrategyPerformanceMultipleTimeframes` -> `v2.portfolio.performance.get` (`timeframes`)
* `strategyInstances.getNetDepositsAndWithdrawals` -> `v2.portfolio.performance.get` (`netFlows`)
* `strategyInstances.getStrategyPerformanceSeries` -> `v2.portfolio.performance.series`
* dashboard asset analytics -> `v2.portfolio.performance.assetMetrics`
* `strategyInstances.getStrategyInstanceVaultsOwnedByAddress` -> `v2.portfolio.recipients.list`
* `strategyInstances.getStrategyInstancesOwnedByAddress` (public profile view) -> `v2.public.portfolio.list`
* curated public portfolio-card hydration -> `v2.public.portfolio.listByIds`
* `strategyInstances.getStrategyInstance` (unowned read) -> `v2.public.portfolio.get`
* `vaults.getVaultsPortfolioDataForStrategyInstance` (unowned read) -> `v2.public.portfolio.vaults.list`
* `schedules.getStrategyInstanceSchedule` (unowned read) -> `v2.public.portfolio.schedule.get`
* public allocation history feed -> `v2.public.portfolio.activity.allocationHistory`
* public fixed allocation history chart -> `v2.public.portfolio.activity.allocationHistoryChart`
* user profile hydration aggregate -> `v2.public.account.profile.get`
* `strategyInstances.getAllAssetsAcrossWalletStrategies` (public profile aggregate) -> `v2.public.account.assets.list`
* `strategyInstances.getAccountPerformanceOverview` (public profile aggregate) -> `v2.public.account.performance.overview`
* `strategyInstances.getAccountPerformanceSeries` (public profile aggregate) -> `v2.public.account.performance.series`
* `rebalance.execute` -> `v2.portfolio.actions.submit` (`kind="rebalance"`)
* `withdrawAndDeposits.process*` -> `v2.portfolio.actions.submit` (`kind="withdraw"`)
* `bridge.processBridgeRequest` -> `v2.portfolio.actions.submit` (`kind="bridge"`)
* `executeLifiQuote.execute` -> `v2.portfolio.actions.submit` (`kind="swap"`)
* backend-quoted LiFi swap execution -> `v2.portfolio.actions.submit` (`kind="swap_backend_quote"`)
* manual Aave/Morpho/Fluid yield execution -> `v2.portfolio.actions.submit`
  (`kind="yield"`, returned as `kind="execution"`)
* `schedules.*` -> `v2.portfolio.schedule.*`
* user-approved scheduled functions -> `v2.portfolio.scheduledFunctions.*`
* `rebalance.getStatus` / `workflows.*` / `temporal.*` / `bridge.getBridgeStatus` / `executeLifiQuote.getStatus` -> `v2.executions.get` / `v2.executions.detail` / `v2.executions.result` / `v2.executions.stream`

## Compatibility Notes

* Selected legacy namespaces remain mounted for backwards compatibility.
* Retired procedure paths are intentionally removed from legacy routers (for example `rebalance.execute` and migrated `schedules.*` write mutations), and should be treated as `v2.portfolio.*`-only.
* New agent/CLI integrations should target only `v2.*`.
* v2 keeps external language portfolio-first (`portfolioId`), even when internal implementations still use strategy-instance identifiers.
* `v2.executions.list` reads from the persisted execution narrative store for `rebalance`, `swap`, `withdraw`, and `bridge`.
* `v2.executions.get` can also hydrate manual engine `execution` handles, including yield actions, from the runtime engine operation API while those operations are in flight.
* `v2.executions.detail` reads the persisted execution narrative store and adds timeline plus optional graph metadata.
* `v2.executions.stream` replays and tails the canonical execution event log rather than synthesizing updates from repeated detail polling in the normal path.
* `v2.operations.*` remains mounted temporarily as a deprecated, best-effort surface only.
* `v2.portfolio.status` is resilient: permission/session data still returns if rebalance status source is unavailable; `rebalance` is `null` and `degraded.rebalanceUnavailable=true`.
* `v2.portfolio.status` includes additive semantic `convergence` metadata for owner UIs:
  * `state`: `awaiting_review | queued | moving | blocked | aligned | failed`
  * `summary`: portfolio-level explanation of the highest-priority outstanding condition
  * `approvalId?`, `proposalId?`, `runId?`, `targetId?`, `targetType?`, `executionStatus?`, `updatedAt`: optional references for detail surfaces
* `v2.portfolio.updates.*` is the desired-state composer surface for `webapp-v2`:
  * `preview` derives semantic before/after state without mutating live strategy definitions
  * `create` writes a `draft` revision
  * `submit(mode="review")` promotes that revision to `pending_review`
  * `submit(mode="apply_now")` accepts the revision and syncs it through blueprint versioning for runtime parity
  * `get` returns `PORTFOLIO_UPDATE_NOT_FOUND` when the revision is missing; it does not return a nullable success payload
  * update rows expose additive `revision` metadata so owner surfaces can reason about desired-state history
  * update status includes `dismissed` for review declines
* `v2.portfolio.approvals.*` is the canonical owner-facing approval surface:
  * approval targets are semantic (`portfolio_update` or `portfolio_update`)
  * `decide` is the canonical owner decision entrypoint
* `v2.portfolio.runs.*` exposes semantic convergence/execution rows projected from desired-state apply, copilot execution, and rebalance runtime sources.
* `v2.portfolio.events.*` exposes stored semantic control-plane history:
  * `list` returns canonical timeline events and paginates with an opaque cursor derived from `(createdAt,id)`
  * `stream` provides polling-backed internal event streaming for owner surfaces and can resume from the last opaque cursor
* semantic control-plane reads are now pure `db-v2` reads by default; operational read repair remains available only behind `PORTFOLIO_CONTROL_PLANE_ENABLE_READ_REPAIR`
* `v2.portfolio.context.*` exposes immutable context snapshots referenced by approvals, runs, and events.
* `v2.portfolio.agents.*` exposes portfolio-scoped automated principals and their allowed scopes.
* Permission UIs should prefer `v2.portfolio.permission.status` (session keys + evm agent only) for lighter polling.
* Portfolio identity payloads add `canonical_strategy_blueprint_id`:
  * `v2.portfolio.list` returns it on each portfolio row.
  * `v2.portfolio.get` and `v2.public.portfolio.get` return it on the nested `blueprint` object.
  * Consumers can use it to route portfolio viewers to the canonical strategy page for unchanged forks.
* Dashboard migration contracts:
  * `v2.portfolio.listByOwnerDbV2` is the db-v2-backed authenticated owner-list route used by the webapp dashboard shell.
  * Each row includes `id`, `archived`, `created_at`, `owner_address`, `owner_account_index`, `blueprint_name`, `blueprint_description`, `is_public`, `primary_chain_id`, `updated_at`, and `vault_addresses`.
  * `v2.portfolio.performance.series` is now a typed envelope, not a raw array:
    * `series`: historical/accounting performance points
    * `stats`: backend-computed `DAY | WEEK | MONTH | YEAR | ALL` metrics with `absoluteChangeUsd` and `twrPct`
    * `live`: current value plus normalized `live.assets`
  * `live.assets` uses frontend-facing asset IDs directly and includes:
    * `assetId`, optional `dbAssetId`, `symbol`, `decimals`, `priceUsd`, `valueUsd`, `liveBalanceFormatted`, `liveBalanceRaw`, `vaultAddress`
  * `v2.portfolio.performance.assetMetrics` augments live assets with db-v2 analytics:
    * `assetId`, optional `dbAssetId`, `marketValueUsd`, `netInvestedUsd`, `pnlUsd`, `priceUsd`, `priceMissing`, `asOf`
* Schedule APIs are manual-first and optional:
  * new/forked/mirrored portfolios do not auto-create a rebalance schedule.
  * `v2.portfolio.schedule.get` adds `scheduleStatus` (`active | paused | disabled | archived | null`).
  * archived schedules are returned as non-active compatibility payloads (`scheduleExists=false`, `scheduleId=null`, `scheduleData=null`) while still reporting `scheduleStatus="archived"`.
  * archived schedules are terminal: `v2.portfolio.schedule.create`, `v2.portfolio.schedule.update`, and `v2.portfolio.schedule.setFromText` return `SCHEDULE_ARCHIVED` and do not reactivate automation.
  * `v2.portfolio.schedule.runNow` includes additive operation identifiers: `accepted`, `runId`, and `operationId`.
* Scheduled function APIs are UTC-anchored:
  * `v2.portfolio.scheduledFunctions.*` is currently in beta/private rollout and is hidden in the webapp unless the relevant UI feature flag is enabled for the user.
  * `v2.portfolio.scheduledFunctions.list` accepts additive
    `includeTargeting=true` to include schedules owned by the user's investing
    account or other owned portfolios when their config targets the requested
    portfolio.
  * `v2.portfolio.scheduledFunctions.create` supports additive
    `schedule` input for `functionKey="recurring_swap"` and `functionKey="recurring_transfer"`:
    `hourly`, or `{ frequency: "daily" | "weekly", hourUtc, day? }`.
  * The server translates that input into persisted `intervalMs/startAt/endAt`.
  * `v2.portfolio.scheduledFunctions.delete` is a soft delete. Rows are retained for audit/history, marked deleted in storage, and excluded from normal list/run queries.
  * `hourUtc` and weekly `day` are interpreted in UTC; local DST shifts are not preserved.
  * Current product-supported handlers:
    * `recurring_swap`: buy a token with USDC or sell a token to USDC.
    * `recurring_transfer`: move Base USDC from the Investing Account into an owned Base portfolio.
* Portfolio builder APIs:
  * `v2.portfolio.builder.*` persists pre-portfolio onboarding state in `db-v2`.
  * `builder.generateDraft` first attempts deterministic portfolio-construction parsing/resolution for allocation prompts, then falls back to the legacy generated strategy path when the prompt is unsupported or incomplete.
  * Portfolio-construction draft responses include resolver review metadata on `construction`: `reviewStatus`, per-asset `requiresConfirmation`, `reviewMode`, `reviewReason`, candidate alternatives, and `questions[]` for asset/chain/contract clarification before a generated draft is applied.
* Policy APIs are authoritative backend preflight surfaces:
  * `v2.portfolio.policy.evaluate` is the canonical machine-facing evaluator for `rebalance`, `schedule`, `swap`, and `withdraw`.
  * `v2.portfolio.policy.scheduleAdvisory` is a convenience wrapper over the same schedule evaluation path.
  * `v2.portfolio.policy.config.get|update` is the additive Smart Portfolio configuration surface for declarative allocation, rebalance, and yield settings.
  * Observed portfolio facts for public policy evaluation come from backend resolvers, not caller-supplied exposure snapshots.
  * Public `swap` and `withdraw` policy evaluation uses `chainIds` to derive request exposure. Raw `requestExposure` remains accepted temporarily for wire compatibility, but is ignored.
  * Public `rebalance`, `schedule`, and `scheduleAdvisory` still accept legacy `currentExposure` / `plannedExposure` fields for compatibility, but those fields are ignored immediately.
  * Public `withdraw` policy evaluation also ignores legacy raw `portfolioTotalUsd` and `isFullWithdraw` inputs; the backend derives those facts authoritatively when available.
  * Decision payloads preserve `status`, `allowed`, `primaryBlockingReason`, `reasons`, `remainingConditions`, and `nextEligibleAt`, and now also include:
    * `authoritative`
    * `facts`
    * `ruleResults`
  * `primaryBlockingReason` and each entry in `reasons[]` now also include:
    * `policyId`
    * `category`
    * `policySetId` when the reason came from a chain-scoped policy set
  * `ruleResults[]` now includes:
    * `reasonCodes`
    * `policyIds`
    * `reasonCode` remains as the first/primary code for backward compatibility.
  * `facts[].status` distinguishes `present`, `missing`, `stale`, and `synthetic`.
  * `facts[].origin` distinguishes backend-loaded facts (`server`), backend-derived request facts (`derived`), and caller-supplied fallback/hypothetical facts (`client`).
  * `authoritative=true` only means the triggered policy rules had sufficient trusted backend facts; missing, stale, synthetic-only, or client-sourced required facts downgrade the decision to non-authoritative.
  * Backend primary-chain resolution is authoritative: service loaders win over caller fallback hints, and `fallbackPrimaryChainId` is treated as a compatibility hint rather than the source of truth.
  * `remainingConditions[]` may include `market_available` for Ondo-backed `rebalance` and `swap` decisions, carrying the blocked asset IDs, symbols, availability type (`closed` or `halted`), and `nextOpenAt` when Ondo provides a reopen time.
  * Omitting `schedule` input on the schedule policy endpoints returns the backend default cadence instead of a denial.
  * Schedule defaults are resolved through a backend chain-policy-set catalog. The generic non-ETH baseline is the current Base-mainnet policy shape, while Ethereum remains stricter because of gas-cost-driven limits.
  * Multi-chain policy-set selection now prefers the authoritative primary chain over catalog order. If multiple supported policy sets match and the primary chain cannot disambiguate them, evaluation defers instead of picking one arbitrarily.
  * Configured-but-unsupported chain policy sets fail closed with a blocking denial rather than silently inheriting the generic non-ETH baseline.

## ETH Mainnet Product Limits (Effective March 2, 2026)

For portfolios where `primary_chain_id = "1"`:

* Rebalance cooldown: max once every 24 hours.
  * error: `REBALANCE_ETH_MAINNET_COOLDOWN_ACTIVE`
* Schedule constraints:
  * interval cadence only,
  * interval must be `>= 24h`,
  * default interval on schedule creation is `24h`.
  * errors: `SCHEDULE_ETH_MAINNET_INTERVAL_TOO_SHORT`, `SCHEDULE_ETH_MAINNET_INTERVAL_ONLY`
* Withdraw constraints:
  * minimum selected withdraw amount `$10`,
  * if portfolio total is below `$10`, only full-balance withdraw is allowed.
  * errors: `WITHDRAW_ETH_MAINNET_BELOW_MINIMUM`, `WITHDRAW_ETH_MAINNET_FULL_REQUIRED_UNDER_MINIMUM`
* Swap-like actions (`kind="swap"` and `kind="swap_backend_quote"`):
  * minimum notional `$10`,
  * error: `LIFI_ETH_MAINNET_BELOW_MINIMUM_TRADE`
* `v2.portfolio.scheduledFunctions.*` currently allows:
  * `functionKey="recurring_swap"`
  * `functionKey="recurring_transfer"`
  * `functionKey="recurring_account_deposit"` for account-origin recurring
    deposits into an existing portfolio
* As of March 30, 2026:
  * `recurring_swap` is intentionally narrow: buy a token with USDC or sell a token to USDC.
  * `recurring_transfer` is intentionally narrow: transfer Base USDC from the Investing Account into an owned portfolio with an active Base vault.

## Public Read Surface Notes

* `v2.public.portfolio.list`, `v2.public.portfolio.listByIds`, `v2.public.portfolio.get`, `v2.public.portfolio.vaults.list`, `v2.public.portfolio.schedule.get`, `v2.public.portfolio.performance.*`, `v2.public.portfolio.activity.list`, `v2.public.portfolio.activity.allocationHistory`, and `v2.public.portfolio.activity.allocationHistoryChart` do not currently enforce `is_public`.
  * These routes currently only return `PORTFOLIO_NOT_FOUND` when the portfolio does not exist.
* The web portfolio detail route now intentionally relies on this public-read surface for shared/read-only sections even when the viewer later authenticates as the owner.
  * Shared page regions do not swap data sources after auth resolves.
* `v2.public.portfolio.listByIds` is the lightweight multi-card hydration route for curated public portfolio pages.
  * Input: `portfolioIds[]` with up to 50 ids.
  * Output: `results` keyed by portfolio id.
  * Missing or unreadable ids return `null` for that entry instead of failing the whole batch.
* `v2.public.account.profile.get` is the canonical user-profile hydration endpoint used by `/user/:userId`.
  * It returns the user-page aggregate header value, Investing Account snapshot, and profile portfolio cards/table payload in one response.
  * The profile payload is intentionally user-page-specific and is not currently filtered by `is_public`.
  * User-profile schedule rendering is db-v2-only: portfolios without a v2 schedule row surface as manual/unscheduled.
* `v2.public.account.assets.list`, `v2.public.account.performance.series`, and `v2.public.account.performance.overview` are owner-scoped aggregates with no per-portfolio visibility check.

## Agent Auth Notes

* `v2.agentAuth.createApiKey` keeps `ownerAddress` optional for compatibility.
* If `ownerAddress` is provided, it must match the authenticated wallet address.
* API key metadata persists the canonical wallet owner; mismatches are rejected.

## Webapp Migration Status

As of February 28, 2026, the webapp is migrating in phases:

* Migrated to `v2.*` in owner-authenticated flows:
  * portfolio create/confirm and permission refresh
  * owner portfolio list/get/archive
  * desired-state composer preview/draft/review/apply via `v2.portfolio.updates.*`
  * owner schedule get/create/update/setFromText/pause/resume/runNow
  * bridge submit via `v2.portfolio.actions.submit`
  * withdraw/transfer submit via `v2.portfolio.actions.submit`
  * swap submit via `v2.portfolio.actions.submit` (`kind="swap"`)
  * backend-quoted swap submit via `v2.portfolio.actions.submit` (`kind="swap_backend_quote"`)
  * unified history via `v2.portfolio.activity.list`
  * rebalance runtime status reads via `v2.portfolio.status`
  * execution polling via `v2.executions.get` in migrated withdraw/transfer/swap paths
* Soft deprecation logging is enabled for selected migrated v1 procedures.
* Migrated to `v2.public.*` in non-owner/public flows:
  * user profile hydration via `v2.public.account.profile.get`
  * competition row vault data + schedule reads

## Deferred v1 Allowlist

The following remain on v1 in this phase by design:

* Operation/status exceptions with no clean parity signal yet:
  * withdraw-as-target-asset (`ETH/USDC`) is unsupported and rejected client-side (legacy backend path is also unsupported)
* Public/unowned reads that conflict with v2 owner-auth requirements:
  * public profile and competition views
  * OG routes and legacy routes reading arbitrary user portfolio data
* Analytics endpoints without v2 parity:
  * chart/account/competitor/backtest-style `strategyInstances.*`
* `schedules.getSchedulesForMultipleStrategies` (no v2 equivalent yet)
