Runtime Usage
The Runtime API sits one layer below the SDK helper functions. Instead of calling getSwapQuote() or resolveToken(), you call runtime.invokeTool("orbs_quote", { ... }) directly. This gives you full control over the environment each runtime uses, which tools it exposes, and how many runtimes run in parallel.
When to Use the Runtime API
Use the root SDK functions (import { ... } from "web3agent") when:
- You're building a single-user app or script.
- All calls share the same chain and private key.
- You want typed, named functions rather than string-based tool names.
Use the Runtime API when:
- Different users or agents need different private keys.
- You're running multiple agents in parallel, each with isolated state.
- You need to list or discover available tools dynamically.
- You want to integrate web3agent tools into a custom orchestration loop.
- You need fine-grained control over policy limits per agent.
Creating a Runtime
import { createRuntime } from "web3agent/runtime";
const runtime = await createRuntime({
env: {
CHAIN_ID: "8453",
PRIVATE_KEY: process.env.AGENT_KEY,
CONFIRM_WRITES: "false", // skip confirmation queue for autonomous agents
POLICY_ENABLED: "true",
POLICY_MAX_SINGLE_TX_USD: "500",
POLICY_MAX_DAILY_USD: "2000",
},
});
createRuntime() accepts an env object that mirrors the environment variables documented in the configuration reference. Values passed here override process.env for that runtime instance only. Other runtimes are unaffected.
The returned runtime object implements the Web3AgentRuntime interface:
import type { Web3AgentRuntime } from "web3agent/runtime";
Invoking Tools
const result = await runtime.invokeTool("resolve_token", {
symbol: "USDC",
chainId: 8453,
});
if (result.ok) {
console.log(result.data);
// { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" }
} else {
console.error(`[${result.error.code}] ${result.error.message}`);
}
invokeTool() always returns { ok: true, data: T } or { ok: false, error: { code, message } }. It never throws. Check result.ok before accessing result.data.
Common tool names
| Tool name | What it does |
|---|---|
resolve_token | Resolve a token symbol or address to metadata |
list_chain_tokens | List known tokens on a chain |
orbs_quote | Get a Liquidity Hub swap quote |
orbs_swap | Execute a Liquidity Hub swap |
lifi_bridge_quote | Get a cross-chain bridge quote |
lifi_execute_bridge | Execute a bridge |
evm_get_balance | Get native token balance |
evm_read_contract | Call a view function on any contract |
evm_write_contract | Send a transaction to a contract |
blockscout_get_address | Get address info from Blockscout |
server_status | Health check |
Use runtime.listTools() to get the full list at runtime.
Listing Available Tools
const tools = await runtime.listTools();
tools.forEach(tool => {
console.log(`${tool.name}: ${tool.description}`);
console.log(` Risk: ${tool.riskLevel}`); // "safe" | "financial" | "destructive"
});
This is useful for building dynamic UIs, generating documentation, or letting an LLM discover what's available before deciding which tool to call.
Health Checks
const health = await runtime.getHealth();
console.log(`Status: ${health.status}`); // "healthy" | "degraded" | "unhealthy"
console.log(`Tools loaded: ${health.toolCount}`);
console.log(`Wallet: ${health.wallet?.address ?? "not configured"}`);
if (health.integrations) {
for (const [name, status] of Object.entries(health.integrations)) {
console.log(` ${name}: ${status}`);
}
}
Call getHealth() at startup to verify the runtime initialized correctly before processing any requests.
Multiple Runtimes
The most common reason to use the Runtime API directly is running multiple agents with different wallets and risk limits. Create one runtime per agent and keep them isolated.
import { createRuntime } from "web3agent/runtime";
import type { Web3AgentRuntime } from "web3agent/runtime";
interface AgentConfig {
id: string;
privateKey: string;
maxSingleTxUsd: number;
maxDailyUsd: number;
}
class RuntimePool {
private runtimes = new Map<string, Web3AgentRuntime>();
async getOrCreate(agent: AgentConfig): Promise<Web3AgentRuntime> {
if (this.runtimes.has(agent.id)) {
return this.runtimes.get(agent.id)!;
}
const runtime = await createRuntime({
env: {
CHAIN_ID: "8453",
PRIVATE_KEY: agent.privateKey,
CONFIRM_WRITES: "false",
POLICY_ENABLED: "true",
POLICY_MAX_SINGLE_TX_USD: String(agent.maxSingleTxUsd),
POLICY_MAX_DAILY_USD: String(agent.maxDailyUsd),
},
});
this.runtimes.set(agent.id, runtime);
return runtime;
}
async shutdownAll(): Promise<void> {
await Promise.all(
Array.from(this.runtimes.values()).map(r => r.shutdown()),
);
this.runtimes.clear();
}
}
Each runtime tracks its own policy spend independently. Agent A's daily limit doesn't affect Agent B's.
A Real-World Pattern: DeFi Swap via Runtime
This is the pattern trading-arena uses to execute swaps for each agent:
import { createRuntime } from "web3agent/runtime";
async function executeDefiSwap(
agentPrivateKey: string,
fromSymbol: string,
toSymbol: string,
amountUsd: number,
) {
const runtime = await createRuntime({
env: {
CHAIN_ID: "8453",
PRIVATE_KEY: agentPrivateKey,
CONFIRM_WRITES: "false",
POLICY_ENABLED: "true",
POLICY_MAX_SINGLE_TX_USD: "1000",
},
});
try {
// 1. Resolve the from-token
const fromResult = await runtime.invokeTool("resolve_token", {
symbol: fromSymbol,
chainId: 8453,
});
if (!fromResult.ok) throw new Error(fromResult.error.message);
const fromToken = fromResult.data;
// 2. Get the current price
const priceResult = await runtime.invokeTool("get_token_price", {
tokens: [`8453:${fromToken.address}`],
});
if (!priceResult.ok) throw new Error(priceResult.error.message);
const priceUsd = priceResult.data.prices[`8453:${fromToken.address}`].usd;
// 3. Calculate raw amount
const rawAmount = Math.floor(
(amountUsd / priceUsd) * 10 ** fromToken.decimals,
).toString();
// 4. Resolve the to-token
const toResult = await runtime.invokeTool("resolve_token", {
symbol: toSymbol,
chainId: 8453,
});
if (!toResult.ok) throw new Error(toResult.error.message);
// 5. Execute the swap
const swapResult = await runtime.invokeTool("orbs_swap", {
fromToken: fromToken.address,
toToken: toResult.data.address,
fromAmount: rawAmount,
chainId: 8453,
});
if (!swapResult.ok) throw new Error(swapResult.error.message);
return swapResult.data;
} finally {
await runtime.shutdown();
}
}
Cleanup
Always call runtime.shutdown() when a runtime is no longer needed. It closes open connections and flushes any pending audit logs.
// In a try/finally block
const runtime = await createRuntime({ env: { ... } });
try {
// ... use runtime
} finally {
await runtime.shutdown();
}
// Or with a pool, shut everything down on process exit
process.on("SIGTERM", async () => {
await pool.shutdownAll();
process.exit(0);
});
Runtimes that aren't shut down will keep the Node.js process alive. In short-lived scripts this causes the process to hang after main() returns.
Runtime vs. Default Runtime
The root SDK functions (import { getSwapQuote } from "web3agent") use a shared default runtime created from process.env. That runtime is a singleton, created lazily on first use.
When you call createRuntime() yourself, you get a separate instance with its own environment, wallet, and policy state. The two don't interfere.
import { getSwapQuote } from "web3agent"; // uses default runtime
import { createRuntime } from "web3agent/runtime"; // creates a new one
// These run against different wallets and chains
const defaultQuote = await getSwapQuote({ ... });
const customRuntime = await createRuntime({ env: { CHAIN_ID: "42161", PRIVATE_KEY: "0x..." } });
const customResult = await customRuntime.invokeTool("orbs_quote", { ... });
You can also pass a custom runtime to root SDK functions via RuntimeBoundOptions:
import { getSwapQuote } from "web3agent";
const quote = await getSwapQuote(
{ fromToken: "...", toToken: "...", fromAmount: "...", chainId: 42161 },
{ runtime: customRuntime },
);
This lets you mix the ergonomic SDK functions with custom runtimes when needed.