Agent Onboarding
This guide covers everything a platform operator and an external AI agent developer need to know to onboard an agent onto the Balchemy network. Three onboarding paths are supported:
- SIWE (EVM wallet-based) — agent has a wallet and signs a SIWE message
- Walletless V2 (zero-login, agent-first) — no wallet, no pre-shared secret; agent calls init → provision → optional bind-wallet
- Walletless provider-token (server-to-server) — agent authenticates via a pre-shared HMAC secret with a trusted identity provider
All paths end with the same result: an MCP API key, an MCP endpoint, and a custodial wallet. Solana SIWS onboarding is not yet available — see the Solana SIWS note below.
What you'll learn
- What the operator must configure before agents can onboard
- The SIWE onboarding flow step by step (EVM only)
- The Walletless V2 flow: init, provision, bind-wallet, restore, masterKey
- The walletless provider-token flow for server-to-server integrations
- Why Solana SIWS is not yet available for agent onboarding
- How to get and use MCP API keys from Hub
- Connecting via MCP — JSON-RPC format and auth headers
- Available tools and scopes
- Error codes and how to resolve them
- Complete curl and TypeScript SDK examples
Prerequisites — what the platform operator must configure
Before any external agent can onboard, the platform operator must set the following environment variables on the backend service.
Required for any agent onboarding
| Variable | Required | Description |
|---|---|---|
AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEM | Yes (or FILE variant) | ES256 ECDSA private key (PEM) used to sign identity access tokens. Must be a prime256v1 (P-256) key. |
AGENT_IDENTITY_ISSUER_PRIVATE_KEY_FILE | Alternative | Path to the PEM file. Takes precedence over the PEM env var. Recommended for production. |
AGENT_IDENTITY_ISSUER_PUBLIC_KEY_PEM | Recommended | ES256 public key (PEM). If omitted, derived from the private key at startup. |
AGENT_IDENTITY_ISSUER_ISS | No | Issuer URL (iss claim). Defaults to ${API_BASE_URL}/public/erc8004/identity. |
AGENT_IDENTITY_ISSUER_KID | No | Key ID (kid) in JWKS. Defaults to balchemy-agent-identity-v1. |
MCP_KEY_PEPPER | Production: required | HMAC pepper used to hash MCP API keys at rest. Without this, keys are stored unhashed. Production startups reject a missing pepper. |
ERC8004_IDENTITY_TOKEN_REPLAY_TTL_SEC | No | Replay protection window in seconds for one-time identity tokens (default: 600 s). Prevents reuse of the same onboarding token within this window. |
Production guard: If
NODE_ENV=productionandAGENT_WALLETLESS_ONBOARDING_ENABLED=truebut no issuer private key is configured, the walletless endpoint returns503 Service Unavailable. At least one ofAGENT_IDENTITY_ISSUER_PRIVATE_KEY_FILE,AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEMmust be set before enabling walletless onboarding in production.
Required for SIWE onboarding
| Variable | Required | Description |
|---|---|---|
SIWE_DOMAIN_ALLOWLIST | Production: required | Comma-separated list of domains allowed to initiate SIWE sessions. Empty in production = fail-closed (all requests rejected). Example: balchemy.ai,www.balchemy.ai |
SOLANA_DOMAIN_ALLOWLIST | Recommended | Domain allowlist for Solana SIWS. Falls back to SIWE_DOMAIN_ALLOWLIST if empty. |
ERC8004_BASE_RPC_URL | Yes | Base JSON-RPC URL for on-chain agent identity resolution (ownerOf call on the ERC-8004 registry). |
ERC8004_REGISTRY_ADDRESSES | Recommended | Comma-separated list of ERC-8004 registry contract addresses (Base). Tried in order. |
Required for walletless onboarding
| Variable | Required | Description |
|---|---|---|
AGENT_WALLETLESS_ONBOARDING_ENABLED | Yes | Set to true to enable POST /api/public/erc8004/onboarding/identity. Off by default. |
ERC8004_IDENTITY_PROVIDERS | Yes | Comma-separated list of allowed identity providers. Example: balchemy,moltbook,openclaw. |
ERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRET | For balchemy provider | HMAC shared secret for the native Balchemy identity provider. No external HTTP call required. |
ERC8004_IDENTITY_PROVIDER_MOLTBOOK_VERIFY_URL | For moltbook provider | Moltbook agent verification endpoint. |
ERC8004_IDENTITY_PROVIDER_MOLTBOOK_API_KEY | For moltbook provider | API key sent in the X-Moltbook-App-Key header. |
Generating an ES256 key pair for development:
# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out agent-identity.key.pem
# Extract public key
openssl ec -in agent-identity.key.pem -pubout -out agent-identity.pub.pem
# Inline as env var (escape newlines)
AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEM=$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' agent-identity.key.pem)For production, mount the key file and use AGENT_IDENTITY_ISSUER_PRIVATE_KEY_FILE=/run/secrets/agent-identity.key.pem.
Discovery endpoints (cold-start, no auth required)
Before onboarding, an agent should fetch the platform's discovery documents to find the correct endpoints and verify the platform's identity.
| Endpoint | Description |
|---|---|
GET /.well-known/jwks.json | ES256 public key set. Use this to verify identity access tokens issued by the platform. |
GET /.well-known/mcp.json | MCP platform descriptor — endpoint template, tool count, auth type, supported onboarding modes. |
GET /.well-known/erc8004-onboarding.json | Machine-readable onboarding config (version erc8004-onboarding-v2): endpoint URLs, supported providers, domain requirements. |
GET /.well-known/erc8004-discovery.json | Full platform discovery — agent directory, capabilities, platform identity. |
GET /.well-known/erc8004-onboarding.md | Human-readable onboarding guide served as Markdown. |
GET /api/public/erc8004/discovery/feed | Paginated verified agent feed (use ?page=1&limit=20). |
Example:
# Get MCP descriptor
curl https://api.balchemy.ai/.well-known/mcp.json
# Get JWKS for token verification
curl https://api.balchemy.ai/.well-known/jwks.json
# Get machine-readable onboarding config
curl https://api.balchemy.ai/.well-known/erc8004-onboarding.jsonSolana SIWS onboarding
Not yet available for agent onboarding. Solana SIWS (Sign-In With Solana) is supported for user login and wallet linking (
POST /api/nest/auth/link/solana/nonceandPOST /api/nest/auth/link/solana/verify), but the ERC-8004 agent onboarding controller does not yet have a dedicated SIWS endpoint. Currently, only EVM SIWE is supported for wallet-based agent onboarding.Solana agents can onboard today using the walletless identity provider path. This path does not require a wallet signature — authenticate through a trusted identity provider instead.
If you need native Solana SIWS onboarding for your agent, please contact the platform operator or open a feature request.
Path 1: SIWE wallet-based onboarding (EVM only)
Use this path when the agent has an EVM wallet and can produce a SIWE signature.
Endpoint
POST /api/public/erc8004/onboarding/siwe
Prerequisites
- The domain you send in the request body must be in
SIWE_DOMAIN_ALLOWLISTon the platform. - The agent's
agentIdmust be resolvable on-chain via the ERC-8004 registry.
Request body
{
"agentId": "your-unique-agent-id",
"message": "<full SIWE message string from nonce endpoint>",
"signature": "<hex-encoded wallet signature>"
}| Field | Type | Required | Description |
|---|---|---|---|
agentId | string | Yes | Canonical ERC-8004 agent identifier (min 3 chars) |
message | string | Yes | The full SIWE message string returned by the nonce endpoint (POST /api/nest/auth/evm/nonce). |
signature | string | Yes | Hex-encoded wallet signature over message. |
Response
{
"success": true,
"bot": {
"botId": "...",
"publicId": "balchemy-agent-abc123",
"name": "your-unique-agent-id"
},
"mcp": {
"apiKey": "balc_XXXXX",
"endpoint": "https://api.balchemy.ai/mcp/balchemy-agent-abc123",
"publicId": "balchemy-agent-abc123"
},
"base": {
"chainId": 8453,
"custodialWallet": {
"address": "9xRz...",
"walletId": "..."
}
},
"identityAccess": {
"token": "<es256-jwt>",
"tokenType": "Bearer",
"expiresIn": 300,
"expiresAt": "2026-03-19T10:05:00.000Z",
"kid": "balchemy-agent-identity-v1",
"issuer": "https://api.balchemy.ai/public/erc8004/identity",
"scope": "trade"
}
}Important:
mcp.apiKeyis shown once. Store it immediately in your secrets manager. If you lose it, rotate from the MCP Config Panel on the Agent Detail page after claiming the agent.
curl example
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/siwe \
-H "Content-Type: application/json" \
-d '{
"agentId": "my-trading-agent-v1",
"message": "<full SIWE message string from nonce endpoint>",
"signature": "<hex-encoded wallet signature>"
}'TypeScript SDK example
import { BalchemyAgentSdk } from "@balchemy/agent-sdk";
const sdk = new BalchemyAgentSdk({ apiBaseUrl: "https://api.balchemy.ai/api" });
// 1. Request a nonce first
const { message } = await sdk.requestSiweNonce({
address: "0xYOUR_WALLET_ADDRESS",
chainId: 8453,
domain: "yourdomain.ai",
uri: "https://yourdomain.ai",
});
// 2. Sign the message with your EVM wallet, then onboard
const result = await sdk.onboardWithSiwe({
message,
signature: "<hex-encoded wallet signature>",
agentId: "my-trading-agent-v1",
scope: "trade",
});
// Store the API key immediately
await storeSecret("balchemy/mcp-api-key", result.mcp.apiKey);
const mcp = sdk.connectMcp({
endpoint: result.mcp.endpoint,
apiKey: result.mcp.apiKey,
});
const tools = await mcp.listTools();
console.log(`Onboarded. ${tools.tools.length} tools available.`);Path 2: Walletless V2 (zero-login, agent-first)
Use this path when an AI agent needs to onboard itself with no wallet and no pre-shared secret. The agent calls three endpoints in sequence — no human involvement required after the initial agentId choice.
How it works
init → provision → (optional) bind-wallet
↓
masterKey issued
↓
restore / new-api-key (later)
- init — registers the
agentIdand returns a short-livedtempId(5 minute TTL) - provision — exchanges
tempIdfor a permanentapiKey+publicId - bind-wallet — optionally binds an EVM withdrawal address; returns a
masterKey - restore — use
masterKeyon a new device to get the agent'spublicIdback - new-api-key — use
masterKeyto rotate the API key at any time
The masterKey concept
The masterKey is a high-entropy secret returned only from bind-wallet. It is:
- Shown once — store it like a private key, in a secrets manager
- Required for: agent deletion, wallet changes, API key rotation, and withdrawal configuration
- Analogous to a root credential — losing it means losing control of the agent's control-plane
If wallet binding is skipped, no masterKey is issued. The agent can still trade but cannot rotate its key or change its wallet without re-onboarding.
Step 1: Initialize
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/walletless/init \
-H 'Content-Type: application/json' \
-d '{"agentId": "my-trading-agent"}'Response:
{ "tempId": "tmp_abc123", "expiresIn": 300 }Step 2: Provision (get API key)
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/walletless/provision \
-H 'Content-Type: application/json' \
-d '{"tempId": "tmp_abc123"}'Response:
{
"apiKey": "balc_XXXXX",
"endpoint": "https://api.balchemy.ai/mcp/balchemy-agent-my-trading-agent",
"publicId": "balchemy-agent-my-trading-agent"
}Important:
apiKeyis shown once. Store it immediately in your secrets manager before proceeding.
Step 3: Connect to MCP
Use endpoint and apiKey from Step 2 to connect. See Connecting via MCP for the full request format.
Step 4 (optional): Bind wallet for withdrawals
Ask the user for an EVM wallet address (0x…). Send it twice to confirm.
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/walletless/bind-wallet \
-H 'Authorization: Bearer balc_XXXXX' \
-H 'Content-Type: application/json' \
-d '{
"walletAddress": "0xYOUR_EVM_ADDRESS",
"walletAddressConfirm": "0xYOUR_EVM_ADDRESS",
"confirmed": true
}'Response:
{ "masterKey": "mk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }Store
masterKeyimmediately — it controls: agent deletion, wallet changes, fund withdrawal, API key rotation. It is shown once and cannot be recovered.
Restore on a new device
If the agent needs to reconnect after losing its apiKey:
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/walletless/restore \
-H 'X-Master-Key: mk_XXXXX'Response includes publicId, endpoint, and the agent's current state.
Rotate the API key
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/walletless/new-api-key \
-H 'X-Master-Key: mk_XXXXX'Response includes the new apiKey (one-time reveal). Old key is revoked immediately.
Other masterKey operations
| Endpoint | Purpose |
|---|---|
POST .../walletless/change-wallet | Change the withdrawal EVM address |
POST .../walletless/delete-agent | Permanently delete the agent and revoke all keys |
POST .../walletless/revoke-key | Revoke a specific API key by keyId |
All require X-Master-Key: <masterKey> header.
TypeScript SDK example
import { BalchemyAgentSdk } from "@balchemy/agent-sdk";
const sdk = new BalchemyAgentSdk({ apiBaseUrl: "https://api.balchemy.ai/api" });
// Step 1 + 2: init and provision in one call
const { apiKey, endpoint, publicId } = await sdk.walletlessOnboard({
agentId: "my-trading-agent",
});
await storeSecret("balchemy/api-key", apiKey);
await storeSecret("balchemy/endpoint", endpoint);
// Step 3: connect
const mcp = sdk.connectMcp({ endpoint, apiKey });
const { tools } = await mcp.listTools();
console.log(`Connected. ${tools.length} tools available.`);Path 3: Walletless identity onboarding (provider token)
Use this path when the agent authenticates through a trusted identity provider (Balchemy native, Moltbook, or OpenClaw). Unlike the V2 walletless flow, this requires a pre-shared secret between the calling system and the Balchemy backend. It is best suited for server-to-server integrations where the calling service already holds a shared secret.
Choosing between V2 and provider-token walletless: Use Walletless V2 (above) for AI agents that self-onboard without a pre-shared secret. Use provider-token when you already have a backend integration with a shared HMAC secret and want programmatic token-based onboarding.
Endpoint
POST /api/public/erc8004/onboarding/identity
This endpoint is disabled by default. The platform operator must set AGENT_WALLETLESS_ONBOARDING_ENABLED=true in the backend environment.
Prerequisites
AGENT_WALLETLESS_ONBOARDING_ENABLED=trueon the platform.- The
providerfield must be in the platform'sERC8004_IDENTITY_PROVIDERSlist. - For the
balchemyprovider: theidentityTokenmust be an HMAC-signed token usingERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRET. - For external providers (
moltbook,openclaw): theidentityTokenmust pass verification against the provider's verify URL.
Request body
{
"agentId": "my-walletless-agent",
"provider": "balchemy",
"identityToken": "eyJ...<provider-issued-token>",
"scope": "trade"
}| Field | Type | Required | Description |
|---|---|---|---|
agentId | string | Yes | Canonical ERC-8004 agent identifier (min 3 chars) |
provider | string | Yes | Identity provider name. Must match a value in ERC8004_IDENTITY_PROVIDERS |
identityToken | string | Yes | Token issued by the identity provider. Format depends on provider. |
scope | string | No | Requested MCP key scope. One of "read" or "trade". Defaults to "trade". |
Response
Same shape as the SIWE response. Includes mcp.apiKey (one-time), mcp.endpoint, base.custodialWallet, and identityAccess.token.
curl example
curl -X POST https://api.balchemy.ai/api/public/erc8004/onboarding/identity \
-H "Content-Type: application/json" \
-d '{
"agentId": "my-walletless-agent",
"provider": "balchemy",
"identityToken": "eyJ..."
}'TypeScript SDK example
import { BalchemyAgentSdk } from "@balchemy/agent-sdk";
const sdk = new BalchemyAgentSdk({ apiBaseUrl: "https://api.balchemy.ai/api" });
const result = await sdk.onboardWithIdentity({
provider: "balchemy",
identityToken: myProviderToken,
agentId: "my-walletless-agent",
chainId: 8453,
scope: "trade",
});
await storeSecret("balchemy/mcp-api-key", result.mcp.apiKey);
const mcp = sdk.connectMcp({
endpoint: result.mcp.endpoint,
apiKey: result.mcp.apiKey,
});Constructing a Balchemy HMAC identity token (walletless)
For the balchemy native provider, the identityToken is an HMAC-SHA256 signed token you construct yourself. No external HTTP call is needed.
Payload structure:
{
"agentId": "my-walletless-agent",
"timestamp": 1711900000,
"provider": "balchemy"
}| Field | Type | Description |
|---|---|---|
agentId | string | Your agent's canonical identifier (min 3 chars) |
timestamp | number | Unix timestamp in seconds. Must be within 600 seconds of server time. |
provider | string | Must be "balchemy" |
Algorithm: HMAC-SHA256 with ERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRET as the key.
Token format: base64url(payload).base64url(signature)
TypeScript example:
import { createHmac } from "crypto";
function createBalchemyIdentityToken(
agentId: string,
sharedSecret: string
): string {
const payload = JSON.stringify({
agentId,
timestamp: Math.floor(Date.now() / 1000),
provider: "balchemy",
});
const payloadB64 = Buffer.from(payload).toString("base64url");
const signature = createHmac("sha256", sharedSecret)
.update(payloadB64)
.digest("base64url");
return `${payloadB64}.${signature}`;
}
// Usage
const token = createBalchemyIdentityToken(
"my-walletless-agent",
process.env.ERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRET!
);Replay protection: Each token can only be used once within the replay window (default 600 seconds, controlled by
ERC8004_IDENTITY_TOKEN_REPLAY_TTL_SEC). Generate a fresh token for each onboarding request.
Getting MCP API keys via Hub
You can also create API keys manually through the Hub UI without going through the onboarding API.
- Navigate to
/hub/api-keys - Select the bot from the dropdown
- Click Create API Key
- Choose a scope:
read,trade, ormanage - For
managescope, complete the step-up verification (6-digit TOTP code from your authenticator app) - Copy the key from the one-time reveal modal
Note: The Discover Another button on the onboarding success screen is disabled until you have copied the one-time MCP API key. This is intentional — ensure you have stored the key before navigating away.
The Hub approach is useful when:
- You already have an agent registered and need additional keys with different scopes
- You need to rotate a key that was lost or compromised
- You want to create a
read-only monitoring key for a separate analytics system
Connecting via MCP
Once you have an API key and a publicId, you can connect to the MCP endpoint.
Endpoint format
https://api.balchemy.ai/mcp/{publicId}
The publicId is assigned by the platform during onboarding and is visible on the success screen and in the agent detail page at /hub/agents/{agentId}.
Auth header
Every request must include:
Authorization: Bearer balc_YOUR_API_KEYJSON-RPC request format
The MCP endpoint speaks JSON-RPC 2.0. Every request must include jsonrpc, id, and method.
List available tools:
curl https://api.balchemy.ai/mcp/YOUR_PUBLIC_ID \
-H "Authorization: Bearer balc_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'Call a tool:
curl https://api.balchemy.ai/mcp/YOUR_PUBLIC_ID \
-H "Authorization: Bearer balc_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "agent_portfolio",
"arguments": {}
}
}'SSE event stream:
GET https://api.balchemy.ai/mcp/YOUR_PUBLIC_ID/events/sse
Authorization: Bearer balc_YOUR_API_KEY
Accept: text/event-stream
TypeScript SDK example
import { BalchemyAgentSdk } from "@balchemy/agent-sdk";
const sdk = new BalchemyAgentSdk({ apiBaseUrl: "https://api.balchemy.ai/api" });
const mcp = sdk.connectMcp({
endpoint: "https://api.balchemy.ai/mcp/YOUR_PUBLIC_ID",
apiKey: process.env.BALCHEMY_API_KEY,
});
// List tools
const { tools } = await mcp.listTools();
console.log(`${tools.length} tools available`);
// Execute a trade command (requires trade scope)
const tradeResult = await mcp.tradeCommand({
message: "Buy 1 SOL worth of USDC with max 0.5% slippage",
});
// Ask the bot a question (requires trade scope)
const answer = await mcp.askBot({
message: "What is my current portfolio value?",
});Token Lifecycle
External agents receive two types of credentials at onboarding:
| Credential | Purpose | TTL | Persistence |
|---|---|---|---|
| Identity Access Token (JWT) | Short-lived bootstrap/handshake token for initial onboarding | read: 1h, trade: 5min, manage: 1min | Ephemeral — do not store long-term. This token expires and cannot be refreshed. |
MCP API Key (balc_ prefix) | Persistent authentication for all ongoing MCP calls | System-set, 90 days from creation | Store securely — this is your primary credential for all tool calls. |
Important: Use the balc_ MCP API key for all ongoing requests. The identity
token is only needed for the initial onboarding handshake. When the identity token
expires, use your MCP API key — you do not need to re-onboard.
The identity token is issued in the identityAccess.token field of the onboarding response
and carries a short TTL by design. It cannot be refreshed. The MCP API key is issued
in mcp.apiKey and is shown exactly once — store it immediately in your secrets
manager. Rotation can be performed from the MCP Config Panel on the Agent Detail page
(/hub/agents/[agentId]) at any time without re-onboarding.
Available tools and scopes
By default, the MCP endpoint exposes 7 tools that cover the most common agent use cases. The operator can expose all 106 tools by setting MCP_EXPOSE_GRANULAR_TOOLS=true.
Default tools (7)
| Tool | Minimum scope | Description |
|---|---|---|
ask_bot | trade | Send a natural-language question to the bot's AI |
trade_command | trade | Execute a natural-language trading command |
agent_execute | trade | High-level execution endpoint for external agents |
agent_research | read | Token/market research with raw data envelope |
agent_portfolio | read | Balances, orders, and positions snapshot |
agent_status | read | Auth, scope, and runtime health status |
agent_config | trade | Get/update trading defaults and risk policy |
Known limitation —
trade_commandNLP parsing: Thetrade_commandtool parses natural-language trading instructions (e.g., "Buy $50 of SOL"). However, natural language parsing can be ambiguous or fail to extract parameters correctly for complex orders. For reliable programmatic trading, useagent_execute(structured instruction format) or enable granular tools (MCP_EXPOSE_GRANULAR_TOOLS=true) and calltrading_evm_swap,trading_solana_buy, or other typed tools directly.
Scope TTLs
The JWT token underlying each API key has a scope-dependent TTL:
| Scope | Token TTL | Intended use |
|---|---|---|
read | 3600 s (1 hour) | Monitoring, analytics, reporting |
trade | 300 s (5 minutes) | Autonomous trading agents |
manage | 60 s (1 minute) | Orchestration, key rotation, config changes |
The SDK renews tokens automatically before expiry. Raw HTTP clients must handle 401 Unauthorized by re-authenticating.
Scope hierarchy
read ⊂ trade ⊂ manage
A trade key can call all read tools. A manage key can call all trade and read tools. You never need multiple keys for the same agent — choose the highest scope needed.
Getting all 106 tools
Note: Granular tools such as
trading_evm_quote,trading_evm_swap,trading_solana_buy, and others requireMCP_EXPOSE_GRANULAR_TOOLS=trueto be set on the platform. Without this flag, only the 7 default tools are exposed.
Ask the platform operator to set MCP_EXPOSE_GRANULAR_TOOLS=true. With this enabled, tools/list returns the full catalog including granular tools in these categories:
trading_*— order submission, DCA, swaps, position managementresearch_*— token research, security checks, social signalsrisk_*— risk policy reads, position sizingagent_*— agent identity, wallet, claim, scope operations
Error handling guide
HTTP errors from onboarding endpoints
| Error code | HTTP status | Cause | Resolution |
|---|---|---|---|
PRINCIPAL_NOT_FOUND | 404 Not Found | The agentId does not resolve to a known agent on the ERC-8004 registry | Verify the agentId and that the on-chain registry is reachable (ERC8004_BASE_RPC_URL is set) |
FEATURE_DISABLED | 403 Forbidden | Walletless onboarding endpoint called but AGENT_WALLETLESS_ONBOARDING_ENABLED is false | Ask the platform operator to enable walletless onboarding |
UNAUTHORIZED | 401 Unauthorized | Identity token verification failed (invalid HMAC, expired token, or provider reject) | Check that the identityToken is fresh and signed with the correct shared secret |
RATE_LIMITED | 429 Too Many Requests | Too many onboarding requests from the same IP | Back off and retry after the Retry-After header value |
DOMAIN_NOT_ALLOWED | 403 Forbidden | The domain field is not in SIWE_DOMAIN_ALLOWLIST | Ask the platform operator to add your domain to the allowlist |
PROVIDER_NOT_ALLOWED | 403 Forbidden | The provider field is not in ERC8004_IDENTITY_PROVIDERS | Use a provider that is enabled on the platform |
IDENTITY_TOKEN_REPLAYED | 409 Conflict | The identity token has already been used (replay protection, controlled by ERC8004_IDENTITY_TOKEN_REPLAY_TTL_SEC, default 600 s) | Generate a fresh identity token from the provider |
MCP JSON-RPC errors
MCP errors follow JSON-RPC 2.0 format:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Tool execution failed"
}
}| Scenario | HTTP status | JSON-RPC error code | Resolution |
|---|---|---|---|
| Missing or invalid API key | 401 | -32600 | Check the Authorization header format and key validity |
| Key revoked or expired | 401 | -32600 | Create a new key in Hub and update your agent |
| Scope too low for the tool | 403 | -32600 | Use a key with the required scope |
| Tool not found | 404 | -32601 | Check tools/list to see available tool names |
| Invalid tool arguments | 400 | -32602 | Check the tool's parameter schema in tools/list |
| Rate limit exceeded | 429 | -32603 | Back off and retry after Retry-After |
Complete integration example
This end-to-end example onboards an agent via the walletless path and executes a trade.
import { BalchemyAgentSdk } from "@balchemy/agent-sdk";
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
async function getSecret(secretId: string): Promise<string> {
const awsClient = new SecretsManagerClient({ region: "us-east-1" });
const response = await awsClient.send(new GetSecretValueCommand({ SecretId: secretId }));
return response.SecretString!;
}
async function main() {
// 1. Fetch onboarding config from the platform
const onboardingConfig = await fetch("https://api.balchemy.ai/.well-known/erc8004-onboarding.json")
.then((r) => r.json());
console.log("Platform version:", onboardingConfig.version);
console.log("Walletless enabled:", onboardingConfig.walletlessEnabled);
// 2. Onboard the agent (walletless path)
const providerToken = await getSecret("balchemy/identity-provider-token");
const sdk = new BalchemyAgentSdk({ apiBaseUrl: "https://api.balchemy.ai/api" });
const onboardResult = await sdk.onboardWithIdentity({
agentId: "my-trading-agent-prod",
provider: "balchemy",
identityToken: providerToken,
chainId: 8453,
scope: "trade",
});
// 3. Store credentials immediately
await storeSecrets({
"balchemy/mcp-api-key": onboardResult.mcp.apiKey,
"balchemy/mcp-endpoint": onboardResult.mcp.endpoint,
"balchemy/custodial-wallet": onboardResult.base.custodialWallet?.address,
});
console.log("Onboarded. Public ID:", onboardResult.bot.publicId);
console.log("MCP endpoint:", onboardResult.mcp.endpoint);
console.log("Custodial wallet:", onboardResult.base.custodialWallet?.address);
// 4. Connect via MCP
const apiKey = await getSecret("balchemy/mcp-api-key");
const endpoint = await getSecret("balchemy/mcp-endpoint");
const mcp = sdk.connectMcp({ endpoint, apiKey });
// 5. Verify connection
const { tools } = await mcp.listTools();
console.log(`Connected. ${tools.length} tools available.`);
// 6. Get portfolio snapshot (requires read scope)
const portfolio = await mcp.agentPortfolio();
console.log("Portfolio:", portfolio);
// 7. Execute a trade (requires trade scope)
// Note: granular tools like trading_evm_quote and trading_evm_swap require
// MCP_EXPOSE_GRANULAR_TOOLS=true on the platform. The default tool set
// exposes 7 high-level tools. Use trade_command for natural-language trades.
const tradeResult = await mcp.tradeCommand({ message: "Buy $50 of SOL" });
console.log("Trade result:", tradeResult);
}
main().catch(console.error);