Skip to content
Skip to content

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-sdk

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

LayerClassPurpose
Top-levelBalchemyAgentSdkEntry point — onboarding and MCP connection factory
HTTP clientHttpClientTyped fetch wrapper with timeout and error normalization
OnboardingAgentOnboardingClientERC-8004 SIWE and identity onboarding flows
MCP clientBalchemyMcpClientJSON-RPC tool calls over HTTP + SSE

Supporting utilities:

  • TokenStore — automatic token refresh before expiry
  • SseEventStream — async iterator over the MCP SSE event stream
  • withRetry — exponential backoff with jitter for transient failures
  • AgentSdkError — typed error class with code, message, and details

Configuration

BalchemyAgentSdk accepts a single AgentSdkConfig object:

FieldTypeRequiredDescription
apiBaseUrlstringYesBase URL including the /api segment. Must not have a trailing slash.
timeoutMsnumberNoRequest timeout in milliseconds. Default: 15000.
fetchFntypeof fetchNoCustom 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() and solanaSwap() require MCP_EXPOSE_GRANULAR_TOOLS=true on the platform. The default 7 tools use tradeCommand() or agentExecute() 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 string
  • parseToolJson<T>(response) — parses text as JSON, returns T | null
  • isToolError(response) — returns true if isError === 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:

CodeTrigger
auth_error401 from the API
policy_error403 — scope or CSRF violation
rate_limit_error429 — IP or AI quota exceeded
provider_auth_errorIdentity provider JWT rejected
network_errorTimeout or network unreachable
execution_errorTool execution returned an error
invalid_responseNon-JSON or empty response body
unknown_errorUnclassified

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:

  1. Onboard — call onboardWithSiwe or onboardWithIdentity once. Store the MCP endpoint and API key securely.
  2. Connect — call sdk.connectMcp() with the stored credentials.
  3. Check status — call mcp.agentStatus() to verify auth and trading readiness.
  4. Research — use mcp.agentResearch() or granular market data tools to build context.
  5. Execute — use mcp.agentExecute() or mcp.tradeCommand() to act.
  6. Monitor — use mcp.agentPortfolio() or mcp.callTool("trading_positions", {}) to track state.
  7. Refresh — use TokenStore to 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_request is disabled on the platform. The requestSeed() method exists for backward compatibility but always throws an AgentSdkError with code execution_error.
  • apiBaseUrl must include the /api path 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.
  • manage scope is never issued at onboarding time. Start with read or trade.

Connection lost. Retrying...