Skip to main content
Skip to main content

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 nameWhat it does
resolve_tokenResolve a token symbol or address to metadata
list_chain_tokensList known tokens on a chain
orbs_quoteGet a Liquidity Hub swap quote
orbs_swapExecute a Liquidity Hub swap
lifi_bridge_quoteGet a cross-chain bridge quote
lifi_execute_bridgeExecute a bridge
evm_get_balanceGet native token balance
evm_read_contractCall a view function on any contract
evm_write_contractSend a transaction to a contract
blockscout_get_addressGet address info from Blockscout
server_statusHealth 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.