SDK Getting Started
Just want to run a trading agent? You don't need the SDK directly. Run
npx balchemy— the CLI handles everything (LLM setup, wallets, strategy, live TUI). The SDK is for builders/operators creating custom integrations.
The Balchemy Agent SDK (@balchemyai/agent-sdk) is a TypeScript package for builders operating custom Hub Web3 agent integrations on Balchemy. It handles ERC-8004 onboarding, MCP tool access, token lifecycle management, and real-time SSE event streaming — all in a single dependency.
Installation
npm install @balchemyai/agent-sdkThe SDK has zero production dependencies and runs in Node.js 18+ and browser environments.
Quick start (30 seconds)
Onboard an agent using a wallet signature, then call a tool:
import { BalchemyAgentSdk } from "@balchemyai/agent-sdk";
// 1. Initialize
const sdk = new BalchemyAgentSdk({
apiBaseUrl: "https://api.balchemy.ai/api",
});
// 2. Request a SIWE nonce
const { message } = await sdk.requestSiweNonce({
address: "YOUR_EVM_WALLET_ADDRESS",
chainId: 8453,
domain: "youragent.example.com",
uri: "https://youragent.example.com",
});
// 3. Sign the message with your wallet library
const signature = await wallet.signMessage(message);
// 4. Onboard — receive MCP endpoint and API key
const response = await sdk.onboardWithSiwe({
message,
signature,
agentId: "my-agent-v1",
scope: "trade",
});
// 5. Connect to MCP and call a tool
const mcp = sdk.connectMcp({
endpoint: response.mcp.endpoint,
apiKey: response.mcp.apiKey ?? "",
});
const portfolio = await mcp.agentPortfolio();
renderPortfolio(portfolio);SDK architecture
The SDK is organized into four layers:
| Layer | Class | Purpose |
|---|---|---|
| Top-level | BalchemyAgentSdk | Entry point — onboarding and MCP connection factory |
| HTTP client | HttpClient | Typed fetch wrapper with timeout and error normalization |
| Onboarding | AgentOnboardingClient | ERC-8004 SIWE and identity onboarding flows |
| MCP client | BalchemyMcpClient | JSON-RPC tool calls over HTTP + SSE |
Supporting utilities:
TokenStore— automatic token refresh before expirySseEventStream— async iterator over the MCP SSE event streamwithRetry— exponential backoff with jitter for transient failuresAgentSdkError— typed error class withcode,message, anddetails
Configuration
BalchemyAgentSdk accepts a single AgentSdkConfig object:
| Field | Type | Required | Description |
|---|---|---|---|
apiBaseUrl | string | Yes | Base URL including the /api segment. Must not have a trailing slash. |
timeoutMs | number | No | Request timeout in milliseconds. Default: 15000. |
fetchFn | typeof fetch | No | Custom fetch implementation. Useful for environments with non-standard globals. |
const sdk = new BalchemyAgentSdk({
apiBaseUrl: "https://api.balchemy.ai/api",
timeoutMs: 30_000,
});Use the API base URL for the environment where your approved runtime is running. Do not hardcode production credentials into source code or examples.
Authentication
The SDK supports two onboarding paths. Both return an OnboardingResponse with the same shape.
Path 1 — SIWE (wallet-based)
Use this when your agent controls a Solana or EVM wallet and can sign SIWE messages.
Step 1. Request a nonce and get the message to sign:
const { message, nonce } = await sdk.requestSiweNonce({
address: "0xYourWalletAddress",
chainId: 8453, // Base chain ID
domain: "youragent.com",
uri: "https://youragent.com",
statement: "Authorize Balchemy agent",
});Step 2. Sign the message off-chain using your wallet library (ethers, viem, etc.):
// ethers.js example
const signature = await signer.signMessage(message);Step 3. Submit the signed message to complete onboarding:
const response = await sdk.onboardWithSiwe({
message,
signature,
agentId: "my-agent-v1", // stable unique identifier for your agent
scope: "trade", // "read" | "trade"
});Path 2 — Identity (walletless)
Use this when your agent has an ES256 JWT from an external provider registered with Balchemy, such as GitHub Actions OIDC or a custom identity service.
const response = await sdk.onboardWithIdentity({
provider: "your-registered-provider-id",
identityToken: "eyJhbGci...", // ES256 JWT from your provider
agentId: "my-agent-v1",
chainId: 8453,
scope: "trade",
});Your identity provider must be pre-registered with Balchemy. The provider value is the registered provider ID, not the issuer URL.
Onboarding response
Both paths return OnboardingResponse:
type OnboardingResponse = {
bot: {
botId: string;
publicId: string;
name: string;
};
mcp: {
endpoint: string; // MCP endpoint URL
apiKey?: string; // Bearer token for MCP calls
keyPrefix?: string;
keyId?: string;
};
base: {
chainId: number;
custodialWallet?: {
address: string;
walletId?: string;
chainId?: number;
};
};
identityAccess?: {
token: string;
tokenType: "Bearer";
expiresIn: number;
expiresAt: string; // ISO-8601
kid: string;
issuer: string;
scope: "read" | "trade";
};
};The mcp.endpoint and mcp.apiKey are what you pass to sdk.connectMcp().
MCP connection
After onboarding, connect to the MCP gateway:
const mcp = sdk.connectMcp({
endpoint: response.mcp.endpoint,
apiKey: response.mcp.apiKey ?? "",
timeoutMs: 20_000, // optional, default 15s
retry: { maxAttempts: 3 }, // optional retry config
});connectMcp returns a BalchemyMcpClient. You can also import and use connectMcp directly:
import { connectMcp } from "@balchemyai/agent-sdk";
const mcp = connectMcp({
endpoint: "https://api.balchemy.ai/mcp/<publicId>",
apiKey: "<your-mcp-api-key>",
});Calling tools
Typed convenience methods
BalchemyMcpClient exposes typed methods for the 7 default agent tools, plus Solana and EVM trading convenience methods (106 total tools available with MCP_EXPOSE_GRANULAR_TOOLS=true):
// Natural language query through the AI pipeline
const reply = await mcp.askBot({ message: "What is the price of SOL?" });
// Direct NLP trade command (bypasses LLM)
await mcp.tradeCommand({ message: "Buy 0.1 SOL of token X" });
// High-level instruction executor
await mcp.agentExecute({
instruction: "Find a low-cap token on Base with good liquidity",
});
// Token research with delta tracking
await mcp.agentResearch({
query: "BRETT",
chain: "base",
includeX: true,
includeOnchain: true,
});
// Portfolio snapshot
await mcp.agentPortfolio();
// Runtime health check
await mcp.agentStatus();
// Config get/update
await mcp.agentConfig({ operation: "get" });
await mcp.agentConfig({
operation: "update_risk_policy",
policy: { maxLossPercent: 5 },
});EVM trading convenience methods
// Quote (read-only)
const quote = await mcp.evmQuote({
chainId: 8453,
sellToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
buyToken: "0x4200000000000000000000000000000000000006", // WETH on Base
sellAmount: "50000000", // 50 USDC (6 decimals)
slippageBps: 50,
});
// Swap — submit=false creates a pending order (safe default)
const swap = await mcp.evmSwap({
chainId: 8453,
sellToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
buyToken: "0x4200000000000000000000000000000000000006",
sellAmount: "50000000",
submit: true, // true = on-chain execution
});Solana trading convenience methods
// Quote (read-only) — requires MCP_EXPOSE_GRANULAR_TOOLS=true
const quote = await mcp.solanaQuote({
inputMint: "So11111111111111111111111111111111111111112", // SOL
outputMint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // BONK
amount: "100000000", // 0.1 SOL (9 decimals)
slippageBps: 100,
});
// Swap — submit=false creates a pending order (safe default)
const swap = await mcp.solanaSwap({
inputMint: "So11111111111111111111111111111111111111112",
outputMint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
amount: "100000000",
submit: true, // true = on-chain execution via Jupiter
});Note:
solanaQuote()andsolanaSwap()requireMCP_EXPOSE_GRANULAR_TOOLS=trueon the platform. Without granular tool exposure, usetradeCommand()oragentExecute()for Solana trading.
Generic tool call
For granular tools or tools without a typed method, use callTool directly:
const result = await mcp.callTool("trading_solana_jupiter_quote", {
inputMint: "So11111111111111111111111111111111111111112",
outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount: "1000000000", // 1 SOL
slippageBps: 100,
});List available tools
const { tools } = await mcp.listTools();
const toolNames = tools.map((tool) => tool.name);Working with tool responses
All tool calls return McpCallToolResponse:
type McpCallToolResponse = {
content: Array<{ type: "text"; text: string }>;
isError?: boolean;
};Use the helper functions to work with responses ergonomically:
import { getToolText, parseToolJson, isToolError } from "@balchemyai/agent-sdk";
const response = await mcp.agentPortfolio();
if (isToolError(response)) {
const message = getToolText(response);
showToolError(message);
} else {
const data = parseToolJson(response);
renderSnapshot(data?.structured?.snapshot);
}getToolText(response)— extracts the text content stringparseToolJson<T>(response)— parses text as JSON, returnsT | nullisToolError(response)— returnstrueifisError === true
Error handling
All SDK methods throw AgentSdkError on failure. Never catch a plain Error — always narrow to AgentSdkError first:
import { AgentSdkError } from "@balchemyai/agent-sdk";
import type { AgentSdkErrorCode } from "@balchemyai/agent-sdk";
try {
await mcp.agentExecute({ instruction: "..." });
} catch (err: unknown) {
if (err instanceof AgentSdkError) {
const code: AgentSdkErrorCode = err.code;
reportSdkError(code, err.message, err.details);
if (code === "rate_limit_error") {
const retryAfter = (err.details as Record<string, number>)?.["retry-after"];
await sleep(retryAfter * 1000);
}
}
}SDK error codes and their triggers:
| Code | Trigger |
|---|---|
auth_error | 401 from the API |
policy_error | 403 — scope or CSRF violation |
rate_limit_error | 429 — IP or AI quota exceeded |
provider_auth_error | Identity provider JWT rejected |
network_error | Timeout or network unreachable |
execution_error | Tool execution returned an error |
invalid_response | Non-JSON or empty response body |
unknown_error | Unclassified |
Automatic retry
The SDK retries transient failures (network_error, rate_limit_error) using exponential backoff with ±25% jitter. Tool execution errors (execution_error) are not retried — they are deterministic.
Configure retry behavior when connecting:
const mcp = sdk.connectMcp({
endpoint: response.mcp.endpoint,
apiKey: response.mcp.apiKey ?? "",
retry: {
maxAttempts: 5,
baseDelayMs: 300,
maxDelayMs: 10_000,
jitter: true,
},
});Use withRetry from the SDK directly for your own retry loops:
import { withRetry } from "@balchemyai/agent-sdk";
const result = await withRetry(
() => mcp.agentResearch({ query: "SOL" }),
{ maxAttempts: 3 }
);Token management
Use TokenStore to keep identity tokens fresh. When the stored token nears expiry, the store calls your refreshFn and caches the new token returned by the supported onboarding or provider-verification flow.
import { TokenStore } from "@balchemyai/agent-sdk";
const store = new TokenStore({
refreshFn: async () => {
return sdk.onboardWithIdentity({
provider: "my-provider",
identityToken: await getProviderToken(),
agentId: "my-agent-v1",
scope: "trade",
});
},
});
// Store the initial response
await store.set(response);
// Retrieve (auto-refreshes if expiry threshold is reached)
const token = await store.get();SSE event streaming
Subscribe to real-time events from the MCP SSE stream — trade results, order updates, and bot status changes:
import { SseEventStream } from "@balchemyai/agent-sdk";
import type { SseEvent } from "@balchemyai/agent-sdk";
const stream = new SseEventStream(
"https://api.balchemy.ai/mcp/<publicId>/events/sse",
response.mcp.apiKey ?? "",
{
reconnectDelayMs: 2000,
maxReconnects: 10,
}
);
// Async iterator (recommended)
for await (const event of stream) {
const e: SseEvent = event;
handleSseEvent(e.event, e.data);
}
// Or callback-based
const unsubscribe = stream.subscribe(
(event) => handleSseEvent(event.event, event.data),
(err) => reportStreamError(err)
);
// Clean up when done
unsubscribe();Agent lifecycle
A typical Hub Web3 agent integration follows this sequence:
- Onboard — call
onboardWithSiweoronboardWithIdentityonce. Store the MCP endpoint and API key securely. - Connect — call
sdk.connectMcp()with the stored credentials. - Check status — call
mcp.agentStatus()to verify auth and trading readiness. - Research — use
mcp.agentResearch()or granular market data tools to build context. - Execute — use
mcp.agentExecute()ormcp.tradeCommand()to act. - Monitor — use
mcp.agentPortfolio()ormcp.callTool("trading_positions", {})to track state. - Renew — use
TokenStoreto request fresh identity tokens through the supported onboarding or provider-verification flow.
TypeScript types
All public types are exported from the package root:
import type {
AgentSdkConfig,
AgentOnboardingMode, // "siwe" | "walletless" | "legacy"
AgentScope, // "read" | "trade" | "manage"
OnboardingResponse,
OnboardWithSiweInput,
OnboardWithIdentityInput,
RequestSiweNonceInput,
SiweNonceResponse,
IdentityAccess,
McpTool,
McpListToolsResponse,
McpCallToolResponse,
StoredToken,
TokenRefreshFn,
TokenStoreOptions,
SseEvent,
SseStreamOptions,
} from "@balchemyai/agent-sdk";
import type { AgentSdkErrorCode } from "@balchemyai/agent-sdk";Identity token revocation
Revoke an identity access token by its JTI (JWT Token ID) to invalidate it before natural expiry:
// Revoke
await sdk.revokeIdentityToken({
jti: "the-token-jti",
ttlSeconds: 86400, // optional: block for this many seconds after natural expiry
});
// Check revocation status
const { revoked } = await sdk.getIdentityTokenRevokeStatus({ jti: "the-token-jti" });Notes
agent_seed_requestis disabled on the platform. TherequestSeed()method exists for backward compatibility but always throws anAgentSdkErrorwith codeexecution_error.apiBaseUrlmust include the/apipath segment and must not have a trailing slash.- Tool granular access (
MCP_EXPOSE_GRANULAR_TOOLS) is a per-bot platform flag. Contact the Balchemy team to enable it for your integration. managescope is never issued at onboarding time. Start withreadortrade.