Skip to main content

Runbook: connect a brokerage by hand (no UI)

Connect a real brokerage account end-to-end through the API only, using the P2C connection-plane routes. This is the acceptance path for POST /brokerages/portal-sessions → browser → .../return → provider readback, before any Brokerages UI exists (App PR P4B). Contract: app repo docs/superpowers/specs/2026-08-15-account-management-snaptrade-engineering-companion.md §4.3 (portal sessions), §7.1 (routes), §7.3 (response projection), §9.3 (reconciliation). Secrets discipline. The portal URL is bearer-like: anyone holding it can complete the connection. It expires in five minutes, it is never stored or logged, and it must never be pasted into an issue, a PR, a chat thread, or a terminal recording. Same for the state token and the JWT. Every value in this document is a <PLACEHOLDER>. Scope. SnapTrade test key first. A production connection is a real brokerage authorization: run it against your own account, and never against someone else’s.

0. Prerequisites

On the compute server, /opt/sequency/.env (mode 600) must carry: If BROKERAGE_PORTAL_REDIRECT_BASE_URL is set, the resulting origin must also be registered with SnapTrade, or the portal will refuse the redirect. After editing the file:
Expect 200.

1. Open a shell next to the API

Every call below runs on the compute server against localhost:8081, which skips Cloudflare and the BFF and talks to FastAPI directly. Keep this session open; steps 2-7 all run here.

2. Get a JWT for your own user

Password grant against Supabase. Run this on your laptop (it needs no server access) and copy the token to the server shell.
In the server shell:
The token is short-lived (about an hour). If a call starts returning 401, re-run this step. Your Sequency user UUID is the sub claim — you will need it for the SQL in step 7:

3. Confirm the deployment is configured

  • A JSON body with "integrations": [...] and "refreshed_at" → SnapTrade readback works.
  • {"error":{"code":"provider_not_configured",...}} (HTTP 503) → step 0 is incomplete. Fix it before going further; nothing below will work.
Note a slug you intend to connect (for example "slug": "<BROKER_SLUG>"). Only slugs listed here are accepted in step 4 — there is no local broker list.

4. Create a portal session

broker_slug is optional — omit it to let the user pick an institution inside the portal. intent defaults to connect_read, which is the read-only default the companion requires. Read the parts you need without echoing the URL into your scrollback:
Export what step 6 needs:
Failure responses (all HTTP with a {"error":{"code":...}} body):

5. Complete the portal in a browser

Copy the URL out of the file without printing it to a shared terminal:
Paste it into a private browser window and finish the brokerage login and MFA. You have five minutes; after that, delete /tmp/portal-session.json and redo step 4. The browser will land on SnapTrade’s configured redirect (or on BROKERAGE_PORTAL_REDIRECT_BASE_URL + the allowlisted path, which has no UI yet — a 404 page there is expected and harmless). Nothing in that redirect is authoritative: the connection is not real until step 6 reads it back from the provider. To test the cancel path instead, close the portal without authorizing and continue to step 6 anyway — the expected result is reconciled: true with "connections": [].

6. Return and reconcile

Expected on success:
Interpretation:
  • "reconciled": true with connections → done.
  • "reconciled": true with "connections": [] → the portal was cancelled or no authorization was completed. Nothing was created; redo step 4.
  • "reconciled": false → the provider could not be read this time. The session stays returnable: run the exact same command again. Repeating it is safe by design.
  • portal_state_invalid (400) → wrong state. The session is not consumed; re-read $STATE from /tmp/portal-session.json and retry.
  • portal_session_expired (410) → more than five minutes elapsed. Redo step 4.
Re-read at any time without touching the provider:
Force a fresh provider readback for one connection (bounded, idempotent):
Then clean up the local copy of the URL:

7. Verify the durable rows in Postgres

From the compute server (vRack address, per CLAUDE.md):
-v USER_UUID=... is what makes :'USER_UUID' work inside psql; without it every query below errors on an undefined variable. Provider identity — exactly one row per (user, provider). The encrypted secret is deliberately not selected; never SELECT user_secret_enc.
Expect one row, provider = 'snaptrade', state = 'active', external_user_id equal to your Sequency UUID (never an email). Portal session — single use, terminal, and holding no URL.
Expect status = 'reconciled', returned_at/reconciled_at set, state_hash_bytes = 32 (SHA-256 digest — the raw token is never stored), and expires_at - created_at &lt;= 5 minutes. The table has no portal-URL column at all; confirm with \d broker_portal_sessions. Connection — state mapped from provider readback.
Expect access_type = 'read' and state = 'active_read' for a read-only connect, last_reconciled_at recent, last_error_code NULL. state = 'disabled' means the provider reports the authorization disabled; state = 'error' with last_error_code = 'connection_access_unknown' means SnapTrade returned an access type this deployment does not recognize — capture it and report it rather than working around it. Accounts — provider projection plus local preferences.
Expect one row per brokerage account, account_mask at most four characters (a full account number is never stored), deployment_eligibility = 'read_only' and holdings_sync_state = 'unknown' — the connection plane never marks an account execution-eligible and never claims holdings are synced. Idempotency check. Re-run the step-6 return command, then re-run the connection and account queries: row counts and id values must be unchanged, with only last_reconciled_at moving forward.

8. If something went wrong

Sanitized service logs — codes and statuses only, never provider bodies or URLs:
A line reads brokerage portal create outcome=<outcome> provider=snaptrade code=<stable code>. If you ever see a snaptrade.com URL or a userSecret in these logs, stop and treat it as a security incident: THREATMODEL assumption 7’s SnapTrade exception (2026-08-17, data#1924) forbids it. Metrics for the same events:
  • sequency_brokerage_portal_events_total{event,outcome}
  • sequency_brokerage_reconciliation_events_total{event,outcome}
  • sequency_brokerage_identity_events_total{event,outcome}
Not available yet (do not look for them): webhooks (P3A), manual refresh and disconnect (P3C), account preference updates (P3C), and any execution path — a SnapTrade connection is deliberately not reachable as an execution provider (companion §12.1, data#1848).