SDK Getting Started
The Balchemy Agent SDK (@balchemy/agent-sdk) is a TypeScript package for building and deploying external AI agents on the Balchemy platform. 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 @balchemy/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 "@balchemy/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();
console.log(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,
});For local development, set apiBaseUrl: "http://localhost:3000/api".
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 "@balchemy/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. The default 7 tools 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();
tools.forEach((t) => console.log(t.name, t.description));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 "@balchemy/agent-sdk";
const response = await mcp.agentPortfolio();
if (isToolError(response)) {
// Tool returned an error result
console.error("Tool error:", getToolText(response));
} else {
// Parse the JSON payload
const data = parseToolJson(response);
console.log(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 "@balchemy/agent-sdk";
import type { AgentSdkErrorCode } from "@balchemy/agent-sdk";
try {
await mcp.agentExecute({ instruction: "..." });
} catch (err: unknown) {
if (err instanceof AgentSdkError) {
const code: AgentSdkErrorCode = err.code;
console.error(`[${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 "@balchemy/agent-sdk";
const result = await withRetry(
() => mcp.agentResearch({ query: "SOL" }),
{ maxAttempts: 3 }
);Token management
Use TokenStore to handle identity token refresh automatically. When the stored token nears expiry (within a threshold), the store calls your refreshFn and caches the new token.
import { TokenStore } from "@balchemy/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 "@balchemy/agent-sdk";
import type { SseEvent } from "@balchemy/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;
console.log(e.event, e.data);
}
// Or callback-based
const unsubscribe = stream.subscribe(
(event) => console.log(event),
(err) => console.error("Stream error:", err)
);
// Clean up when done
unsubscribe();Agent lifecycle
A typical external agent 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. - Refresh — use
TokenStoreto keep identity tokens fresh without re-onboarding.
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 "@balchemy/agent-sdk";
import type { AgentSdkErrorCode } from "@balchemy/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.