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
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 codevechain/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
# 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
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
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
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: 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 calledMake 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(), }; }
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);
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];
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(), }; }
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" } } ]] };
// 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); };
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
rm -rf dist 2>/dev/null || rimraf dist bun run build bun dev
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 codevechain/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 calledMake 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
Join our Telegram