Security
Rotate all secrets
ACCESS_TOKEN_SECRET(embedded mode) must be at least 32 characters and cryptographically random. Generate one with:
GAS_WALLET_PRIVATE_KEYshould be a dedicated hot wallet, never your main wallet. Fund it with just enough ETH to cover expected gas costs.KEY0_WALLET_PRIVATE_KEY(for the refund cron) should be stored in a secrets manager (e.g., AWS Secrets Manager, Doppler, Vault), not in.envfiles checked into source control.
Never expose private keys in logs or responses
Key0 never logs private keys itself, but make sure your
fetchResourceCredentials callback and onPaymentReceived hook do not inadvertently log sensitive data. Audit your logging pipeline.Set AGENT_URL / agentUrl to your public HTTPS address
The agent card (
/.well-known/agent.json) and the resourceEndpoint in the AccessGrant both use agentUrl as the base URL. If it is wrong, agents will call the wrong host. In standalone mode, set AGENT_URL=https://api.yourdomain.com.Network switch: testnet → mainnet
Set network to mainnet
- Embedded:
network: "mainnet"inSellerConfigandnew X402Adapter({ network: "mainnet" }) - Standalone:
KEY0_NETWORK=mainnet
8453, testnet (Base Sepolia) = 84532.Update USDC contract address
Key0 selects the correct USDC address automatically based on the
You never need to set this manually — just ensure
network setting:| Network | USDC Address |
|---|---|
| Base mainnet | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Base Sepolia | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
network is correct.Fund wallets with real USDC and ETH
- Seller wallet (
walletAddress/KEY0_WALLET_ADDRESS): receives USDC payments. No ETH needed unless you also use it for refunds. - Gas wallet (
GAS_WALLET_PRIVATE_KEY): needs Base mainnet ETH to pay gas fortransferWithAuthorization. Top up as needed. - Refund wallet (
KEY0_WALLET_PRIVATE_KEY): needs both USDC (to refund) and ETH (for gas). Keep a buffer for refunds in proportion to your transaction volume.
Infrastructure
Use managed Redis and Postgres — not in-process stores
The in-memory challenge store (
MemoryChallengeStore) loses all state on restart. In production, use:- Redis (
RedisChallengeStore,RedisSeenTxStore) — required for the refund cron and gas wallet nonce locking - Postgres (
PostgresChallengeStore) — optional alternative for challenge storage; Postgres has stronger ACID guarantees
Enable health checks
The standalone server exposes
GET /health returning { status: "ok" }. Add this as your container/load balancer health check endpoint.For embedded mode, add your own health endpoint that verifies the Redis/Postgres connection.Pin Docker image versions
Use a specific version tag in production (e.g.,
key0ai/key0:1.2.3), never latest. This prevents unexpected breaking changes during deployments.Horizontal scaling
Key0 is stateless — all state lives in Redis/Postgres. You can run multiple instances behind a load balancer with no sticky sessions. Ensure:
- All instances share the same Redis instance (for the gas wallet nonce lock and BullMQ)
- All instances share the same Postgres database (if using Postgres storage)
Settlement
Choose your settlement mode
Key0 supports two settlement modes. Choose one for production:
See Settlement Strategies for a full comparison.
| Mode | How it works | Best for |
|---|---|---|
Gas Wallet (gasWalletPrivateKey) | Key0 submits EIP-3009 transactions using a gas wallet you control | Full control, no third-party dependency |
CDP Facilitator (cdpApiKeyId + cdpApiKeySecret) | Coinbase’s infrastructure submits the transaction | Managed gas, simpler ops |
Gas wallet: ensure ETH balance
If using a gas wallet, monitor its ETH balance. A depleted gas wallet will cause settlement to fail (and trigger refunds). Set up an alert when ETH balance drops below a threshold.
Refunds
Enable the refund cron
The refund cron is disabled by default. Enable it by providing Or in embedded mode:
KEY0_WALLET_PRIVATE_KEY (for the refund wallet) and configuring:Set an appropriate grace period
REFUND_MIN_AGE_MS should be longer than your TOKEN_ISSUE_TIMEOUT_MS × TOKEN_ISSUE_RETRIES to avoid refunding challenges that are still being processed. Default: 5 minutes.Observability
Enable the audit store
The audit store (
IAuditStore) logs every payment event and state transition. In production, use the Redis or Postgres audit store for durable event history:Use the onPaymentReceived hook
Wire the This hook fires asynchronously and never blocks the payment response.
onPaymentReceived hook in your SellerConfig to emit events to your analytics pipeline (Segment, Mixpanel, Amplitude, or a webhook):Set up error alerting
Monitor for
INTERNAL_ERROR and TOKEN_ISSUE_TIMEOUT error codes — these indicate your fetchResourceCredentials / ISSUE_TOKEN_API endpoint is failing. Excessive timeouts may trigger automatic refunds.Deployment
Set all required environment variables
Review the Environment Variables reference. The minimum required for production:Standalone:
KEY0_WALLET_ADDRESSISSUE_TOKEN_API(your token endpoint)KEY0_NETWORK=mainnetAGENT_URL(your public HTTPS URL)REDIS_URLorDATABASE_URL
KEY0_WALLET_ADDRESSACCESS_TOKEN_SECRET(≥ 32 chars, cryptographically random)KEY0_NETWORK=mainnetREDIS_URL
Reverse proxy configuration
Key0 responds with custom headers (
payment-required, payment-response, www-authenticate). Ensure your reverse proxy does not strip them. In nginx:
