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 forPOST /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:
200.
1. Open a shell next to the API
Every call below runs on the compute server againstlocalhost:8081, which
skips Cloudflare and the BFF and talks to FastAPI directly.
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.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.
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:
{"error":{"code":...}} body):
5. Complete the portal in a browser
Copy the URL out of the file without printing it to a shared terminal:/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
"reconciled": truewith connections → done."reconciled": truewith"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) → wrongstate. The session is not consumed; re-read$STATEfrom/tmp/portal-session.jsonand retry.portal_session_expired(410) → more than five minutes elapsed. Redo step 4.
7. Verify the durable rows in Postgres
From the compute server (vRack address, perCLAUDE.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.
provider = 'snaptrade', state = 'active',
external_user_id equal to your Sequency UUID (never an email).
Portal session — single use, terminal, and holding no URL.
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 <= 5 minutes. The table has no portal-URL column at
all; confirm with \d broker_portal_sessions.
Connection — state mapped from provider readback.
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.
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: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}
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).