Status: Draft
Date: 2026-05-05
Author: software-architect
Parent epic: (to be filed by product-manager)
BLR parallel track: docs/legal/research/fidelity-api-integration-2026-05-05.md
ADRs produced: 0050-fidelity-api-surface-choice, 0051-fidelity-auth-flow, 0052-broker-adapter-interface
Raxx confirmed a Hybrid BYOB strategy (2026-04-29): Alpaca as default, plus an aggregator and first-party broker integrations in the roster. Fidelity is the first first-party addition under evaluation. It is a high-value target — largest retail custodian by AUM in the US, large overlap with the active-trader persona Raxx targets.
This design covers the technical side of the integration. The parallel BLR track covers legal access eligibility, partner agreement requirements, and regulatory posture. Neither track produces implementation code until both tracks converge with approval.
The integration must be:
frontend/status-page/src/__tests__/lintNoBrokerNames.test.js).All platform invariants apply. Those with heightened relevance to this design:
| # | Invariant | Implications here |
|---|---|---|
| I1 | No stored credentials. | Customer's Fidelity session tokens stored only as envelope-encrypted ciphertext if OAuth is used; never plaintext at rest. |
| I2 | Passkeys only for Raxx auth. | Fidelity OAuth does not change Raxx's own auth surface. Step-up WebAuthn required before every live order submit. |
| I3 | Paper-first gating. | Fidelity live-handoff requires paper-profitable-for-N-cycles gate or explicit per-flow audited override. |
| I4 | Audit trail for state changes affecting money / permissions. | Every Fidelity connection lifecycle event + every order submit writes an audit_log row. |
| I5 | GDPR by default. | Fidelity OAuth tokens (if stored) are PII-adjacent; erasure on DSR must reach Fidelity (revoke upstream) and our DB. |
| I6 | Credentials into infra, not code. | Fidelity OAuth client_id + client_secret live in Infisical. Rotatable without redeploy. |
| I7 | Kill switch on live execution paths. | FIDELITY_LIVE_HANDOFF_DISABLED=1 must block new connections and new live order submissions. |
This is the top-level architectural question that must be answered before implementation begins. BLR findings determine what is actually accessible. This section scores each candidate surface so operator + BLR can converge on one.
| Surface | Description | Order capability | Auth UX | Data freshness | Partner friction |
|---|---|---|---|---|---|
| Wealthscape Integration Xchange (WIX) | Fidelity's institutional / RIA API program. REST + FIX. Full order lifecycle. | Full trade lifecycle (read + trade + corporate actions) | Redirect OAuth; customer authorizes via Fidelity institutional login | WebSocket available for institutional tier | High — formal partnership, RIA/custodial relationship required, legal agreement, onboarding fee likely |
| Active Trader Pro (ATP) unofficial | No documented public API. ATP's local API is reverse-engineered / community. | Read + some order submission (unofficial) | Proprietary token; not OAuth | Polling or local hooks | Not a viable path — no partner agreement possible, ToS violation risk |
| Brokerage Developer Portal (FDX / Open Finance) | Open Finance Data Exchange (FDX) spec. Primarily read-focused (accounts, positions, transactions). FDX v6+ has limited payment initiation. | Primarily read-only; limited write (ACH, no securities orders in FDX 6.0) | 3-legged OAuth 2.0 (PKCE). Customer redirects to Fidelity.com | Polling (no WebSocket in FDX standard) | Medium — formal app registration, FDX member compliance, but no RIA requirement |
| eMoney / Fidelity Wealth API | Aimed at financial planning software, not trading platforms. | No order submission | OAuth | Data aggregation only | Not relevant — targets wealth planning, not execution |
Primary target: Wealthscape Integration Xchange (WIX) — contingent on BLR confirming access eligibility.
Rationale: WIX is the only surface that provides the full trade lifecycle (order submit, bracket, OCO, fills, position management) at institutional quality. FDX is read-only for securities — unsuitable for order routing. ATP unofficial path is a ToS violation and cannot be a supported integration.
Fallback if WIX is inaccessible: FDX for account/portfolio read + hold live trading pending a later WIX agreement.
This is the highest-priority open question for BLR. See §9.
Based on publicly available Fidelity developer documentation and industry knowledge as of 2026-05-05:
BLR must confirm: eligibility, agreement requirements, timeline, and whether a "pending partnership" sandbox exists before Raxx commits engineering.
fidelity_live_connections tableMirrors the pattern from alpaca_live_connections (ADR 0014). One active connection per user maximum.
CREATE TABLE fidelity_live_connections (
id TEXT PRIMARY KEY, -- uuid v4
user_id TEXT NOT NULL
REFERENCES users(id) ON DELETE CASCADE,
broker_account_id TEXT NOT NULL, -- Fidelity account identifier, opaque
scopes TEXT NOT NULL, -- space-separated OAuth scopes
access_token_ciphertext BLOB NOT NULL, -- envelope-encrypted; NO plaintext
access_token_iv BLOB NOT NULL,
access_token_wrapped_dek BLOB NOT NULL,
kms_key_id TEXT NOT NULL,
refresh_token_ciphertext BLOB, -- if issued
refresh_token_iv BLOB,
refresh_token_wrapped_dek BLOB,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
needs_reauth BOOLEAN NOT NULL DEFAULT 0,
revoked_at TIMESTAMP,
disconnected_at TIMESTAMP, -- customer-side revoke detected
CHECK (length(access_token_ciphertext) > 0),
UNIQUE (user_id) WHERE revoked_at IS NULL AND disconnected_at IS NULL
);
CREATE INDEX fidelity_live_connections_user_idx
ON fidelity_live_connections(user_id)
WHERE revoked_at IS NULL AND disconnected_at IS NULL;
Forbidden columns (CI-grep check): access_token TEXT, access_token VARCHAR, refresh_token TEXT, refresh_token VARCHAR. The plaintext columns must never exist.
broker_order_log table (unified, not Fidelity-specific)The BrokerAdapter abstraction (§5) routes orders from multiple brokers. A unified order-log table spans adapters, scoped by broker_id. This avoids per-broker log tables as the roster grows.
CREATE TABLE broker_order_log (
id TEXT PRIMARY KEY, -- uuid v4
broker_id TEXT NOT NULL, -- 'alpaca' | 'fidelity' | ...
user_id TEXT NOT NULL
REFERENCES users(id),
raxx_order_id TEXT NOT NULL, -- Raxx internal order UUID
broker_order_id TEXT, -- broker-assigned ID after submit
status TEXT NOT NULL, -- 'pending'|'submitted'|'filled'|'rejected'|'cancelled'
order_spec_hash TEXT NOT NULL, -- SHA-256 of serialized OrderRequest; for idempotency
submitted_at TIMESTAMP,
filled_at TIMESTAMP,
rejected_reason TEXT,
audit_event_id TEXT REFERENCES audit_log(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
The key architectural decision (ADR 0052): introduce a BrokerAdapter abstract base class. AlpacaBrokerAdapter and FidelityBrokerAdapter both implement it. A service-locator (BrokerAdapterRegistry) resolves the correct adapter per user at runtime.
# backend_v2/api/services/broker/base.py (new)
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class OrderRequest:
raxx_order_id: str # our idempotency key
symbol: str
side: str # 'buy' | 'sell'
qty: float
order_type: str # 'market' | 'limit' | 'stop' | 'stop_limit' | 'trailing_stop'
time_in_force: str # 'day' | 'gtc' | 'ioc' | 'fok'
limit_price: Optional[float]
stop_price: Optional[float]
trail_amount: Optional[float]
trail_percent: Optional[float]
legs: Optional[list["OrderRequest"]] # for bracket / OCO
@dataclass
class OrderResult:
broker_order_id: Optional[str]
status: str # 'submitted' | 'rejected' | 'error'
rejected_reason: Optional[str]
idempotent_duplicate: bool # True if broker confirmed this was a duplicate
@dataclass
class Position:
symbol: str
qty: float
avg_entry: float
current_price: Optional[float]
class BrokerAdapter(ABC):
broker_id: str # class-level constant, e.g. 'fidelity'
@abstractmethod
def submit_order(self, order: OrderRequest, user_id: str) -> OrderResult: ...
@abstractmethod
def cancel_order(self, broker_order_id: str, user_id: str) -> bool: ...
@abstractmethod
def get_order_status(self, broker_order_id: str, user_id: str) -> dict: ...
@abstractmethod
def get_positions(self, user_id: str) -> list[Position]: ...
@abstractmethod
def get_account(self, user_id: str) -> dict: ...
@abstractmethod
def is_market_hours(self) -> bool: ...
@abstractmethod
def health_check(self) -> dict: ... # for status-page poller
class BrokerAdapterRegistry:
"""Runtime service locator. Resolves adapter per user from their live connection."""
def get_adapter_for_user(self, user_id: str) -> BrokerAdapter: ...
def register(self, adapter: BrokerAdapter) -> None: ...
alpaca_integration.py and trading_runtime.py are NOT changed in this design pass. The refactor into AlpacaBrokerAdapter(BrokerAdapter) is a sub-card (SC-3). The new FidelityBrokerAdapter is built against the interface from the start (SC-6).
sequenceDiagram
participant U as Customer (Antlers)
participant R as Raptor (backend_v2)
participant F as Fidelity OAuth
participant V as Velvet (secret store)
U->>R: POST /api/broker/fidelity/connect
Note over R: gate: paper-profitable OR override+step-up WebAuthn
R->>R: generate state=UUID + PKCE code_verifier
R->>R: store (state, user_id, code_verifier) in session_store TTL=10min
R-->>U: redirect_uri → Fidelity OAuth authorization endpoint
U->>F: browser redirect with client_id, scope, state, code_challenge
F-->>U: Fidelity login + consent screen
U->>F: customer authenticates + grants consent
F-->>U: redirect to Raxx callback: /api/broker/fidelity/callback?code=X&state=Y
U->>R: GET /api/broker/fidelity/callback?code=X&state=Y
R->>R: verify state matches session_store (CSRF protection)
R->>F: POST /oauth/token (code + code_verifier + client_secret from Infisical)
F-->>R: {access_token, refresh_token, expires_in, scope}
R->>R: envelope-encrypt access_token + refresh_token (KMS)
R->>R: INSERT fidelity_live_connections row
R->>R: write audit_log: FIDELITY_CONNECT, user_id, broker_account_id
R-->>U: 200 {connected: true, broker_account_id: "..."}
Note over U: UI shows "Connected" status; broker name not shown
Raptor refreshes proactively when expires_at - now() < REFRESH_WINDOW (default: 15 min). On 401 from Fidelity, sets needs_reauth=true, surfaces re-auth UX to customer.
When Fidelity returns a 401 or explicit "revoked" error on an API call:
disconnected_at = now() on the fidelity_live_connections row.broker_order_log with status='pending' for this user.audit_log: FIDELITY_DISCONNECTED_UPSTREAM, user_id.FIDELITY_OAUTH_CLIENT_ID and FIDELITY_OAUTH_CLIENT_SECRET live in Infisical at path /raxx/v1/{env}/fidelity/. Rotatable without redeploy. These are Raxx's platform credentials for the WIX partner registration, not per-customer credentials.
Raxx's internal OrderRequest (§5a) maps to Fidelity WIX order submission:
| Raxx field | Fidelity WIX equivalent | Notes |
|---|---|---|
symbol |
symbol (CUSIP or ticker) |
WIX may require CUSIP for some order types; adapter normalizes |
side |
orderType.action |
BUY / SELL |
qty |
quantity |
|
order_type='market' |
MARKET |
|
order_type='limit' |
LIMIT with limitPrice |
|
order_type='stop' |
STOP with stopPrice |
|
order_type='stop_limit' |
STOP_LIMIT with both prices |
|
order_type='trailing_stop' |
TRAILING_STOP |
BLR/testing must confirm WIX supports this |
time_in_force='day' |
DAY |
|
time_in_force='gtc' |
GOOD_TILL_CANCEL |
|
legs (2 legs) |
BRACKET or OCO order |
Confirm WIX bracket/OCO support in sandbox |
Open question for BLR/WIX onboarding: confirm that WIX supports bracket orders and trailing stops via the REST API (vs FIX only).
Preferred: WebSocket subscription to Fidelity order-update stream (if available on WIX REST tier). Fallback: polling at 5s intervals for open orders, 30s for confirmed fills. Order fill webhook from Fidelity (if available) is the most efficient path but requires a public inbound endpoint.
Recommendation: design for polling first (simpler, no inbound webhook infra), migrate to WebSocket/webhook once partnership is confirmed and sandbox validates.
raxx_order_id (uuid v4, generated by Raptor before the first submit attempt) is included in the Fidelity order submission as a client-order-id field (if WIX supports it; BLR to confirm). Raptor tracks order_spec_hash in broker_order_log; on retry, checks for an existing row with the same hash before resubmitting.
Fidelity WIX provides market data (quotes, depth, options chains) scoped to the customer's account. This is distinct from Raxx's server-side market data layer.
Design decision: Fidelity market data is not integrated into MarketDataHub in v1. Reasons:
Fidelity market data is used only for post-fill confirmation and position reconciliation, not as a feed for MBT or charting. This is the same posture as Alpaca live-handoff (live account provides position/order state; shared hub provides market data for MBT).
If the BLR track surfaces a distinct "Fidelity data-only" partnership path (e.g., FDX read-only), this is a separate design track.
Fidelity's Velvet adapter manages Raxx's platform-level OAuth credentials (client_id + client_secret for the WIX app registration), not per-customer tokens. Per-customer tokens are managed by Raptor's token-refresh logic (§6b).
velvet/adapters/fidelity.py sketch# velvet/adapters/fidelity.py (stub — feature-developer fills in)
class FidelityOAuthClientAdapter(BusAdapter):
"""
Manages rotation of Raxx's Fidelity WIX OAuth client secret.
Stage flow:
Verify — POST /oauth/token with current client_secret (client_credentials grant)
→ confirms client_id+client_secret pair is still valid.
Mint — NOT AUTOMATABLE: Fidelity does not expose a client-secret
rotation API. New secret must be minted in the Fidelity
WIX developer portal by an operator.
→ pre-staged-mint flow (same pattern as PostmarkSenderTokenAdapter).
Distribute — InfisicalWriteAdapter writes new secret to vault; this
adapter validates the new secret is live against Fidelity.
Revoke — Operator confirms old secret revoked in WIX portal; this adapter
cannot automate revocation.
RotationContext.new_value = the new client_secret (pre-staged by operator).
RotationContext.credential_name = 'FIDELITY_OAUTH_CLIENT_SECRET'
Env vars read at push() time (never at module load):
FIDELITY_OAUTH_CLIENT_ID — from Infisical /raxx/v1/{env}/fidelity/
FIDELITY_OAUTH_CLIENT_SECRET — current (old) secret, pre-staged rotation
Security invariants:
- client_secret is NEVER logged. SHA-256[:12] only in audit.
- AdapterResult.error_message MUST NOT contain credential values.
Feature flag:
FLAG_VELVET_FIDELITY_ADAPTER=1 to enable network calls.
"""
broker_id = "fidelity"
def push(self, credential_name, new_value, context) -> AdapterResult:
self._assert_flag_on()
# 1. Verify new_value is live: POST /oauth/token (client_credentials)
# 2. On success: return AdapterResult(ok=True)
# 3. On failure: return AdapterResult(ok=False, error_message=...)
...
The RotationContext fields needed:
| Field | Value |
|---|---|
credential_name |
FIDELITY_OAUTH_CLIENT_SECRET |
new_value |
Pre-staged new secret (operator-minted in WIX portal) |
env |
prod or staging |
job_id |
UUID from rotation_jobs.id |
rotate_timestamp |
UTC ISO-8601 |
| Failure | Raptor behavior | Customer-facing | Kill switch |
|---|---|---|---|
| Fidelity API rate limit (429) | Exponential backoff (3 retries, max 30s). If all retries exhausted, order rejected with status='error', audit log row written. |
"Your order could not be submitted. Please try again in a few minutes." | FIDELITY_LIVE_HANDOFF_DISABLED=1 |
| Fidelity partial outage (5xx on order submit) | Same backoff as rate limit. Paper-mode fallback is NOT offered (paper is MBT, not Fidelity). Orders are queued in broker_order_log with status='pending' for operator review. |
Status page trade-execution → degraded. "Order submission delayed." |
FIDELITY_LIVE_HANDOFF_DISABLED=1 |
| Fidelity full outage | Same as above. No new live orders submitted. Existing positions unaffected (Fidelity holds them). | Status page degraded. Console auto-creates FreeScout ticket (existing "Investigate" pattern). | FIDELITY_LIVE_HANDOFF_DISABLED=1 |
| Customer OAuth token expired | Raptor proactive refresh (§6b). If refresh fails, needs_reauth=true. Subsequent order attempts return HTTP 403 to Antlers. |
"Reconnect your brokerage to resume live trading." Re-auth flow triggered. | Same |
| Customer revokes access at Fidelity | Detected on next API call (401). disconnected_at set. Pending orders cancelled. Audit log written. |
"Your brokerage connection was disconnected. No orders will be submitted until you reconnect." | Same |
| Network partition (Raptor can't reach Fidelity) | Same as 5xx. Backoff, queue, surface to status page. | Status page degraded. | Same |
| KMS unavailable (can't decrypt token) | Order rejected immediately. Audit log written with KMS_UNAVAILABLE. No retry (KMS unavailability is not transient on the seconds scale). |
"Service temporarily unavailable." | Separate KMS health gate |
Add a sub-entry under the existing broker-connectivity surface. Per existing posture, partner_name stays null publicly.
# config/status-surfaces.yaml — proposed addition
- id: broker-fidelity-connectivity
display_name: "Brokerage Connection"
category: downstream_3p
probe_url: null
partner_status_url: null # Fidelity does not expose a public machine-readable status API
# as of 2026-05-05; manual operator update or polling fidelity.com
# HTTP probe if it proves stable. Flag for BLR/partnership track.
partner_name: null # intentionally null — generic copy only per posture
public_description: "Connection to your brokerage for order routing and account data."
feature_flag: fidelity_live_handoff # not shown on status page until flag is GA
Fidelity public status: Fidelity does not expose a machine-readable status API (e.g., Atlassian statuspage JSON) as of 2026-05-05. The partner_poller would need to probe https://www.fidelity.com/ or a known API endpoint directly for an HTTP 200 as a liveness proxy. This is a weak signal — treat as "no public status URL" until the partnership produces a dedicated status endpoint.
WIX provides a sandbox environment for registered partners. Access requires the formal partnership agreement. Until that is in place:
unittest.mock / pytest-mock to mock FidelityBrokerAdapter.submit_order().FIDELITY_SANDBOX_URL is set in the test environment (CI skips otherwise).Following the Alpaca pattern (backend_v2/tests/):
fixtures/fidelity_order_response.json — sample WIX order submit response.fixtures/fidelity_order_fill.json — sample fill notification.fixtures/fidelity_oauth_callback.json — sample OAuth token response.fixtures/fidelity_account.json — sample account/positions response.lintNoBrokerNames testThe existing lintNoBrokerNames.test.js already covers fidelity as a banned string in customer-facing copy. No change needed — the lint test enforces the brand-invisible posture automatically.
| Migration | Description | Rollback |
|---|---|---|
M1: broker_order_log table |
Create unified order log table (§4b). Additive. | DROP TABLE broker_order_log |
M2: fidelity_live_connections table |
Create per-user Fidelity connection table (§4a). Additive. | DROP TABLE fidelity_live_connections |
M3: audit_log event types |
Add FIDELITY_CONNECT, FIDELITY_DISCONNECT, FIDELITY_TOKEN_REFRESH, FIDELITY_ORDER_SUBMIT to event-type enum or constraint. |
Remove enum values (safe if no rows use them yet) |
All migrations are additive. No existing tables are altered. Rollback is DROP TABLE on the new tables, safe to run at any point before GA.
The Alpaca alpaca_live_connections table is not touched. The new broker_order_log starts empty.
dark (flag off) — schema migrations run; adapter code ships; no customer exposure
↓
flag: fidelity_live_handoff=false (default)
↓
alpha (operator + internal accounts only)
- FIDELITY_SANDBOX_URL set; integration tests run against WIX sandbox
- Paper-first gate active; only manually-overridden test accounts can connect
↓
beta (Pro+ invite-only cohort, <25 users)
- WIX production credentials active (FIDELITY_OAUTH_CLIENT_ID/SECRET from Infisical)
- paper-profitable-for-N gate enforced (not override)
- Status-page broker-fidelity-connectivity surface active
- Console kill switch `FIDELITY_LIVE_HANDOFF_DISABLED` live
↓
GA (flag promoted to prod default)
- Full Pro+ access
- Velvet adapter for FIDELITY_OAUTH_CLIENT_SECRET rotation active
Feature flags required:
- fidelity_live_handoff — controls customer-facing connect flow + order submission path
- fidelity_adapter_velvet — controls Velvet adapter push() calls (separate from live handoff)
Both flags managed via /console/flags with mark-promote → promote workflow (existing console flag UI).
PII collected:
- Fidelity broker_account_id (opaque account identifier) — stored in fidelity_live_connections.
- OAuth access_token and refresh_token — stored encrypted at rest (envelope encryption, KMS-backed).
- Neither is a "credential" in the replay sense of invariant I1 if encrypted with non-exportable KMS key and no plaintext column exists.
Retention:
- fidelity_live_connections rows retained for 90 days after revoked_at or disconnected_at (audit trail need). Purged by the existing retention job.
- broker_order_log rows retained for 7 years (US securities recordkeeping) regardless of account deletion. This is an intentional carve-out from GDPR erasure — justified by legal retention obligation; must be documented in the GDPR records-of-processing.
DSR (data subject request):
- Access: return fidelity_live_connections metadata (not the decrypted tokens) + broker_order_log rows.
- Erasure: revoke upstream OAuth at Fidelity (where possible via /token/revoke), then delete fidelity_live_connections row. broker_order_log rows are retained under legal retention carve-out but anonymized (user_id nulled, broker_account_id nulled) after retention period.
- Portability: order history exported as CSV on demand.
Audit log:
- Every connection lifecycle event + every order submit is an audit_log row.
- No credential values in audit. SHA-256 prefix only.
- Retention: 7 years (same as broker_order_log).
- Access to audit rows: scoped to active support ticket per existing workflow-uuid-tracing design.
Credential replay protection: - Envelope-encrypted at rest. KMS wrapping key not exportable. Decrypted value exists only in process memory during the request. - If KMS key is compromised, rotation revokes + re-encrypts all rows (sub-card SC-9, future).
Breach notification:
- If fidelity_live_connections ciphertext is exfiltrated, GDPR 72-hour notification clock starts. KMS key status determines whether breach is material (encrypted without KMS key = low risk; if KMS is also compromised = high risk). Breach response procedure already documented in platform GDPR posture.
Secrets rotation:
- FIDELITY_OAUTH_CLIENT_ID + FIDELITY_OAUTH_CLIENT_SECRET in Infisical. Rotatable without redeploy. Velvet adapter (§9) automates verification; rotation of the secret in the WIX portal is manual (pre-staged-mint pattern).
Kill switches:
- FIDELITY_LIVE_HANDOFF_DISABLED=1 — blocks new connections + new live order submissions. Set in Infisical, propagated to Heroku config-vars by Velvet (no redeploy needed).
- FIDELITY_ADAPTER_VELVET_DISABLED=1 — disables Velvet adapter push() without disabling live handoff.
FIDELITY_OAUTH_CLIENT_SECRET once the partnership is live? (90 days same as Alpaca, or partner-agreement-driven?)Feature-developer-sized. Each is one PR.
| ID | Title | Size | Blocks |
|---|---|---|---|
| SC-1 | Schema migration: create broker_order_log table + fidelity_live_connections table |
S | SC-2, SC-5 |
| SC-2 | audit_log migration: add Fidelity event types |
S | SC-5, SC-6 |
| SC-3 | Refactor alpaca_integration.py + trading_runtime.py into AlpacaBrokerAdapter(BrokerAdapter) |
M | SC-6 (parallel ok) |
| SC-4 | Define BrokerAdapter ABC + BrokerAdapterRegistry in backend_v2/api/services/broker/ |
S | SC-3, SC-6 |
| SC-5 | FidelityBrokerAdapter: OAuth connect flow (callback handler + token storage) |
M | SC-6 |
| SC-6 | FidelityBrokerAdapter: order submit + cancel + status poll |
M | — |
| SC-7 | FidelityBrokerAdapter: token refresh + needs_reauth surfacing |
S | SC-5 |
| SC-8 | FidelityBrokerAdapter: upstream-revoke detection + graceful disconnection flow |
S | SC-5 |
| SC-9 | Velvet adapter FidelityOAuthClientAdapter (platform credential rotation) |
S | SC-5 must be in prod first |
| SC-10 | Feature flag wiring: fidelity_live_handoff + console kill switch |
S | SC-4 |
| SC-11 | Onboarding wizard integration: "Connect a broker" flow in Antlers (Fidelity slot) | M | SC-5 |
| SC-12 | Status-page entry: broker-fidelity-connectivity surface + poller probe |
S | — |
| SC-13 | Integration tests: WIX sandbox fixtures + CI skip gate | S | SC-6 |
| SC-14 | DSR handler: Fidelity data in access/erasure/portability pipeline | S | SC-1 |
| SC-15 | broker_order_log 7-year retention job + anonymization on DSR erasure |
S | SC-1 |
Total: 15 sub-cards. Order of claim: SC-4 → SC-1 + SC-2 → SC-3 + SC-5 (parallel) → SC-6 through SC-15.
Hard gates before any SC beyond SC-4: - BLR confirms WIX access eligibility (open question BLR-1). - Operator confirms paper-gate N (open question OP-1). - Operator confirms Pro vs Pro+ tier scope (open question OP-2).
End of design doc. See ADRs 0050, 0051, 0052 for individual decision records. Parallel legal research at docs/legal/research/fidelity-api-integration-2026-05-05.md.