Build an AI agent
ElizaOS is an open-source framework for creating AI agents that interact with blockchain networks. This tutorial will guide you through setting up an AI agent on the Linea blockchain using ElizaOS. By the end, your agent will be able to execute smart contract transactions and interact with the blockchain autonomously. You’ll be able to add any custom action to improve it and make it the best agent in town.
Prerequisites
- Install Node.js (23.3) and pnpm.
- Basic knowledge of TypeScript and blockchain concepts.
- Access to a Linea RPC endpoint and a funded Ethereum wallet for testing. We strongly recommend you use a development wallet. Stay safe!
1. Set up the environment
To start, we want to clone the main ElizaOS repo, there’s also a starter-kit if you prefer but I recommend you to use the full package:
git clone https://github.com/elizaOS/eliza.git # Starter kit: eliza-starter.git
cd eliza # Starter kit : eliza-starter
This sets up the framework on your local machine. ElizaOS is iterating fast, so a few things might have changed if you’re following this tutorial a long time after its publication.
Be sure you’re using the right version of Node. If you get an error message, it can help to to start again with a clean Node modules installation.
Then, you need to checkout to the latest version of ElizaOS:
git checkout $(git describe --tags --abbrev=0)
# If the above doesn't checkout the latest release, this should work:
# git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
pnpm install
Installation can take time, so relax and go grab a drink ☕
2. Configure the AI agent for Linea
ElizaOS uses environment variables stored in a .env
file to manage configurations, including blockchain settings.
Set up the .env
file
Copy the .env.example
file in a .env
file in the root directory and configure it as follows:
# Blockchain Connection
EVM_RPC_URL=https://rpc.linea.build
EVM_PRIVATE_KEY=your_private_key_here
Replace your_private_key_here
with your Ethereum development private key. Be sure to keep this key private and do not reuse it to store funds.
We’ll also need a modelProvider
(unless you feel comfortable running a local model on your computer; it can be quite slow depending on your computer's performance). You can add your API key in the .env
file, depending on which provider you’ll use (OpenAI, Anthropic, Gaia, etc.). Some modelProvider
s provide different options, such as enabling you to choose a specific model for certain actions.
Install the EVM plugin
We’ll use the EVM Plugin to interact with the Linea blockchain.
If you used the main repository, it should already be installed, otherwise, you need to install it:
pnpm add @elizaos/plugin-evm
Then, configure the plugin in your character settings.
ElizaOS uses some “characters” files that define your agent's personality. You can create one or choose a preconfigured one in the characters folder.
At the top of the file, you'll need to add the following:
{
...,
"modelProvider": "your_provider_name", //eg. openai
"settings": {
"chains": {
"evm": [ "lineaSepolia" ]
}
},
"plugins": ["@elizaos/plugin-evm"],
...
}
This will allow you to use all the actions in the EVM plugin and configure your AI agent to use your provider.
3. How it works
If you check the /plugin-evm
folder, you’ll see different types of files:
Actions files
They define actions that can be performed by the agent.
import { type ByteArray, formatEther, parseEther, type Hex } from "viem";
import {
type Action,
composeContext,
generateObjectDeprecated,
type HandlerCallback,
ModelClass,
type IAgentRuntime,
type Memory,
type State,
} from "@elizaos/core";
import { initWalletProvider, type WalletProvider } from "../providers/wallet";
import type { Transaction, TransferParams } from "../types";
import { transferTemplate } from "../templates";
// Exported for tests
export class TransferAction {
constructor(private walletProvider: WalletProvider) {}
async transfer(params: TransferParams): Promise<Transaction> {
console.log(
`Transferring: ${params.amount} tokens to (${params.toAddress} on ${params.fromChain})`
);
if (!params.data) {
params.data = "0x";
}
this.walletProvider.switchChain(params.fromChain);
const walletClient = this.walletProvider.getWalletClient(
params.fromChain
);
try {
const hash = await walletClient.sendTransaction({
account: walletClient.account,
to: params.toAddress,
value: parseEther(params.amount),
data: params.data as Hex,
kzg: {
blobToKzgCommitment: (_: ByteArray): ByteArray => {
throw new Error("Function not implemented.");
},
computeBlobKzgProof: (
_blob: ByteArray,
_commitment: ByteArray
): ByteArray => {
throw new Error("Function not implemented.");
},
},
chain: undefined,
});
return {
hash,
from: walletClient.account.address,
to: params.toAddress,
value: parseEther(params.amount),
data: params.data as Hex,
};
} catch (error) {
throw new Error(`Transfer failed: ${error.message}`);
}
}
}
const buildTransferDetails = async (
state: State,
runtime: IAgentRuntime,
wp: WalletProvider
): Promise<TransferParams> => {
const chains = Object.keys(wp.chains);
state.supportedChains = chains.map((item) => `"${item}"`).join("|");
const context = composeContext({
state,
template: transferTemplate,
});
const transferDetails = (await generateObjectDeprecated({
runtime,
context,
modelClass: ModelClass.SMALL,
})) as TransferParams;
const existingChain = wp.chains[transferDetails.fromChain];
if (!existingChain) {
throw new Error(
"The chain " +
transferDetails.fromChain +
" not configured yet. Add the chain or choose one from configured: " +
chains.toString()
);
}
return transferDetails;
};
export const transferAction: Action = {
name: "transfer",
description: "Transfer tokens between addresses on the same chain",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: any,
callback?: HandlerCallback
) => {
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}
console.log("Transfer action handler called");
const walletProvider = await initWalletProvider(runtime);
const action = new TransferAction(walletProvider);
// Compose transfer context
const paramOptions = await buildTransferDetails(
state,
runtime,
walletProvider
);
try {
const transferResp = await action.transfer(paramOptions);
if (callback) {
callback({
text: `Successfully transferred ${paramOptions.amount} tokens to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`,
content: {
success: true,
hash: transferResp.hash,
amount: formatEther(transferResp.value),
recipient: transferResp.to,
chain: paramOptions.fromChain,
},
});
}
return true;
} catch (error) {
console.error("Error during token transfer:", error);
if (callback) {
callback({
text: `Error transferring tokens: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("EVM_PRIVATE_KEY");
return typeof privateKey === "string" && privateKey.startsWith("0x");
},
examples: [
[
{
user: "assistant",
content: {
text: "I'll help you transfer 1 ETH to 0x9FA746b844747f77c6C54F4f88ab71048c608864",
action: "SEND_TOKENS",
},
},
{
user: "user",
content: {
text: "Transfer 1 ETH to 0x9FA746b844747f77c6C54F4f88ab71048c608864",
action: "SEND_TOKENS",
},
},
],
],
similes: ["SEND_TOKENS", "TOKEN_TRANSFER", "MOVE_TOKENS"],
};
Contracts artifacts and sources
If you want to allow your agent to deploy new contracts or interact with existing contracts, you can put them in this folder to be able to refer to the ABI in your actions files.
Providers
Contains wallet providers files. You can modify these to use AA wallets, for example, or connect your agent with an MPC provider.