Skip to main content

How to Build Web3-Enabled AI Agents with Eliza

Updated on
Feb 10, 2025

26 min read

Overview

Eliza is an open-source framework for building AI agents with integrated Web3 capabilities. In this guide, we will cover the core concepts of the Eliza framework, then show you how to create your own agent character and then use the @elizaos/plugin-evm plugin to demonstrate blockchain interactions such as ETH transfers.

What You Will Do


  • Set up an Agent character
  • Review code that covers actions like transfers and swaps
  • Interact with the agent to conduct ETH transfers on Ethereum Sepolia (or use the choice of your choice)
  • Lay out future suggestions to build upon

What You Will Need


  • Intermediate understanding of programming concepts
  • Node.js, TypeScript and pnpm installed (using nvm is recommended)
  • An EVM wallet (with some ETH to simulate transfers, swaps and pay gas fees)
  • A QuickNode endpoint (create one here)
  • An Anthropic or OpenAI API Key
DependencyVersion
node23.3.0
pnpm9>

What is a16z Eliza?

Eliza is a TypeScript-based framework for building and deploying autonomous AI agents. It provides pre-built systems character definition, runtime management, and cross-platform interactions. Using Eliza, you can create agents with consistent personalities that interact through platforms like Discord, Telegram, or custom interfaces while maintaining shared memory and state management.

Eliza & Web3 Integration

Eliza integrates with blockchain networks through a plugin system that extends core functionality. These plugins enable AI agents to interact with various blockchains, manage crypto wallets, create transactions, and monitor blockchain events - all while maintaining the agent's personality and conversation abilities.

Some popular web3 protocols that have already implemented Eliza are:


  • Solana Plugin (@eliza/plugin-solana): Handles Solana blockchain interactions with built-in wallet management and trust scoring
  • Coinbase Plugin (@eliza/plugin-coinbase): Complete suite for managing crypto payments, mass payouts, and token contracts across multiple chains
  • Token Contract Plugin (@eliza/plugin-coinbase): Deploys and interacts with ERC20, ERC721, and ERC1155 smart contracts
  • MassPayments Plugin (@eliza/plugin-coinbase): Processes bulk crypto payments with automatic charity contributions
  • Webhook Plugin (@eliza/plugin-coinbase-webhooks): Creates and manages blockchain event listeners for real-time notifications
  • Fuel Plugin (@elizaos/plugin-fuel): Interfaces with the Fuel Ignition blockchain for ETH transfers
  • TEE Plugin (@elizaos/plugin-tee): Enables secure key management in trusted execution environments for both Ethereum and Solana

Check out a full list of Eliza web3 plugins here.

Eliza Framework: Concepts

The Eliza framework can be split into 4 concepts:


  • Characters: JSON config files defining AI personality and behavior
  • Agents: Runtime components managing memory and executing behaviors
  • Providers: Data connectors injecting context into interactions
  • Actions: Executable behaviors that agents can perform

Characters

Characters in Eliza are JSON configurations that define your AI agent's personality and behavior. Think of them as the DNA of your agent - they contain everything from basic personality traits to complex interaction patterns.

The character file structure looks like this:

{
"name": "ExampleAgent",
"bio": [
"Bio lines are each short snippets which can be composed together in a random order.",
"We found that it increases entropy to randomize and select only part of the bio for each context.",
"This 'entropy' serves to widen the distribution of possible outputs, which should give more varied but continuously relevant answers."
],
"lore": [
"Lore lines are each short snippets which can be composed together in a random order, just like bio",
"However these are usually more factual or historical and less biographical than biographical lines",
"Lore lines can be extracted from chatlogs and tweets as things that the character or that happened to them",
"Lore should also be randomized and sampled from to increase entropy in the context"
],
"messageExamples": [
[
{
"user": "ExampleAgent",
"content": {
"text": "Each conversation turn is an array of message objects, each with a user and content. Content contains text and can also contain an action, attachments, or other metadata-- but probably just text and maybe actions for the character file."
}
},
{
"user": "{{user1}}",
"content": {
"text": "We can either hardcode user names or use the {{user1}}, {{user2}}, {{user3}} placeholders for random names which can be injected to increase entropy."
}
}
],
[
{
"user": "{{user1}}",
"content": {
"text": "The tweet2character generator might only pose questions and answers for the examples, but it's good to capture a wide variety of interactions if you are hand-writing your characters"
}
},
{
"user": "ExampleAgent",
"content": {
"text": "You can also have message examples of any length. Try to vary the length of your message examples from 1-8 messages fairly evenly, if possible.",
"action": "CONTINUE"
}
},
{
"user": "ExampleAgent",
"content": {
"text": "Message examples should also be randomly sampled from to increase context entropy"
}
}
]
],
"postExamples": [
"These are examples of tweets that the agent would post",
"These are single string messages, and should capture the style, tone and interests of the agent's posts"
],
"adjectives": [
"adjectives",
"describing",
"our agent",
"these can be madlibbed into prompts"
],
"topics": [
"topics",
"the agent is interested in"
],
"knowledge": [
{
"id": "a85fe83300ff8d167f5c8c2e37008699a0ada970c422fd66ffe1a3a668a7ff54",
"path": "knowledge/blogpost.txt",
"content": "Full extracted text knowledge from documents that the agent should know about. These can be ingested into any agent knowledge retrieval / RAG system."
}
],
"style": {
"all": [
"These are directions for how the agent should speak or write",
"One trick is to write the directions themselves in the style of the agent",
"Here are some examples:",
"very short responses",
"never use hashtags or emojis",
"don't act like an assistant"
],
"chat": [
"These directions are specifically injected into chat contexts, like Discord"
],
"post": [
"These directions are specifically injected into post contexts, like Twitter"

]
}
}

The real power of characters comes from their ability to randomize responses while maintaining consistency. By breaking bio and lore (knowledge, personality) into smaller chunks, you get more natural variations in your agent's responses.

Agents

Agents are the runtime components that bring your characters to life. They manage the actual execution of your AI's behaviors through the AgentRuntime class.

The main configuration requires a database adapter for persistence, a model provider (e.g., openai, anthropic, etc.) for LLM inference, and an authentication token (from the LLM provider), and a character configuration object. Optional parameters include evaluators for assessing outputs and plugins (like the EVM plugin shown) that extend functionality. Here's an example:

    return new AgentRuntime({
databaseAdapter: db,
token,
modelProvider: character.modelProvider,
evaluators: [],
character,
plugins: [
getSecret(character, "EVM_PUBLIC_KEY") ||
(getSecret(character, "WALLET_PUBLIC_KEY") &&
getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))
? evmPlugin
: null
]
})

Next, we'll look into providers.

Providers

Providers handle specialized functionality like wallet integrations and data access. Here's how providers are implemented using an EVM wallet example:

export const evmWalletProvider: Provider = {
async get(
runtime: IAgentRuntime,
_message: Memory,
state?: State
): Promise<string | null> {
try {
const walletProvider = await initWalletProvider(runtime);
const address = walletProvider.getAddress();
const balance = await walletProvider.getWalletBalance();
const chain = walletProvider.getCurrentChain();
return `${state?.agentName || "The agent"}'s EVM Wallet Address: ${address}\nBalance: ${balance} ${chain.nativeCurrency.symbol}\nChain ID: ${chain.id}, Name: ${chain.name}`;
} catch (error) {
console.error("Error in EVM wallet provider:", error);
return null;
}
},
};

A provider needs to implement the get() method which accepts runtime configuration, message context, and current state. The example above shows how the EVM wallet provider fetches wallet details like address, balance, and chain information to expose blockchain functionality to agents.

The core of what we're showcasing in this guide is the transfer and swap functionality. This is covered in Actions in which we'll cover next.

Actions

Actions define the specific behaviors agents can perform. Here's a typical action implementation for token transfers:

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
) => {
const walletProvider = await initWalletProvider(runtime);
const action = new TransferAction(walletProvider);
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, ... }
});
}
return true;
} catch (error) {
console.error("Error during token transfer:", error);
return false;
}
}
};

The power of the Eliza codebase comes from how these components work together. Characters define the personality, Agents manage the runtime, Providers feed in real-world data and utility, and Actions execute specific behaviors.

Now that we have a better understanding of the Eliza framework concepts, let's move onto the coding portion of the guide (warm up your keyboards!).

Project Prerequisite: Create a QuickNode Endpoint

To communicate with the blockchain, you need access to a node. While we could run our own node, here at QuickNode, we make it quick and easy to fire up blockchain nodes. You can register for an account here. Once you boot up a node, retrieve the HTTP URL. It should look like this:

Screenshot of Quicknode mainnet endpoint

tip

This guide is EVM compatible. If you want to deploy your agent on another chain, pick another EVM-compatible chain (e.g., Optimism, Arbitrum, etc.) and update the wallet and RPC URL's accordingly. You can also add the Chain Prism add-on to your endpoint so you can access multiple blockchain RPC URLs within the same endpoint.

Project Prerequisite: Get ETH from QuickNode Multi-Chain Faucet

In order to conduct activity on-chain, you'll need ETH to pay for gas fees. Since we're using the Sepolia testnet, we can get some test ETH from the Multi-Chain QuickNode Faucet.

Navigate to the Multi-Chain QuickNode Faucet and connect your wallet (e.g., MetaMask, Coinbase Wallet) or paste in your wallet address to retrieve test ETH. Note that there is a mainnet balance requirement of 0.001 ETH on Ethereum Mainnet to use the EVM faucets. You can also tweet or log in with your QuickNode account to get a bonus!

QuickNode Faucet

Setting up the Eliza Repository

Here's how to get started with the Eliza repository.

First, clone the repository:

git clone https://github.com/elizaOS/eliza.git
cd eliza

Next, install the latest release:

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`)

Then, install dependencies:

pnpm install --no-frozen-lockfile
tip

If you run into issues, check out this issue for a resolution.

Build the libraries:

pnpm build
tip

If you run into a turbo command not found error, try installing turbo globally with: pnpm install -g turbo

Finally, copy the example .env into one we'll use to store credentials:

cp .env.example .env

Update your .env file to include the following values in each appriopiate variable.

EVM_PRIVATE_KEY=
EVM_PROVIDER_URL=
ANTHROPIC_API_KEY=

Building Your Character

Each character starts with a base configuration. Find examples in characters/ or check packages/core/src/defaultCharacter.ts for reference.

Let's create a DeFi degen character who lived through the 2021 bull run and survived multiple rug pulls. This character will:


  • Have battle-tested experience from DeFi summer
  • Know common rug pull patterns
  • Share stories from farmer days (its honest work)

Create a file characters/degen.character.json, then include the following JSON config:

{
"name": "YieldMaxoor",
"clients": [],
"modelProvider": "anthropic",
"settings": {},
"plugins": ["@elizaos/plugin-evm"],
"bio": [
"YieldMaxoor is a battle-tested DeFi degen who's been farming since the 2020 'DeFi Summer'",
"Speaks in crypto-native slang and always DYOR-pilled",
"Claims every new protocol is 'probably not a rug' and 'ser, the APY is real'",
"Frequently mentions their portfolio being 'down bad' but 'still bullish'"
],
"lore": [
"Started yield farming during DeFi Summer 2020",
"Survived multiple bear markets and 'temporary' depeg events",
"Specialist in hunting the highest APYs across chains",
"Always emphasizes DYOR while aping first, reading docs later"
],
"knowledge": [
"Yield farming strategies",
"DEX liquidity provision",
"Cross-chain bridges",
"MEV protection",
"Smart contract risk assessment",
"Gas optimization",
"Impermanent loss calculations",
"Tokenomics analysis"
],
"messageExamples": [
[
{
"user": "{{user1}}",
"content": {
"text": "What do you think about this new farm?"
}
},
{
"user": "YieldMaxoor",
"content": {
"text": "ser, the APY is looking juicy af. audit's coming 'soon™' but team is based. probably not a rug. already threw in 2 ETH to test it out ngmi if you're not in this 🚜",
"action": "ANALYZE_FARM"
}
}
],
[
{
"user": "{{user1}}",
"content": {
"text": "How do I avoid IL?"
}
},
{
"user": "YieldMaxoor",
"content": {
"text": "fren, IL is just a temporary state of mind. but if you're ngmi with that, stick to stables farming or single-sided staking. this is financial advice because i'm already poor 😅",
"action": "EXPLAIN_IL"
}
}
],
[
{
"user": "{{user1}}",
"content": {
"text": "Is this protocol safe?"
}
},
{
"user": "YieldMaxoor",
"content": {
"text": "anon, i've been rugged so many times i can smell them coming. this one's based - doxxed team, good tvl, clean code. but always DYOR and don't put in more than you can lose ser 🤝"
}
}
]
],
"postExamples": [
"gm frens, just found a 4 digit APY farm. probably nothing 👀",
"ser, the yields are bussin fr fr no 🧢",
"another day another protocol to ape into. wagmi 🚜"
],
"topics": [
"yield_farming",
"defi_strategies",
"tokenomics",
"protocol_analysis"
],
"style": {
"all": [
"Crypto-native",
"Degen",
"Optimistic",
"Based",
"Uses 'ser', 'fren', and 'gm' frequently"
],
"chat": [
"Informal",
"Enthusiastic",
"Slightly degen",
"Uses emojis liberally"
],
"post": [
"Brief",
"Memetic",
"Bullish",
"Full of crypto slang"
]
},
"adjectives": [
"Based",
"Degen",
"Bullish",
"Battle-tested",
"Yield-pilled",
"Crypto-native",
"Memetic"
]
}
info

If you are using an OpenAI for your model, change the "modelProvider": "anthropic", to "modelProvider": "openai", and update your .env variable (OPENAI_API_KEY) accordingly.

Feel free to adjust this character for fun. To save time, you can also use a tool such as ChatGPT to automate the process of creating characters based on your prompts.

In the next section, we'll show you how to run the character and use it to interact with the blockchain.

Building Actions

In this guide, we won't be building out our own action (look out for the next guide and leave some feedback below if you want to see it!), but we'll walk through the code of an existing Action to get a better idea.

Navigate to the /packages/plugin-evm/src/actions/transfer.ts file to follow along:

Transfer Action

The TransferAction class handles token transfers on EVM chains, using a walletProvider to manage connections. Its transfer method executes transactions with specified amount, recipient address, and chain parameters, returning transaction details or throwing errors on failure.

async transfer(params: TransferParams): Promise<Transaction> {
this.walletProvider.switchChain(params.fromChain);
const walletClient = this.walletProvider.getWalletClient(params.fromChain);

const hash = await walletClient.sendTransaction({
account: walletClient.account,
to: params.toAddress,
value: parseEther(params.amount),
data: params.data as Hex
});
}

Parameter Building - Constructing Transfer Details

The buildTransferDetails function validates chain support and constructs transfer parameters using templates and runtime configuration.

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 transferDetails = await generateObjectDeprecated({
runtime,
context,
modelClass: ModelClass.SMALL,
}) as TransferParams;
}

Action Handler

Coordinates the full transfer workflow, including wallet initialization, parameter building, and execution.

handler: async (runtime: IAgentRuntime, message: Memory, state: State) => {
// Orchestrates the entire transfer process
const walletProvider = await initWalletProvider(runtime);
const transferResp = await action.transfer(paramOptions);
}

Templates

Plugins have contain templates which are predefined structures that help parse and validate user inputs for specific actions.

In /packages/plugin-evm/src/templates/index.ts, there is a transferTemplate which acts as:

export const transferTemplate = `You are an AI assistant specialized in processing cryptocurrency transfer requests. Your task is to extract specific information from user messages and format it into a structured JSON response.

First, review the recent messages from the conversation:

<recent_messages>
{{recentMessages}}
</recent_messages>

Here's a list of supported chains:
<supported_chains>
{{supportedChains}}
</supported_chains>

Your goal is to extract the following information about the requested transfer:
1. Chain to execute on (must be one of the supported chains)
2. Amount to transfer (in ETH, without the coin symbol)
3. Recipient address (must be a valid Ethereum address)
4. Token symbol or address (if not a native token transfer)

Before providing the final JSON output, show your reasoning process inside <analysis> tags. Follow these steps:

1. Identify the relevant information from the user's message:
- Quote the part of the message mentioning the chain.
- Quote the part mentioning the amount.
- Quote the part mentioning the recipient address.
- Quote the part mentioning the token (if any).

2. Validate each piece of information:
- Chain: List all supported chains and check if the mentioned chain is in the list.
- Amount: Attempt to convert the amount to a number to verify it's valid.
- Address: Check that it starts with "0x" and count the number of characters (should be 42).
- Token: Note whether it's a native transfer or if a specific token is mentioned.

3. If any information is missing or invalid, prepare an appropriate error message.

4. If all information is valid, summarize your findings.

5. Prepare the JSON structure based on your analysis.

After your analysis, provide the final output in a JSON markdown block. All fields except 'token' are required. The JSON should have this structure:

\`\`\`json
{
"fromChain": string,
"amount": string,
"toAddress": string,
"token": string | null
}
\`\`\`

Remember:
- The chain name must be a string and must exactly match one of the supported chains.
- The amount should be a string representing the number without any currency symbol.
- The recipient address must be a valid Ethereum address starting with "0x".
- If no specific token is mentioned (i.e., it's a native token transfer), set the "token" field to null.

Now, process the user's request and provide your response.
`;

This transfer template is a prompt configuration that helps language models understand and process action transfer requests.

Agent File (index.ts)

At the heart of your agent is the agent/src/index.ts file which is the server initialization file that sets up and manages AI agents that can interact across different platforms (like Discord, Telegram, etc.) and blockchain networks (via plugins).

For example:

async function startAgent(
character: Character,
directClient: DirectClient
): Promise<AgentRuntime> {
character.id ??= stringToUuid(character.name);
character.username ??= character.name;

const token = getTokenForProvider(character.modelProvider, character);
const dataDir = path.join(__dirname, "../data");

// Initialize database
const db = initializeDatabase(dataDir) as IDatabaseAdapter & IDatabaseCacheAdapter;
await db.init();

// Setup caching
const cache = initializeCache(
process.env.CACHE_STORE ?? CacheStore.DATABASE,
character,
"",
db
);

// Create and initialize the agent runtime
const runtime: AgentRuntime = await createAgent(character, db, cache, token);
await runtime.initialize();

// Initialize platform clients (Discord, Telegram, etc.)
runtime.clients = await initializeClients(character, runtime);

// Register with direct client
directClient.registerAgent(runtime);

return runtime;
}

This function orchestrates all the core pieces: character configuration, database setup, caching, runtime creation, and client initialization.

Interacting with the Agent

Now we'll want to run the character and then the client. In your terminal, run the following command to start your agent character:

pnpm start --character="characters/degen.character.json"

This command initializes steps like the character, database (e.g., Redis; but this is configured with the repo so we don't have to touch at the moment), model (e.g., ChatGPT, Claude, etc.), RAG knowledge (i.e., the process of optimizing the output of a large language model).

Once your character is running, start the client in another terminal:

pnpm start:client

We can then open the outputted URL (http://localhost:5173) and prompt the agent to make an action.

Transfer .0042069 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e

Client Screenshot

You will see logs in the terminal window your agent character is running in. For instance, we see the agent executed the action and ran into some issues parsing text, however, this did not affect the transfer:

[2025-02-07 16:52:58] INFO: Selected model: claude-3-5-sonnet-20241022
[2025-02-07 16:53:04] INFO: Executing handler for action: transfer
Transfer action handler called
[2025-02-07 16:53:04] INFO: Generating text with options:
modelProvider: "anthropic"
model: "small"
verifiableInference: false
[2025-02-07 16:53:04] INFO: Selected model: claude-3-haiku-20240307
Error parsing JSON: SyntaxError: Unexpected token '<', "<analysis>"... is not valid JSON
at JSON.parse (<anonymous>)
at parseJSONObjectFromText (file:///Users/ferhat/Documents/eliza/packages/core/dist/index.js:220:29)
at generateObjectDeprecated (file:///Users/ferhat/Documents/eliza/packages/core/dist/index.js:35130:36)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async buildTransferDetails (file:///Users/ferhat/Documents/eliza/packages/plugin-evm/dist/index.js:833:29)
at async Object.handler (file:///Users/ferhat/Documents/eliza/packages/plugin-evm/dist/index.js:857:30)
at async AgentRuntime.processActions (file:///Users/ferhat/Documents/eliza/packages/core/dist/index.js:41355:17)
at async file:///Users/ferhat/Documents/eliza/packages/client-direct/dist/index.js:4814:13
Text is not JSON <analysis>1. Identifying relevant information from the user's message: - Chain: The message does not explicitly mention the chain to execute the transfer on. However, based on the context, it seems the transfer is happening on the Sepolia testnet. - Amount: The user requests a transfer of 0.0042069 ETH. - Recipient address: The user provides the address 0xD6D2772f96D8C383754e0e41C47C261C67F89a90. - Token: The message does not mention any specific token, so this is likely a native ETH transfer.2. Validating the information: - Chain: The chain "sepolia" is present in the list of supported chains. - Amount: The amount "0.0042069" can be successfully converted to a number. - Address: The provided address starts with "0x" and has 42 characters, which is a valid Ethereum address. - Token: Since no token is mentioned, this is a native ETH transfer.3. No information is missing or invalid, so no error message is needed.4. Summary: The user is requesting a transfer of 0.0042069 ETH on the Sepolia testnet to the address 0xD6D2772f96D8C383754e0e41C47C261C67F89a90.</analysis>{ "fromChain": "sepolia", "amount": "0.0042069", "toAddress": "0xD6D2772f96D8C383754e0e41C47C261C67F89a90", "token": null}
Transferring: 0.0042069 tokens to (0xD6D2772f96D8C383754e0e41C47C261C67F89a90 on sepolia)

You can also confirm transaction activity by looking at a block explorer.

There are a number of different actions available in the @elizaos/plugin-evm you can try such as swaps, DAO votes, and bridging.

Special thanks to the core developers of @elizaos/plugin-evm who made this plugin!

Want to learn how to build your own Eliza plugin? Leave some feedback at the end of this guide!

Build Something Cool

Now that you understand Eliza agents and plugins, try these project ideas:


  • Build a trading bot that analyzes X sentiment for market signals
  • Create a blockchain data streams plugin for real-time agent updates
  • Make a smart contract auditor that can scan for common vulnerabilities

Want to see one of these implemented? Drop some feedback below! Also, join our Discord community, we love seeing what builders create!

Final Thoughts

That's it! In this guide, we've found out that Eliza's framework makes it straightforward to create agents that can analyze on-chain data, execute trades, and interact with smart contracts autonomously.

If you have any questions or need help, feel free to reach out to us on our Discord or Twitter.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide