Security
Balchemy is built around a wallet-first trust model. You authenticate with a cryptographic signature, not a password. This page explains how that works, what Balchemy can and cannot do with your wallet, and the technical architecture behind every security layer.
For Users
How authentication works
Balchemy uses Sign-In with Solana (SIWS) for Solana wallets and Sign-In with Ethereum (SIWE) for EVM-compatible wallets. Both follow the same flow: you sign a short text message in your wallet extension, and that signature proves you own the wallet address without transmitting your private key. Signing a message is categorically different from approving a transaction — no spending authority is granted.
When you connect your wallet, the backend generates a one-time cryptographic nonce tied to your address. Your wallet signs a message embedding that nonce, and Balchemy verifies the signature against your public key. Once verified, the nonce is discarded and cannot be reused, preventing replay attacks.
Step-by-step authentication flow:
- Click "Connect Wallet" in Hub or Explorer.
- Balchemy requests a fresh nonce from the server.
- Your wallet extension displays a human-readable message containing the nonce, your address, and a timestamp.
- You approve the signature — no funds move at any point.
- Balchemy verifies the cryptographic signature against your wallet's public key.
- A signed JWT is issued and stored in your browser session.
- All subsequent API calls use that JWT as a Bearer token.
Tip: Use a hardware wallet (Ledger, Trezor) for maximum security. Hardware wallets require physical button confirmation before signing anything, so malware on your computer cannot silently approve requests on your behalf.
Session management
Your Balchemy session is controlled by a JWT token issued at authentication time. The default session lifetime is 24 hours. When the token expires you will be prompted to reconnect your wallet and sign a new nonce — your agent settings and trading history are never affected by session expiry.
Session lifetimes vary by context:
- Hub / app sessions: 24 hours by default (operator-configurable up to 7 days)
- Legacy embedded-chat sessions: 1 hour TTL where retired embedded-chat compatibility code remains reachable
- Hub agent JWTs: scope-dependent (see the Hub agent scope table below for TTLs per scope)
Warning: Never share your JWT token. If you suspect it has been compromised, use the authorized Hub/account flow to revoke it. Revocation is immediate — the token is added to a distributed blocklist and will be rejected on every subsequent request even if it has not expired.
Wallet security
Balchemy uses a two-layer wallet model. The two layers have different custody properties, and Balchemy states both plainly:
- Your main wallet stays in your custody. Connecting with SIWS/SIWE only proves ownership through a signed message. Balchemy never receives your private key, and sign-in never grants spending authority over your wallet.
- Agent trading wallets are platform-managed. A Hub agent trades from a separate, dedicated wallet. You decide how much to fund it, and your rules and server-enforced risk limits cap what it can do. Private keys for agent wallets are managed by Balchemy's key vault and encrypted at rest using AES-256-GCM with a data encryption key (DEK) wrapped by a master key encryption key (KEK). Private keys are never stored in plain text on Balchemy servers and never appear in logs or error responses.
Plain-language summary: your own wallet is never in Balchemy's custody; the agent's trading wallet is platform-managed, capped by your rules, and only ever holds what you choose to fund.
Here is what Balchemy can and cannot do with connected wallets:
| Action | Balchemy can do | Balchemy cannot do |
|---|---|---|
| Authenticate you | Verify a signed message against your public key | Read or access your private key |
| Execute trades | Submit transactions from the agent's platform-managed trading wallet | Move funds from external wallets you have not connected |
| Display balances | Read your on-chain balance via public RPC | Modify any on-chain state without your authorization |
| Revoke access | Invalidate your session JWT | Retain access after you disconnect your wallet |
You control the trading wallet your Hub agents use. You fund it, and withdrawals must go through the authorized product flow where available. Balchemy only executes trades within the risk limits and strategy parameters you configure.
Recommended security practices:
- Review your Hub agent trade history regularly. Unexpected orders should be investigated immediately.
- Configure risk limits on each agent — max trade size, daily loss limit, and approval thresholds — to prevent runaway strategies.
- Enable step-up verification for sensitive operations such as withdrawals and API key rotation.
- Disconnect your wallet when using shared or public computers. Clearing the browser session also invalidates the JWT.
- Always verify you are on the official Balchemy domain (
https://balchemy.ai) before signing any wallet message. API calls usehttps://api.balchemy.ai, and public docs live athttps://balchemy.ai/docs. Balchemy will never request a login signature on a third-party site.
Multi-factor authentication
MFA is available on Balchemy accounts. When enabled, sensitive operations — changing trading strategies, rotating API keys, or modifying sensitive agent permissions — require a second verification step. Standard MFA sessions are valid for 30 minutes; setup-level operations (configuring MFA itself) have a shorter 5-minute validity window to reduce exposure.
You can enable and manage MFA through the authorized account/settings flow where available. If MFA is not enabled, authentication relies entirely on your wallet signature, which is already a strong cryptographic factor because your private key never leaves your wallet or hardware device.
Technical Architecture
This section covers the full technical implementation of Balchemy's security stack. All details are sourced from the live codebase at balchemy-backend/src/core/auth/ and balchemy-backend/src/common/guards/.
JWT architecture
Balchemy uses ES256 (ECDSA P-256) asymmetric JWTs as the primary signing algorithm. The JwtKeyringService loads PEM-encoded keys from environment at startup:
JWT_PRIVATE_KEY_PEM— signs tokens on issuanceJWT_PUBLIC_KEY_PEM— verifies tokens on every authenticated request
Each token carries a kid (key ID) header field. During key rotation, the keyring maintains both currentKid and previousKid and attempts verification against both keys, allowing graceful rotation without forcing all active sessions to re-authenticate. A HS256 symmetric fallback exists for environments still migrating (JWT_SECRET, JWT_SECRET_PREVIOUS), but ES256 is the production standard.
JWT payload shape:
interface JwtPayload {
userId: string; // MongoDB ObjectId serialized as string
walletAddress?: string; // Solana base58 or EVM checksummed address
role: UserRole; // 'user' | 'admin' | 'moderator' | 'developer' | 'guest'
iat: number; // Issued-at (Unix epoch seconds)
exp: number; // Expiry (Unix epoch seconds)
}The JwtAuthGuard (Passport strategy jwt) extracts the token from Authorization: Bearer, verifies the signature and expiry, then normalizes the user onto request.auth.user and request.auth.userId for all downstream handlers. When a token is expired, the guard returns a distinct TOKEN_EXPIRED error code so clients can distinguish expiry from invalid tokens and prompt re-authentication cleanly.
Hub agent JWT scopes
Hub Web3 agents receive scoped JWTs through the supported identity flows. Token lifetime is calibrated to the risk of each scope level — shorter lifetimes reduce the blast radius of any key compromise:
| Scope | Capabilities | TTL |
|---|---|---|
read | Query agents, balances, history, market data | 3600 s (1 hour) |
trade | Submit orders, approve pending trades | 300 s (5 minutes) |
manage | Rotate keys, update agent config, claim ownership | 60 s (1 minute) |
manage scope is only available through the claim path and requires a step-up token. Hub agents must request fresh tokens rather than refreshing — there is no token refresh flow for agent JWTs by design. Agents that need long-running trade execution should re-authenticate on each trade cycle.
Step-up endpoints:
POST /api/nest/agents/:agentId/control/mcp/step-upToken blacklist: 3-layer revocation
Every authenticated request is checked against a three-layer revocation system before the payload is processed:
- In-memory cache: O(1) lookup within the current process. Populated on revocation. Flushed on service restart.
- Redis: Distributed revocation store. Tokens are written with a TTL equal to their remaining validity window. Revocation propagates to all backend replicas within milliseconds.
- MongoDB: Persistent record of all revocations. Acts as the audit trail and source of truth when Redis is unavailable (fail-safe behavior).
Any single layer can block a token independently. This means revocation survives service restarts, Redis flushes, and horizontal scaling events — a token revoked in MongoDB will be blocked even if the in-memory and Redis caches are cold.
CSRF protection
Balchemy uses timing-safe nonce comparison for CSRF prevention. Nonces are generated with crypto.randomBytes(32) and embedded in the authentication message shown to the user's wallet. The nonce is consumed and deleted from the user record on first successful use, making replay attacks structurally impossible: a second request using the same signed message will fail nonce validation regardless of token validity.
Core API endpoints require the Authorization: Bearer header rather than cookie-based sessions, which eliminates classical CSRF vectors for the primary API surface. Retired embedded-chat compatibility endpoints use origin allowlisting as a secondary layer where still reachable.
Inter-service authentication
All backend-to-backend communication — including calls from the NestJS backend to the Rust trading engine over gRPC — uses HMAC signing with INTER_SERVICE_HMAC_KEY. The InternalRpcAuthGuard validates the HMAC envelope on every inbound RPC call before the payload reaches any handler.
The guard extracts and propagates OpenTelemetry trace context from the envelope, then strips all internal transport metadata before forwarding to DTO validators:
_interServiceAuth— HMAC signature and source service identity_interServiceMeta— correlation ID and routing metadata_interServiceTrace— OpenTelemetrytraceparent/tracestate
This design ensures internal service identity cannot be spoofed from external HTTP requests because the internal TCP transport is never exposed to the public network.
Rate limiting
Balchemy rate limiting is layered instead of one universal counter. Public HTTP ingress, MCP transport, MCP key/scope windows, research budgets, WebSocket events, provider quotas, and provider circuits each own separate limits and error codes.
Read-only paths can use bounded fallback behavior where safe. Privileged
trade and manage paths fail closed when required rate-limit, replay, policy,
or provider-safety state is unavailable. Provider limits and research-budget
blocks are reported as source-health, missing-evidence, or budget conditions
instead of being collapsed into a generic "no market data" result.
The RateLimitGuard and related MCP/research policies resolve product limits
from global server-level constants. Balchemy has no plan, tier, or enterprise
entitlement system.
MFA implementation
The MfaGuard protects routes decorated with @RequireMFA() (standard operations) or @RequireMFASetup() (setup operations). For users with MFA enabled, the guard compares lastMfaVerifiedAt on the user record against the current time. If the gap exceeds the validity window, the guard rejects with MFA_VERIFICATION_REQUIRED.
Validity windows:
- Standard operations (
@RequireMFA): configurable viaMFA_VALID_PERIODenv var, default 30 minutes - Setup operations (
@RequireMFASetup): hardcoded 5 minutes
MFA secrets are stored encrypted via MfaSecretCryptoService. The guard is a no-op for users who have not enabled MFA — enabling is always opt-in.
API keys
API keys are managed through ApiKeyService with these security properties:
- Format validation (
/^[A-Za-z0-9_-]+$/, minimum 20 characters) runs before any database lookup, preventing timing attacks on malformed inputs. - Keys are returned to the user exactly once — at generation time. Only a hash is stored server-side.
rotatePrimaryApiKeyatomically invalidates the previous key before issuing a new one.- Key permissions derive from the user's
rolefield, not from the key record itself, ensuring role changes propagate immediately to all active keys.