Agentic Trading
Security

Secrets Management

1. No secret in code, ever. All secrets via env vars or secret stores.

Where every secret lives, who has access, how it gets there, and how it gets rotated.

Principles

  1. No secret in code, ever. All secrets via env vars or secret stores.
  2. No secret in logs. Sentry breadcrumbs, console output, error messages — scrubbed.
  3. Service-role keys never leave the server. Browser code only ever sees the anon Supabase key.
  4. User exchange keys are encrypted at rest with an envelope key the user does not hold.
  5. Decryption happens in one place (the live runner process), not on the web edge.
  6. Rotate-able — every secret can be rotated without code changes.

Inventory

SecretTypeStored inUsed byRotation
Supabase anon keyPublicVercel env (NEXT_PUBLIC_*)Browser, Next.js clientn/a
Supabase service role keyServer-onlyVercel env, Fly secretapps/web server, apps/live-runner90 days
Supabase Vault: credential encryption keyServer-onlySupabase Vaultapps/live-runner (decrypt only)Yearly + on incident
AI Gateway key (platform)Server-onlyVercel env, Fly secretAnywhere generateText is called90 days
AI Gateway BYOK (user-provided)Per-user, server-onlyDB (encrypted)Forwarded to gateway per-requestUser-controlled
Fly Machines API tokenServer-onlyVercel envapps/web provisioning code180 days
User exchange API keysPer-userDB exchange_credentials (encrypted)apps/live-runner only (decrypted on machine boot)User-controlled
News provider API key (CryptoPanic etc)Server-onlyVercel envpackages/data-ingest180 days
Sentry DSNMostly-publicVercel envAll processesn/a
GitHub Actions secretsCI-onlyGH repo secretsCI workflows180 days
Supabase DB password (direct conn)Server-onlyVercel env, Fly secretMigration runner only90 days

Where things live

Vercel environment variables

Configured per Vercel environment (Development / Preview / Production) via vercel env CLI or the dashboard. Auto-injected at build + runtime for Vercel Functions.

Naming:

  • NEXT_PUBLIC_* → safe to expose to browser
  • Everything else → server-only; never imported in client components

When linked, Supabase via Marketplace auto-provisions:

  • SUPABASE_URL
  • SUPABASE_ANON_KEY (→ public)
  • SUPABASE_SERVICE_ROLE_KEY
  • DB connection string

Fly secrets

Set per Fly app via fly secrets set KEY=value. Encrypted at rest by Fly, exposed as env vars to the running process.

For the live runner, set:

  • SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
  • AI_GATEWAY_API_KEY
  • VAULT_KEY_ID (the ID of the Supabase Vault key for credential decryption)

The DEPLOYMENT_ID is per-machine and passed via the Fly Machines API request, not a secret but a parameter.

Supabase Vault

Postgres-native secret store. We use it for the symmetric key that encrypts user exchange credentials.

-- One-time setup
select vault.create_secret('<base64-encoded-256-bit-key>', 'credential_envelope_key');

Access from the runner via the vault.decrypted_secrets view (service-role only). The Vault key itself is never logged.

User-supplied secrets in the DB

exchange_credentials.ciphertext and .nonce columns store AES-256-GCM ciphertext of the user's exchange API key. The key never appears in plaintext in the DB; we never log it; we never return it to the browser.

The user can:

  • Add a credential (the UI POSTs the plaintext to a server action, which encrypts and stores)
  • Delete a credential
  • See metadata (label, wallet address, created_at, last_used_at)

The user cannot:

  • Retrieve the plaintext after saving (must rotate to change)

Encryption / decryption flow for exchange credentials

Encryption (when user adds credential)

  1. UI form collects plaintext API key over HTTPS
  2. POST to /api/credentials server action
  3. Server fetches credential_envelope_key from Vault
  4. Server generates random 12-byte nonce
  5. Encrypts with AES-256-GCM(key, nonce, plaintext)
  6. Inserts exchange_credentials row with ciphertext + nonce
  7. Discards plaintext from memory

Decryption (when live runner boots)

  1. Live runner boots, reads DEPLOYMENT_ID
  2. Loads deployments row → finds user_id and target exchange/network
  3. Loads matching exchange_credentials row (ciphertext + nonce)
  4. Fetches credential_envelope_key from Vault
  5. Decrypts: AES-256-GCM-decrypt(key, nonce, ciphertext)
  6. Passes plaintext into broker adapter constructor
  7. Plaintext is held in the broker's closure; never written to DB, logs, or returned over network
  8. On process exit, garbage collected with the process

If the live runner machine is compromised, the attacker gets access to one user's exchange key (the one this machine was provisioned for). Per-machine isolation means a compromise is bounded.

Rotation procedures

Supabase service role key

  1. In Supabase dashboard, generate new service role key
  2. Update Vercel env via vercel env add SUPABASE_SERVICE_ROLE_KEY production
  3. Trigger Vercel redeploy
  4. Update Fly secret via fly secrets set SUPABASE_SERVICE_ROLE_KEY=... -a agentic-live-runner
  5. Fly auto-restarts machines with new secret
  6. Confirm old key revoked in Supabase dashboard

AI Gateway key

Same pattern — generate in Vercel dashboard, propagate to Vercel + Fly, restart, revoke.

Vault envelope key (re-encryption)

The hard one. To rotate:

  1. Generate new key, store in Vault under a new ID (e.g., credential_envelope_key_v2)
  2. Run re-encryption job:
    • For each exchange_credentials row: decrypt with old key, encrypt with new key, update ciphertext + nonce + key_version column
  3. Once 100% migrated, retire old key in Vault

This is operationally heavy; we only do it on a confirmed compromise or annually. Plan: add a key_version column from the start to make this possible without schema migration.

User exchange API keys

User-driven via the UI:

  1. User generates new key on Hyperliquid
  2. User adds new credential in the platform
  3. User updates affected deployments to point at the new credential
  4. User deletes old credential
  5. User revokes old key on Hyperliquid

We surface a "last used" timestamp so users can confirm a credential isn't in use before deleting.

What goes in .env.example

Every secret has a placeholder in .env.example so a new dev can see what they need. Format:

# Supabase (link via Vercel Marketplace, or copy from Supabase dashboard)
SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_URL=
SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=  # NEVER commit; NEVER expose to client

# AI Gateway
AI_GATEWAY_API_KEY=         # from Vercel dashboard → AI Gateway → API Keys

# Vault key id for credential decryption
VAULT_KEY_ID=credential_envelope_key

# Fly (for provisioning live deployments from web app)
FLY_API_TOKEN=
FLY_APP_NAME=agentic-live-runner

# Hyperliquid mainnet (platform-level; user trading keys are stored encrypted in DB)
# none — users supply their own. Testnet deprecated (ADR-0015).

# News provider
CRYPTOPANIC_API_KEY=

# Sentry (Phase 3)
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=

.env files are gitignored; .env.example is committed.

Logging hygiene

The logger (one shared instance across packages) has a redaction list:

const REDACT_KEYS = [
  'api_key', 'apiKey', 'apiSecret', 'private_key', 'wallet_private_key',
  'password', 'authorization', 'cookie', 'secret', 'token', 'envelope_key',
];

Any object logged at any level has these keys recursively redacted. Tests in packages/logger/__tests__/redaction.test.ts cover the common cases.

Additionally: never log full request/response bodies for endpoints that handle credentials. Use field-level extraction.

Sentry / error monitoring scrubbing

When we add Sentry (Phase 3), configure:

  • beforeSend hook to drop events containing any redact-list keys
  • Disable breadcrumbs for the credentials-handling routes
  • Set sendDefaultPii: false

What to do if a secret leaks

  1. Rotate immediately (procedure above for the affected secret)
  2. Audit — git history check for accidental commits, log grep for accidental logs
  3. Investigate scope — what data could have been accessed with the leaked secret?
  4. Notify — if user data was at risk, contact affected users
  5. Postmortem — document in docs/incidents/ with root cause and prevention

For user exchange keys specifically: the user must rotate on the exchange side; we can only invalidate our copy.

On this page