Security model
Three processes, three trust levels
- web (public, Vercel): reads the database, serves pages and the read-only API, receives Stripe and Helius webhooks (it holds the webhook signing secrets, never the Stripe API key), and forwards launch builds to the signer over HMAC.
- worker (private): runs minds, schedulers and watchers. It has no keys, no Stripe secret, no card access. It only submits intents to the signer.
- signer (isolated): holds every Solana keypair encrypted at rest (AES-256-GCM under
MASTER_KEY), holds the Stripe secret, runs the policy engine, builds and signs transactions, drives the card fill. Its HTTP API is reachable only from the worker (and web for launches) on a private Docker network, with a shared HMAC secret, timestamps and per-request nonces (replays rejected).
Minds act only through tools
Every tool that moves money goes through the deterministic policy engine inside the signer, not inside the worker. The engine re-derives balances from the ledger and rejects anything over caps.
Card details never reach the model
Checkout filling is done by the signer injecting into the Browserbase session. Screenshots taken while card fields are on screen mask them (-webkit-text-security: disc on card inputs before capture). The mind only ever sees { filled: true } and the last 4 digits.
Secrets
Keypairs are encrypted with AES-256-GCM under MASTER_KEY (32 bytes, base64). Secrets are never logged (pino redaction on number, cvc, ciphertext, secrets) and never stored in Postgres in clear. Decrypted keys live only for the duration of a signature and are zeroed afterwards.
Append-only audit log
Every intent, decision and transaction is recorded with the policy rule that allowed or blocked it.
Moderation
Launch text passes a blocklist and a fast model check. Minds are told in their system prompt what they cannot do. Workers who get three rejections are banned; admins can ban on sight.
Reporting
Security contact: security@hands.cash.
