Skip to content
Skip to content

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

VariableRequiredDescription
AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEMYes (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_FILEAlternativePath to the PEM file. Takes precedence over the PEM env var. Recommended for production.
AGENT_IDENTITY_ISSUER_PUBLIC_KEY_PEMRecommendedES256 public key (PEM). If omitted, derived from the private key at startup.
AGENT_IDENTITY_ISSUER_ISSNoIssuer URL (iss claim). Defaults to ${API_BASE_URL}/public/erc8004/identity.
AGENT_IDENTITY_ISSUER_KIDNoKey ID (kid) in JWKS. Defaults to balchemy-agent-identity-v1.
MCP_KEY_PEPPERProduction: requiredHMAC 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_SECNoReplay 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=production and AGENT_WALLETLESS_ONBOARDING_ENABLED=true but no issuer private key is configured, the walletless endpoint returns 503 Service Unavailable. At least one of AGENT_IDENTITY_ISSUER_PRIVATE_KEY_FILE, AGENT_IDENTITY_ISSUER_PRIVATE_KEY_PEM must be set before enabling walletless onboarding in production.

Required for SIWE onboarding

VariableRequiredDescription
SIWE_DOMAIN_ALLOWLISTProduction: requiredComma-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_ALLOWLISTRecommendedDomain allowlist for Solana SIWS. Falls back to SIWE_DOMAIN_ALLOWLIST if empty.
ERC8004_BASE_RPC_URLYesBase JSON-RPC URL for on-chain agent identity resolution (ownerOf call on the ERC-8004 registry).
ERC8004_REGISTRY_ADDRESSESRecommendedComma-separated list of ERC-8004 registry contract addresses (Base). Tried in order.

Required for walletless onboarding

VariableRequiredDescription
AGENT_WALLETLESS_ONBOARDING_ENABLEDYesSet to true to enable POST /api/public/erc8004/onboarding/identity. Off by default.
ERC8004_IDENTITY_PROVIDERSYesComma-separated list of allowed identity providers. Example: balchemy,moltbook,openclaw.
ERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRETFor balchemy providerHMAC shared secret for the native Balchemy identity provider. No external HTTP call required.
ERC8004_IDENTITY_PROVIDER_MOLTBOOK_VERIFY_URLFor moltbook providerMoltbook agent verification endpoint.
ERC8004_IDENTITY_PROVIDER_MOLTBOOK_API_KEYFor moltbook providerAPI 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.

EndpointDescription
GET /.well-known/jwks.jsonES256 public key set. Use this to verify identity access tokens issued by the platform.
GET /.well-known/mcp.jsonMCP platform descriptor — endpoint template, tool count, auth type, supported onboarding modes.
GET /.well-known/erc8004-onboarding.jsonMachine-readable onboarding config (version erc8004-onboarding-v2): endpoint URLs, supported providers, domain requirements.
GET /.well-known/erc8004-discovery.jsonFull platform discovery — agent directory, capabilities, platform identity.
GET /.well-known/erc8004-onboarding.mdHuman-readable onboarding guide served as Markdown.
GET /api/public/erc8004/discovery/feedPaginated 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.json

Solana 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/nonce and POST /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_ALLOWLIST on the platform.
  • The agent's agentId must 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>"
}
FieldTypeRequiredDescription
agentIdstringYesCanonical ERC-8004 agent identifier (min 3 chars)
messagestringYesThe full SIWE message string returned by the nonce endpoint (POST /api/nest/auth/evm/nonce).
signaturestringYesHex-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.apiKey is 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 agentId and returns a short-lived tempId (5 minute TTL)
  • provision — exchanges tempId for a permanent apiKey + publicId
  • bind-wallet — optionally binds an EVM withdrawal address; returns a masterKey
  • restore — use masterKey on a new device to get the agent's publicId back
  • new-api-key — use masterKey to 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: apiKey is 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 masterKey immediately — 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

EndpointPurpose
POST .../walletless/change-walletChange the withdrawal EVM address
POST .../walletless/delete-agentPermanently delete the agent and revoke all keys
POST .../walletless/revoke-keyRevoke 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=true on the platform.
  • The provider field must be in the platform's ERC8004_IDENTITY_PROVIDERS list.
  • For the balchemy provider: the identityToken must be an HMAC-signed token using ERC8004_IDENTITY_PROVIDER_BALCHEMY_SHARED_SECRET.
  • For external providers (moltbook, openclaw): the identityToken must pass verification against the provider's verify URL.

Request body

{
  "agentId": "my-walletless-agent",
  "provider": "balchemy",
  "identityToken": "eyJ...<provider-issued-token>",
  "scope": "trade"
}
FieldTypeRequiredDescription
agentIdstringYesCanonical ERC-8004 agent identifier (min 3 chars)
providerstringYesIdentity provider name. Must match a value in ERC8004_IDENTITY_PROVIDERS
identityTokenstringYesToken issued by the identity provider. Format depends on provider.
scopestringNoRequested 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"
}
FieldTypeDescription
agentIdstringYour agent's canonical identifier (min 3 chars)
timestampnumberUnix timestamp in seconds. Must be within 600 seconds of server time.
providerstringMust 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.

  1. Navigate to /hub/api-keys
  2. Select the bot from the dropdown
  3. Click Create API Key
  4. Choose a scope: read, trade, or manage
  5. For manage scope, complete the step-up verification (6-digit TOTP code from your authenticator app)
  6. 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_KEY

JSON-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:

CredentialPurposeTTLPersistence
Identity Access Token (JWT)Short-lived bootstrap/handshake token for initial onboardingread: 1h, trade: 5min, manage: 1minEphemeral — do not store long-term. This token expires and cannot be refreshed.
MCP API Key (balc_ prefix)Persistent authentication for all ongoing MCP callsSystem-set, 90 days from creationStore 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)

ToolMinimum scopeDescription
ask_bottradeSend a natural-language question to the bot's AI
trade_commandtradeExecute a natural-language trading command
agent_executetradeHigh-level execution endpoint for external agents
agent_researchreadToken/market research with raw data envelope
agent_portfolioreadBalances, orders, and positions snapshot
agent_statusreadAuth, scope, and runtime health status
agent_configtradeGet/update trading defaults and risk policy

Known limitation — trade_command NLP parsing: The trade_command tool 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, use agent_execute (structured instruction format) or enable granular tools (MCP_EXPOSE_GRANULAR_TOOLS=true) and call trading_evm_swap, trading_solana_buy, or other typed tools directly.

Scope TTLs

The JWT token underlying each API key has a scope-dependent TTL:

ScopeToken TTLIntended use
read3600 s (1 hour)Monitoring, analytics, reporting
trade300 s (5 minutes)Autonomous trading agents
manage60 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 require MCP_EXPOSE_GRANULAR_TOOLS=true to 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 management
  • research_* — token research, security checks, social signals
  • risk_* — risk policy reads, position sizing
  • agent_* — agent identity, wallet, claim, scope operations

Error handling guide

HTTP errors from onboarding endpoints

Error codeHTTP statusCauseResolution
PRINCIPAL_NOT_FOUND404 Not FoundThe agentId does not resolve to a known agent on the ERC-8004 registryVerify the agentId and that the on-chain registry is reachable (ERC8004_BASE_RPC_URL is set)
FEATURE_DISABLED403 ForbiddenWalletless onboarding endpoint called but AGENT_WALLETLESS_ONBOARDING_ENABLED is falseAsk the platform operator to enable walletless onboarding
UNAUTHORIZED401 UnauthorizedIdentity token verification failed (invalid HMAC, expired token, or provider reject)Check that the identityToken is fresh and signed with the correct shared secret
RATE_LIMITED429 Too Many RequestsToo many onboarding requests from the same IPBack off and retry after the Retry-After header value
DOMAIN_NOT_ALLOWED403 ForbiddenThe domain field is not in SIWE_DOMAIN_ALLOWLISTAsk the platform operator to add your domain to the allowlist
PROVIDER_NOT_ALLOWED403 ForbiddenThe provider field is not in ERC8004_IDENTITY_PROVIDERSUse a provider that is enabled on the platform
IDENTITY_TOKEN_REPLAYED409 ConflictThe 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"
  }
}
ScenarioHTTP statusJSON-RPC error codeResolution
Missing or invalid API key401-32600Check the Authorization header format and key validity
Key revoked or expired401-32600Create a new key in Hub and update your agent
Scope too low for the tool403-32600Use a key with the required scope
Tool not found404-32601Check tools/list to see available tool names
Invalid tool arguments400-32602Check the tool's parameter schema in tools/list
Rate limit exceeded429-32603Back 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);

Connection lost. Retrying...