Security architecture

How the platform is secured, as built.

How requests are authenticated, limited, billed, isolated and recorded. Each section gives the parameters from the code, the failure outcome of each step, and the claims we do not make.

01Request lifecycle

Nine steps from request to response.

Every public API call, playground request and agent step follows this sequence. Sandbox executions follow the same order with the sandboxes scope, per-second pricing and a sandbox node in place of a model provider.

  1. 01

    Request enters the gateway

    • A request ID is issued and returned on every response, including errors.
    • CORS headers are sent only to origins on the API_CORS_ORIGINS allowlist.
    • Keys are read from Authorization: Bearer or x-api-key, never from query strings.

    RecordedNothing.

    On failure

    No failure mode of its own at this step.

    Not billed
  2. 02

    Key authenticated

    • The key must match proj_live_ plus 32 base62 characters. Its first 8 random characters select one row by unique prefix.
    • HMAC-SHA256 of the full key is compared in constant time, and the comparison runs even when no row matches.
    • Malformed, revoked and expired keys fail with the same error. Keys on archived projects are refused.
    • The body is then read: application/json only, 2 MB maximum, schema-validated.

    RecordedNothing.

    On failure
    • invalid_api_key401
    • forbidden403
    • invalid_request400
    Not billed
  3. 03

    Policy evaluated

    • Key scope must include inference (or sandboxes for code execution).
    • The model slug must exist, be enabled and have a configured provider.
    • Project allowlist first, then key allowlist. A key can only narrow what the project allows.
    • Tool definitions are rejected for models without tool support.
    • Rate limits: per key (default 60/min), then per project (default 120/min).

    RecordedRate-limit counters for the key and project windows.

    On failure
    • insufficient_scope403
    • model_not_found404
    • model_not_allowed403
    • service_unavailable503
    • rate_limit_exceeded429
    Not billed
  4. 04

    Spend reserved

    • Input tokens are over-estimated at one token per two UTF-8 bytes. Output is capped at max_tokens, or 4,096 by default, never above the model maximum.
    • Reservation = cost of estimated input + cost of maximum output, at the model’s current price.
    • In one transaction holding the project’s balance row lock: key monthly limit, project monthly budget, then a ledger reserve that fails if available balance would drop below zero.
    • The request row is written as pending, with a salted input fingerprint.

    RecordedLedger reserve entry · request row (pending) · key last-used time.

    On failure
    • budget_exceeded402
    • insufficient_balance402
    Not billed

    The transaction rolls back.

  5. 05

    Request routed

    • The slug resolves to one upstream model and one adapter. There is no silent fallback to a different model.
    • The adapter attaches the platform’s provider credential server-side. Upstream timeout defaults to 120 s.
    • Sandbox executions go to a separate node as HMAC-signed, timestamped requests.

    RecordedStructured log line with IDs, model and reserved µUSD. No content.

    On failure

    No failure mode of its own at this step.

    Not billed
  6. 06

    Provider executes

    • Upstream HTTP errors map to fixed, generic messages. Upstream response bodies are discarded, never logged or returned.
    • A client disconnect aborts the upstream call.

    RecordedNothing until completion.

    On failure
    • provider_error502
    • provider_timeout504
    • invalid_request400
    • sandbox_error502
    Billed only for delivered output

    If nothing was delivered, the full reservation is released and no receipt is issued. If a stream fails after output was delivered, the delivered portion is billed and receipted with status failed.

  7. 07

    Usage reconciled

    • Token counts come from the provider’s usage report. If absent, they are estimated and flagged usage_estimated.
    • Each price component rounds up to the next µUSD: ⌈tokens × price per 1M ÷ 1,000,000⌉.
    • The ledger debits the actual cost and releases the unused reservation.
    • If actual cost exceeds the reservation, the overage is drawn only from available balance. Any shortfall is logged as uncollected, never recorded as a negative balance.

    RecordedLedger debit and release · request row (tokens, costs, latency, output fingerprint).

    On failure
    • internal_error500
    Settled cost

    Settlement and receipt storage commit in one transaction. If it fails, nothing is debited and the reservation stays held until an operator reconciles it.

  8. 08

    Receipt generated

    • Payload version 1: IDs, hashed project ID, operation, model, provider, token counts, µUSD cost, latency, fingerprints, timestamps, run link.
    • Canonical JSON → keccak256 → EIP-191 signature from the dedicated receipt key.
    • The receipt is signed from metered usage before step 07 commits, and stored in the same transaction.

    RecordedReceipt row: payload, hash, signature, signer address.

    On failure

    No failure mode of its own at this step.

    Shares step 07’s transaction
  9. 09

    Response returned

    • The body includes usage, billing.cost_micro_usd, billing.fingerprint_salt and a receipt summary with its public URL.
    • Headers: x-request-id, x-receipt-id, cache-control: no-store, x-content-type-options: nosniff.
    • Errors use { error: { type, message, request_id } }, with no stack traces, credentials or upstream bodies.

    RecordedCompletion log line with token counts, cost and latency.

    On failure

    No failure mode of its own at this step.

    Billed at settled cost
02Identity and access

Credentials, sessions, roles and rate limits.

Password and API secret hashing

Live

Passwords use scrypt, a memory-hard function, with parameters stored in each hash so they can be raised later.

API keys carry about 190 bits of entropy, so a keyed HMAC is used instead of a slow hash. Because the HMAC key lives outside the database, a database leak alone cannot be used to test candidate keys offline.

Password KDF
scrypt, N = 2^15 (32,768), r = 8, p = 1, 64-byte output
Salt
16 random bytes per password; input normalized to NFKC; 10–256 characters
Stored as
scrypt$15$8$1$<salt>$<hash>
Timing
Unknown accounts are checked against a dummy hash; comparison uses timingSafeEqual
API key format
proj_live_ + 32 base62 characters from rejection sampling (no modulo bias)
API key stored as
Lookup prefix (proj_live_ + 8 characters) and HMAC-SHA256(API_KEY_HASH_SECRET, "api-key:v1:" + key)
Verification
Prefix lookup, then constant-time comparison, which also runs on a lookup miss
Disclosure
The full key is shown once at creation and cannot be retrieved again

Session handling

Live

Console sessions are stored in the database. The cookie holds a random token and the database holds only its SHA-256, so a copied database row cannot be replayed as a session.

Wallet sign-in uses Sign-In with Ethereum (EIP-4361) with single-use nonces, restricted to Robinhood Chain networks and the application domain. Smart-contract wallet signatures (ERC-1271) are not supported.

Token
32 random bytes, base64url
At rest
SHA-256 of the token, unique-indexed
Cookie
HttpOnly · SameSite=Lax · Path=/ · Secure in production
Name
__Host-session in production (no Domain attribute, Secure required); session in local development
Lifetime
30 days; expired sessions are deleted on access and swept periodically
Metadata
Keyed hash of the client IP and the user agent (256 characters maximum)
SIWE nonces
Single use, 10-minute expiry, consumed atomically

Role separation

Live

Members of an organization hold one of four ranked roles. Each console route declares a minimum role, and the server checks it against the session on every request.

A project that does not exist and one the user cannot access return the same not_found error. Whoever creates an organization becomes its owner. API keys are separate from roles: a key acts only within its project and its scopes.

Capabilityowneradmindeveloperviewer
Read usage, receipts and run historyAllowedAllowedAllowedAllowed
Create API keys and use the playgroundAllowedAllowedAllowedNot allowed
Create and run agents and sandboxesAllowedAllowedAllowedNot allowed
Change policies, billing and project settingsAllowedAllowedNot allowedNot allowed
Manage members and destructive organization actionsAllowedNot allowedNot allowedNot allowed

Rate limiting

Live

Limits are fixed windows stored in Postgres and updated with a single atomic upsert, so they hold across serverless instances without a separate cache.

When a limit is exceeded the response is rate_limit_exceeded (429) with a retry-after header. A fixed window can admit up to twice the limit across a window boundary.

EndpointKeyed byLimitWindow
API requestsAPI keyKey setting, default 601 min
API requestsProjectPolicy, default 1201 min
Sign-inClient IP (HMAC)3015 min
Sign-inAccount (SHA-256 of email)1015 min
Sign-upClient IP (HMAC)101 h
Wallet nonceClient IP (HMAC)3010 min
Wallet sign-inClient IP (HMAC)2010 min
Sign-outClient IP (HMAC)601 min
03Browser security

CSRF protection and response headers.

Cross-site request checks

Live

Every state-changing console and auth endpoint rejects cross-site requests explicitly, in addition to SameSite cookies.

The public API is authenticated with bearer keys, not cookies. It sends CORS headers only to allowlisted origins.

Methods
All methods except GET and HEAD
Origin present
Must equal the application origin or the request’s own origin
Origin absent
Sec-Fetch-Site must be same-origin
Failure
forbidden (403), with no side effects
Redirects
Post-sign-in destinations are restricted to console paths

Security headers

Live

Headers are applied to every route. Scripts are restricted to the site’s own origin, but inline scripts are allowed: the policy does not use nonces.

Browsers can connect only to this origin and the public Robinhood Chain RPC endpoints. Server-side RPC URLs, which may contain provider keys, are never listed.

X-Content-Type-Options
nosniff
X-Frame-Options
DENY
Referrer-Policy
strict-origin-when-cross-origin
Permissions-Policy
camera=(), microphone=(), geolocation=(), payment=()
Cross-Origin-Opener-Policy
same-origin-allow-popups
Strict-Transport-Security
max-age=63072000; includeSubDomains; preload
X-Powered-By
Not sent
Content-Security-Policy · production
default-src 'self'
script-src 'self' 'unsafe-inline'
style-src 'self' 'unsafe-inline' https://fonts.reown.com
img-src 'self' data: blob: https://walletconnect.org https://walletconnect.com
        https://secure.walletconnect.org https://secure.walletconnect.com
        https://api.web3modal.org https://api.web3modal.com
font-src 'self' data: https://fonts.reown.com
connect-src 'self' https://rpc.mainnet.chain.robinhood.com https://rpc.testnet.chain.robinhood.com
            WalletConnect relay, RPC, keys and verify services
            (*.walletconnect.org / *.walletconnect.com, api.web3modal.org / .com)
            wss://www.walletlink.org https://cca-lite.coinbase.com
frame-src https://verify.walletconnect.org https://verify.walletconnect.com
          https://secure.walletconnect.org https://secure.walletconnect.com
frame-ancestors 'none'
base-uri 'self'
form-action 'self'
object-src 'none'
04Secrets, logs and retention

What is encrypted, logged and kept.

Encrypted secrets

Live

Values that must be decrypted later are sealed with AES-256-GCM, which authenticates the ciphertext as well as encrypting it.

Every value is encrypted with one deployment key, which production refuses to start without. There is no external KMS or per-tenant key hierarchy today.

Cipher
AES-256-GCM, 96-bit random IV per value, authentication tag verified on decrypt
Format
v1.<iv>.<tag>.<ciphertext>
Key
32 bytes from SECRETS_ENCRYPTION_KEY
Encrypted
Upstream provider credentials · agent instructions · agent run inputs · agent run outputs
Hashed, not encrypted
Passwords, API keys, session tokens, client IPs

Server-side provider credentials

Live

Customers never supply or see upstream provider keys. The gateway holds one credential per provider and uses it for every project.

Source
The active encrypted row in provider_credentials, falling back to a server environment variable
In memory
Decrypted in the server process and cached for 60 seconds
Exposure
Never sent to the browser; no NEXT_PUBLIC variable holds a secret
Upstream rejection
Reported as a generic provider_error. Upstream response bodies are never forwarded.
Operator hint
The last four characters are stored for identification

Request IDs and structured logging

Live

Logs are one JSON line per event, correlated by request, project, key, provider, model and receipt IDs. Callers never pass prompts, outputs or secrets to the logger.

Redaction is a second line of defense. Values under sensitive-looking field names are replaced, and any string shaped like an API key is masked. Errors are logged as name and message only.

Redacted fields
Names matching secret, password, authorization, api_key, private_key, token, cookie, prompt, content, messages, stdout, stderr or code
Masked values
proj_live_…proj_live_[redacted]
Sandbox node
Logs never contain code, stdin, stdout or stderr. Containers run with --log-driver none.
Log line · inference.completed
{
  "time": "2026-09-14T12:04:33.271Z",
  "level": "info",
  "message": "inference.completed",
  "service": "gateway",
  "request_id": "req_01J8Z3QK4W…",
  "project_id": "…",
  "api_key_id": "…",
  "provider": "anthropic",
  "model": "claude-sonnet",
  "receipt_id": "rcpt_01J8Z4B2KX…",
  "status": "succeeded",
  "input_tokens": 1284,
  "output_tokens": 431,
  "usage_estimated": false,
  "customer_cost_micro_usd": "7566",
  "latency_ms": 1840
}

Metadata retention

Live

The database keeps what metering, billing and proof require. Content passes through the gateway and is not written to the database.

The fingerprint salt is stored with the request so the owner can reproduce a commitment. Anyone holding the database could therefore test a guess against a fingerprint.

Agent instructions and run input and output are the exception: they are stored, encrypted. No automatic expiry is applied to request metadata today.

Stored per request
  • Request ID, project, API key ID, source (API, console, agent)
  • Operation, provider, model slug and upstream model
  • Status, HTTP status, error type, stream flag
  • Input and output token counts; reserved, upstream and customer cost
  • Latency and time to first token
  • Input and output fingerprints, and the fingerprint salt
  • Receipt ID and timestamps
  • Sandbox runs: runtime, exit code, duration, cost, stdout and stderr byte counts
Never stored
  • Prompts, messages and tool definitions sent to models
  • Completions and tool-call arguments returned by models
  • Sandbox source code, stdin, stdout and stderr
  • API keys (only prefix and HMAC) and session tokens (only SHA-256)
  • Passwords (only scrypt hashes)
  • Raw client IP addresses (only keyed hashes)
05Sandbox isolation

Untrusted code runs on a separate host.

The web app and gateway never execute user code. They send signed requests to a sandbox node, a separate service that starts one disposable container per execution.

Node authentication

Live

Requests and responses are both authenticated with HMAC-SHA256 under a shared secret of at least 32 characters. Because responses are signed, a spoofed or misrouted endpoint cannot inject results into signed receipts.

The gateway refuses a non-HTTPS node URL in production unless an operator explicitly overrides it. The node is meant to be reachable only from the gateway.

Signing strings
request  = HMAC-SHA256(secret, "v1\nrequest\nPOST\n/v1/execute\n" + timestamp + "\n" + sha256(body))
response = HMAC-SHA256(secret, "v1\nresponse\n" + execution_id + "\n" + timestamp + "\n" + sha256(body))
Timestamp
Rejected if more than 60 seconds from the node’s clock
Replay
Each execution ID is accepted once. IDs are held in memory for twice the skew window; restarting the node clears them.
Comparison
Constant-time, on exactly 64 lowercase hex characters
Capacity
Four concurrent executions per node by default. Beyond that the node returns 503, which the gateway reports as service_unavailable.

Container boundary

Live

Each container runs as the unprivileged user 65534, with a read-only root filesystem, all capabilities dropped, no privilege escalation and no network. Limits are enforced by cgroups.

User code is never placed in argv. A fixed launcher reads it from stdin into a noexec tmpfs and runs it under GNU timeout. Runtime images have setuid bits stripped and package managers removed.

docker run · labels and environment omitted
docker run -i --pull never \
  --network none --ipc private --hostname sandbox \
  --read-only --user 65534:65534 \
  --cap-drop ALL --security-opt no-new-privileges \
  --memory 256m --memory-swap 256m --oom-score-adj 1000 \
  --cpus 0.5 --pids-limit 64 \
  --tmpfs /workspace:rw,noexec,nosuid,nodev,size=16m,mode=1777 \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m,mode=1777 \
  --ulimit nofile=256:256 --ulimit fsize=16777216:16777216 --ulimit core=0:0 \
  --log-driver none --init --stop-timeout 1 \
  --entrypoint /bin/sh platform-sandbox-python:1 \
  /opt/sandbox/launcher.sh python 30000

Host model

Access to a rootful Docker daemon is equivalent to root on the host. Sandbox nodes therefore run on dedicated hosts that hold no data and no credentials besides the sandbox secret, and never on the web or gateway host.

Recommended
Rootless Docker, or rootful Docker with userns-remap
Optional
gVisor user-space kernel via SANDBOX_DOCKER_RUNTIME=runsc
Images
Built locally and run with --pull never; no registry access at execution time
Cleanup
Containers are removed after each run; stale ones are removed at startup and every 5 minutes
Development runner
A plain-process runner exists for local development only. It provides no isolation and refuses to start when NODE_ENV=production.
06Receipts and settlement

Signed records and verified purchases.

Receipts

Live

Receipts are signed with a dedicated key that only signs receipts. It is separate from the key that sends anchoring transactions and from contract owner and treasury keys.

Development deployments fall back to a publicly known key and flag their receipts as untrustworthy. Production refuses to start without a configured signer.

Signing key
RECEIPT_SIGNER_PRIVATE_KEY; anchoring uses ANCHOR_SIGNER_PRIVATE_KEY
Rotation
Previous signer addresses stay trusted through a configured signer list
Payload excludes
Prompts, outputs, code, instructions, API secrets and raw project IDs (only keccak256 of the project ID)
Fingerprints
sha256(salt ‖ canonical content), with a fresh 32-byte salt per operation, returned to the caller
Verification
On every public view: payload structure, hash match, signature recovery, trusted signer, Merkle inclusion when anchored
Anchoring
Beta. Merkle roots are submitted to ReceiptAnchor on Robinhood Chain only when an operator enables it.

On-chain purchase verification

Beta

Compute credits are bought with USDG through the ComputeCreditRouter contract, which moves funds directly to the treasury and never holds a balance. A purchase buys non-withdrawable credits; it is not a deposit.

The backend never trusts a transaction hash from the browser. It treats the hash as a hint and verifies the transaction against its own RPC before crediting. A background indexer also scans confirmed blocks, so credits arrive even if the browser is closed.

Network
The RPC’s chain ID must match the configured Robinhood Chain network
Transaction
Status success, with at least the configured confirmations (default 3)
Event
CreditPurchased emitted by the configured router, with the configured USDG token, a non-zero amount and the configured treasury
Account
The event’s account ID must map to an existing project
Amount
Converted to µUSD and rounded down; never credits more than was received
Idempotency
Unique on (chain ID, transaction hash, log index) in the deposits table and again in the ledger idempotency key
Contract
Rejects fee-on-transfer tokens by checking the treasury balance delta; pausable; two-step ownership transfer

Confidential compute

Coming soon

Execution inside attested trusted execution environments is on the roadmap. The private_routing and confidential privacy tiers are reserved in the schema and receipt format, but only the standard tier exists today.

Available today
Standard tier: prompts are processed in plaintext by the selected provider, over TLS
Not available
TEE execution, remote attestation, private routing
07Limits of this page

What we do not claim.

  • No third-party security audit has been completed.
  • There is no uptime SLA.
  • Confidential or TEE compute is not available.
  • The development sandbox runner provides no isolation. It refuses to start in production.
  • The smart contracts have not been audited.
Responsible disclosure

Report suspected vulnerabilities by email. Include the affected endpoint, reproduction steps and any request_id values from responses.

Use only accounts and data you own, and do not degrade service for other users.

support@example.com