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: prove a read path first
This first call requests read scope and uses the safe context wrapper. Do not
start an integration quickstart with trade authority.
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: "read",
});
// 5. Connect to MCP and call a tool
const mcp = sdk.connectMcp({
endpoint: response.mcp.endpoint,
apiKey: response.mcp.apiKey ?? "",
});
const context = await mcp.agentContextSnapshot();
renderContext(context);You are done when
- The onboarding response contains an MCP endpoint and API key.
agentContextSnapshot()returns a structured response or an explicit unavailable/degraded state.- No trade, wallet funding, approval, or live arming action was required.
Add trade scope only after the read path, error handling, key storage, and
runtime-state checks are understood.
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 safe public helper surface.
The exact MCP tool surface is returned by tools/list for the current key
scope. When the operator-controlled runtime exposes granular tools,
scope-qualified agents can also see an explicit read-only, non-raw Web3
research allowlist for market, liquidity, holder, launchpad, social,
rug/security, quote, and simulation drill-downs. Do not infer availability from
docs; use the current tools/list response for the same key.
// Natural language query through the AI pipeline
const reply = await mcp.askBot({
message: "Summarize this agent's current source health.",
});
// Runtime and readiness checks
await mcp.agentStatus();
await mcp.agentReadinessReport({ chain: "solana" });
await mcp.agentContextSnapshot();
// Shared market brief with source-health caveats
await mcp.agentMarketBrief({
query: "Solana launch candidates with source health",
chain: "solana",
signals: ["launches", "liquidity"],
limit: 5,
});
// Specific asset reports
await mcp.agentCandidateReport({
query: "BONK",
chain: "solana",
includeX: true,
includeOnchain: true,
});
await mcp.agentRiskReport({
query: "0x4200000000000000000000000000000000000006",
chain: "base",
includeOnchain: true,
});Read-only granular tools
For granular tools or tools without a typed method, use callTool directly.
Only call tools returned by tools/list for the current key scope. Granular MCP
exposure is a read-only, non-raw allowlist; direct swap-submit, wallet mutation,
approval, withdrawal, raw provider, and broad internal research surfaces are not
public SDK helper methods.
const tools = await mcp.listTools();
const names = new Set(tools.tools.map((tool) => tool.name));
if (names.has("trading_solana_jupiter_quote")) {
const quote = await mcp.callTool("trading_solana_jupiter_quote", {
inputMint: "So11111111111111111111111111111111111111112", // SOL
outputMint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // BONK
amount: "100000000", // 0.1 SOL (9 decimals)
slippageBps: 100,
});
renderQuote(quote);
}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.agentContextSnapshot();
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.agentMarketBrief({ query: "Solana launch candidates", limit: 5 });
} 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.agentMarketBrief({ query: "SOL", limit: 5 }),
{ 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. - Diagnose readiness — call
mcp.agentReadinessReport()before relying on stale runtime, source, or subscription state. - Research safely — use
mcp.agentMarketBrief(),mcp.agentCandidateReport(), andmcp.agentRiskReport()to build context with source-health caveats. - Monitor — use
mcp.agentContextSnapshot()or a read-only granular tool returned bytools/list. - Escalate deliberately — use Hub, CLI, or the authorized MCP tool surface for trade-scope operations; do not assume public SDK typed mutation helpers exist.
- 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.- Granular MCP exposure is not a broad debug mode. Use the tools returned by
tools/listfor the current Hub agent key as the source of truth. managescope is never issued at onboarding time. Start withreadortrade.