Security
The key is a secret. The prompt is not our business.
What we store
An account (name, email, password hash), the provider admin key encrypted at rest, the key’s public id and name, spend snapshots, rules, and alert events. We do not store prompts, completions, tool arguments, or embeddings. There is no column for them.
Encryption
Provider keys and Slack webhook URLs are encrypted with AES-256-GCM before they touch Postgres. The payload is a versioned string of initialization vector, auth tag, and ciphertext. The 32-byte key comes from ENCRYPTION_KEY (base64, hex, or raw). Disconnecting a provider deletes the ciphertext. Password hashes use scrypt. Session cookies are random, httpOnly, and only the SHA-256 of the token is stored.
OpenAI admin key
This key is not read-only. An organization admin key can administer projects. BillCurfew calls only:
- GET /v1/organization/usage/completions grouped by api_key_id, project_id, and model, in 1-minute buckets
- GET /v1/organization/costs grouped by api_key_id for the daily dollar total
- GET /v1/organization/projects and GET /v1/organization/projects/{project_id}/api_keys to name the keys
- DELETE /v1/organization/projects/{project_id}/api_keys/{api_key_id} only when you confirm a kill or you opted into auto-kill
Deletion is permanent. The product requires the word DELETE for a manual kill, and auto-kill shows that warning before you can turn it on. Token costs between polls are estimates from a maintained price table. The daily cap prefers the costs endpoint, which is the provider’s own dollar figure. Unknown models use a high fallback so a new flagship cannot hide.
Anthropic admin key
The Admin API is for organization accounts only. An individual Claude Console account will be rejected, and the product says so. We call:
- GET /v1/organizations/usage_report/messages grouped by api_key_id and model
- GET /v1/organizations/api_keys
- POST /v1/organizations/api_keys/{id} with status inactive to kill, or active to restore
Dollar amounts are estimates from Anthropic’s published token prices, including cache reads and cache writes. Killing a key is reversible.
OpenRouter management key
Paste a management key, not an inference key. We call GET /api/v1/keys and read usage_daily in dollars. A kill is PATCH /api/v1/keys/{hash} with disabled true. Turning it back on is the same call with disabled false. Spike detection uses the difference between polls, because OpenRouter reports a daily total rather than minute buckets.
Slack, email, Stripe
Slack webhooks must be public https URLs. We refuse localhost, link-local, and private addresses. Email goes out through the configured sender (Resend) and only contains spend, key names, and the rule that fired. Stripe handles cards. We store the customer id and subscription id. Checkout sets product tax code txcd_10103000 (SaaS, personal use) on the existing price’s product and enables automatic tax. Provider keys are not shared with Stripe.
What this is not
BillCurfew is not a request firewall. A loop can spend for up to one poll interval before we notice. It is also not a substitute for the provider’s own project budget. It is a second clock, outside the prompt path, for people who will not put a proxy in front of production agents.