3. Connecting your AI Agent to VeChain

Now you will learn how to connect them together by building actions that let your AI agent interact with VeChain blockchain.

3.1 Project Setup

Environment Configuration

Create a .env file in your project root. This file holds configuration that shouldn't be in your code (like API keys or network URLs).

Why separate environments?

Testnet is a practice blockchain where nothing is real. Mainnet is the real blockchain with real VET and real consequences. You can develop on mainnet or testnet because we are just fetching data.

The environment variables let you switch between them by changing one line instead of hunting through your code.

File Organization

Your project should look like this:

your-vechain-agent/
├── .env                  # Configuration
├── package.json          # Dependencies
├── src/
├── index.ts         # Registers actions
└── character.ts     # Agent personality (from Part 2)
└── vechain/
    ├── balance.ts       # Balance checking
    ├── alias.ts         # Alias lookup
    └── utils.ts         # Helper functions

This organization separates concerns:

  • src/ contains ElizaOS-specific code

  • vechain/ contains blockchain-specific code. (you will be adding all your VeChain-related tools here)

  • If you switch blockchains later, you only change the vechain/ folder

3.2 Testing Your Agent

Build and Start:

# Compile TypeScript to JavaScript
bun run build

# Start with hot-reload (automatically rebuilds on code changes)
bun run dev

# Or start without hot-reload (must rebuild manually after changes)
bun run start

What happens when you start:

ElizaOS reads your character file, initializes the runtime, calls your initCharacter() function which registers your actions, connects to OpenAI (or whatever AI provider you configured), and starts listening for messages.

You'll see console output showing initialization steps. If something fails (like a missing API key), it will show an error here.

Test Your Actions 

Once running, test with these examples:

Test 1: Balance with address

You: What's the balance of 0x9366662519dc456bd5b8bc4ee4b6852338d82f08?

Agent: Balance for 0x9366662519dc456bd5b8bc4ee4b6852338d82f08:
💎 VET: 1234.56
VTHO: 890.12

Test 2: Balance without address

You: Show me my balance

Agent: Please provide a VeChain address. Example: balance 0xYourAddressHere

Test 3: Alias lookup

You: alias 0x9366662519dc456bd5b8bc4ee4b6852338d82f08

Agent: The VNS alias for 0x9366662519dc456bd5b8bc4ee4b6852338d82f08 is: vechain.vet

Test 4: Invalid address

You: balance 0x123

Agent: Couldn't fetch balance right now. Please try again later.

You can see one of our developers test his AI Agent here.

Understanding Test Results

If the agent doesn't respond:

  • Check that the action is registered in initCharacter()

  • Add console.log() statements in your validate function to see if it's being called

  • Make sure your keywords match what you're typing

If you get errors:

  • Read the error message carefully - it usually tells you what's wrong

  • Check the console for more detailed error logs

Verify your .env file has the right values

3.3 Adding Your Own Actions

Now that you understand the pattern, you can add any blockchain functionality you want. Here's the recipe:

1. Create the VeChain SDK function

First, figure out what blockchain data you need and write a function to fetch it. Use the VeChain SDK documentation as a reference.

For example, let’s say you want to get the latest deposit on Stargate. The simplest way will be to filter transactions sent `to` the Stargate smart contract. 

To get the Stargate contract address, you go to VeChain Stats.

Just like we did at the start of this tutorial, we’ll create a new script in the ./vechain directory. We’ll use the `filterTransferLogs()` method:

import { ThorClient } from "@vechain/sdk-network";


export async function getLastTransfers(address, network = process.env.VECHAIN_NETWORK) {
  const thor = ThorClient.at(network);


  const deposits = await thor.logs.filterTransferLogs({
    criteriaSet: [{ sender: address, recipient: '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7'}],
    range: { unit: "block", from: 20_000_000 },
    options: { limit: 1 },
    order: 'desc',
  });


  const d = deposits[0];


  return {
    from: address,
    to: d.recipient,
    value: (BigInt(d.amount) / 1000000000000000000n).toString(),
  };
}

Let’s unpack this. We’re using the vechain SDK to fetch data. For a more detailed step-by-step on how to use the SDK, check the Developer Fundamentals course. 

We start by importing the ThorClient and initiating it within a export function:

import { ThorClient } from "@vechain/sdk-network";


export async function getLastTransfers(address, network = process.env.VECHAIN_NETWORK) {
  const thor = ThorClient.at(network);

We then move to setting up our filters for VET transfers. Note that: 

  • we’re using the Stargate address as the `recipient` 

  • we add a further filter to only look for transfers starting from block 20M. This limits the search, so we’re not looking for transfers from the genesis block.

const deposits = await thor.logs.filterTransferLogs({
    criteriaSet: [{ sender: address, recipient: '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7'}],
    range: { unit: "block", from: 20_000_000 },
    options: { limit: 1 },
    order: 'desc',
  });
  const d = deposits[0];

Finally, we prepare the response to be easily readable by our agent:

  return {
    from: address,
    to: d.recipient,
    value: (BigInt(d.amount) / 1000000000000000000n).toString(),
  };
}

2. Create the action

Now create the ElizaOS action that connects to your VeChain function:

// src/index.ts
const yourAction: any = {
  name: "YOUR_FEATURE",
  similes: ["RELEVANT", "KEYWORDS"],
  description: "Clear description of what it does",
  
  validate: async (_runtime: any, message: any) => {
    const text = message?.content?.text || "";
    // Check for your specific patterns
    return /your-pattern/.test(text);
  },
  
  handler: async (_runtime: any, message: any, _state: any, _options: any, callback: any) => {
    const address = extractAddress(message?.content?.text || "");
    
    if (!address) {
      await callback({ text: "Helpful error message" });
      return true;
    }
    
    try {
      const result = await yourFeature(address);
      await callback({ text: `Formatted result: ${result}` });
    } catch (error) {
      await callback({ text: "Error message" });
    }
    
    return true;
  },
  
  examples: [[
    { user: "user", content: { text: "example input" } },
    { user: "assistant", content: { text: "example output" } }
  ]]
};

3. Register it

Every new action you add to your agent needs to be included in the character initialization so it gets registered on launch.

To do this, add runtime.registerAction(yourAction); to the initCharacter function:

const initCharacter = ({ runtime }: { runtime: any }) => {
  runtime.registerAction(yourAction);
};

4. Test it

After creating your new action, rebuild your agent and test it with various inputs to make sure it works correctly.

To rebuild your agent you need to run this command:

rm -rf dist 2>/dev/null || rimraf dist

bun run build

bun dev