“I think we’re going to live in a world where there are going to be hundreds of millions or billions of different AI agents. Eventually, probably, more AI agents than there are people in the world.”
— Mark Zuckerberg.
In recent months, AI agents seem to have struck a deep resonance with the ethos of web3. The crypto community has gone bonkers over innovative agents like aixbt, Clanker, El1za, Freysa AI, Botto, and countless others emerging daily. Platforms like Virtuals Protocol, Truth Terminal, and so on are opening doors to possibilities we've barely begun to imagine.
But what exactly are these onchain AI agents?
AI agents in crypto are autonomous, AI-powered systems designed to perform specific tasks within the blockchain ecosystem. These agents use LLMs and other ML models to analyze data, make decisions, and execute actions with minimal or no human intervention.
How are crypto AI Agents different from bots?
It's easy to confuse crypto AI agents with bots – after all, both automate tasks and help users. But the difference is fundamental.
Bots are deterministic, meaning they follow strict rules and scripts. Think of a trading bot that blindly buys when a token drops below $10, regardless of market conditions. No learning, no adapting – just following preset instructions.
AI agents, on the other hand, are probabilistic. They don't just follow rules – they learn from data, spot patterns, and adapt their strategies. When market conditions change, they adjust. When new trends emerge, they notice. This ability to learn and evolve makes them more intelligent assistants than simple automation tools.
What makes up the core architecture of Onchain AI Agents?
There are three main components in an AI agent’s architecture:
Data Input Layer: The data input layer forms the foundation of any AI agent - it's where all the magic begins. To gather blockchain data, agents connect directly to nodes or use web3 libraries like Viem, ethers.js, or web3.js. This gives them access to everything happening onchain - from real-time transactions to smart contract states. But blockchain data alone isn't enough. That's where oracles come in. By integrating with services like Chainlink, agents can pull in off-chain data such as market prices, social media sentiment, and other real-world information, giving them a complete view of both onchain and off-chain landscapes.
AI / ML Layer: The AI/ML layer uses LSTM networks for pattern spotting, Random Forests for predictions, and reinforcement learning for strategy optimization. These models train on historical data through backpropagation and Q-learning, while LLM wrappers (OpenAI's GPT or Anthropic's Claude) add market sentiment analysis and complex reasoning capabilities.
Blockchain Interaction Layer: Agents interact with Ethereum Virtual Machine (EVM) compatible smart contracts through ABI (Application Binary Interface). The use of libraries for transaction signing, gas estimation, and nonce management to ensure transactions are executed correctly on the blockchain, enabling the AI agent to take actions on behalf of the user on the blockchain.
How Onchain AI Agents work?
AI agents have three key parts that make them tick - Assistant, Thread, and Run.
Think of the Assistant as the brain, complete with its own personality and special functions to handle blockchain tasks.
The Thread is where conversations live. When you ask your agent to mint an NFT or deploy a contract or send a transaction over to the Blockchain, the thread understands what needs to be done and kicks off the action.
The Run is where it all comes together - your agent executes the blockchain tasks, monitors the progress, and sends back updates.
These three components work together, turning blockchain interactions into simple conversations.
Enough theory - Let's dive in
Pre-Requisites:
Bun Installed
OpenAI API Key (Get it here)
Reference Implementation: Check out the live repository alongside to better understand the codebase flow.
Setting up the Project and Dependencies:
Initialise the Project
Install Dependencies
Set up environment variables by creating a .env file in your root directory.
Creating the AI Agent Boilerplate:
Create these files in your project's src/openai
directory:
createAssistant.ts
import OpenAI from "openai"; import { Assistant } from "openai/resources/beta/assistants"; import { tools } from "../tools/allTools.js"; import { assistantPrompt } from "../const/prompt.js"; /** * Creates a new OpenAI assistant configured for blockchain interactions * @param client The OpenAI client instance to use * @returns Promise<Assistant> The created assistant instance * @custom * - Uses GPT-4 mini model * - Named "OnchainAIAgent" * - Configured with custom instructions from assistantPrompt * - Tools mapped from tools directory */ export async function createAssistant(client: OpenAI): Promise<Assistant> { return await client.beta.assistants.create({ model: "gpt-4o-mini", name: "OnchainAIAgent", instructions: assistantPrompt, tools: Object.values(tools).map((tool) => tool.definition), }); }
createThread.ts
import OpenAI from "openai"; import { Thread } from "openai/resources/beta/threads/threads"; /** * Creates a new OpenAI conversation thread and optionally adds an initial message * @param client The OpenAI client instance to use * @param message Optional initial message to add to the thread * @returns Promise<Thread> The created thread instance * @dev If a message is provided, it's added as a user message to the thread */ export async function createThread(client: OpenAI, message?: string): Promise<Thread> { const thread = await client.beta.threads.create(); if (message) { await client.beta.threads.messages.create(thread.id, { role: "user", content: message, }); } return thread; }
createRun.ts
import OpenAI from "openai"; import { Run } from "openai/resources/beta/threads/runs/runs"; import { Thread } from "openai/resources/beta/threads/threads"; /** * Creates and monitors a run for a given thread using the specified assistant. * The function polls the run status until completion. * @param client - The OpenAI client instance used to make API calls * @param thread - The thread object where the run will be created * @param assistantId - The ID of the assistant to use for this run * @returns A Promise that resolves to the completed Run object * @dev Polls every 1 second until the run is no longer in progress or queued */ export async function createRun(client: OpenAI, thread: Thread, assistantId: string): Promise<Run> { console.log(`Creating run with assistant ${assistantId} for thread ${thread.id}`); let run = await client.beta.threads.runs.create(thread.id, { assistant_id: assistantId }); while(run.status === 'in_progress' || run.status === 'queued'){ await new Promise(resolve => setTimeout(resolve, 1000)); run = await client.beta.threads.runs.retrieve(thread.id, run.id); } return run; }
handleRunToolCall.ts
import OpenAI from "openai"; import { Run } from "openai/resources/beta/threads/runs/runs"; import { Thread } from "openai/resources/beta/threads/threads"; import { tools } from "../tools/allTools.js"; /** * Processes and executes tool calls required by an assistant run. * @param run - The current Run instance containing tool calls to be executed * @param client - OpenAI client instance for API interactions * @param thread - The Thread instance associated with this run * @returns Promise resolving to an updated Run instance after tool execution * @dev Executes multiple tool calls in parallel using Promise.all * @notice If a tool is not found or execution fails, appropriate error handling is performed * @notice Tool outputs are automatically converted to strings before submission * @notice Empty or failed tool outputs are filtered out before submission */ export async function handleRunToolCalls( run: Run, client: OpenAI, thread: Thread ): Promise<Run> { const toolCalls = run.required_action?.submit_tool_outputs?.tool_calls; if (!toolCalls) return run; const toolOutputs = await Promise.all( toolCalls.map(async (tool) => { const toolConfig = tools[tool.function.name]; if (!toolConfig) { console.error(`Tool ${tool.function.name} Not Found!`); return null; } try { const args = JSON.parse(tool.function.arguments); const output = await toolConfig.handler(args); console.log(`Tool ${tool.function.name} returned ${output}`); return { tool_call_id: tool.id, output: String(output), }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { tool_call_id: tool.id, output: `Error: ${errorMessage}`, }; } }) ); const validOutputs = toolOutputs.filter( Boolean ) as OpenAI.Beta.Threads.Runs.RunSubmitToolOutputsParams.ToolOutput[]; if (validOutputs.length === 0) return run; return client.beta.threads.runs.submitToolOutputsAndPoll(thread.id, run.id, { tool_outputs: validOutputs, }); }
performRun.ts
import OpenAI from "openai"; import { Thread } from "openai/resources/beta/threads/threads"; import { Run } from "openai/resources/beta/threads/runs/runs"; import { handleRunToolCalls } from "./handleRunToolCall.js"; /** * Handles a run's execution lifecycle, including tool calls and error handling. * @param run - The current Run instance to be processed * @param client - OpenAI client instance for API interactions * @param thread - The Thread instance associated with this run * @returns Promise resolving to the assistant's message content or an error message object * @dev Continues processing tool calls until completion or failure * @notice If the run fails, creates an error message in the thread and returns it */ export async function performRun(run: Run, client: OpenAI, thread: Thread) { while (run.status === "requires_action") { run = await handleRunToolCalls(run, client, thread); } if (run.status === "failed") { const errorMessage = `I encountered an error: ${run.last_error?.message || `Unknown Error`}`; console.error("Run Failed:", run.last_error); await client.beta.threads.messages.create(thread.id, { role: 'assistant', content: errorMessage }); return { type: 'text', text: { value: errorMessage, annotations: [] } } } const messages = await client.beta.threads.messages.list(thread.id); const assistantMessage = messages.data.find(message => message.role === "assistant"); return assistantMessage?.content[0] || { type: "text", text: { value: "No Response from Assistant", annotations: [] } }; }
Creating Tools/Functions for the Assistant:
Create these files in your project's src/tools
directory:
allTools.ts
import { deployErc20Tool } from "./deployERC20Tool.js"; import { getBalanceTool } from "./getBalance.js"; export interface ToolConfig<T = any> { definition: { type: 'function'; function: { name: string; description: string; parameters: { type: 'object'; properties: Record<string, unknown>; required: string[]; }; }; }; handler: (args: T) => Promise<any>; }; export const tools: Record<string, ToolConfig> = { // Add Tools Here // READ get_balance: getBalanceTool, }
getBalance.ts
import { Address } from 'viem'; import { createViemPublicClient } from '../viem/createViemPublicClient.js'; import { ToolConfig } from './allTools.js'; import { formatEther } from 'viem'; interface GetBalanceArgs { wallet: Address; } export const getBalanceTool: ToolConfig<GetBalanceArgs> = { definition: { type: 'function', function: { name: 'get_balance', description: 'Get the balance of a wallet', parameters: { type: 'object', properties: { wallet: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', description: 'The wallet address to get the balance of', } }, required: ['wallet'] } } }, handler: async ({ wallet }) => { return await getBalance(wallet); } }; async function getBalance(wallet: Address) { const publicClient = createViemPublicClient(); const balance = await publicClient.getBalance({ address: wallet }); return formatEther(balance); }
Bringing It All Together: Building Our Entry Point:
import 'dotenv/config';
import OpenAI from "openai";
import readline from 'readline';
import { createAssistant } from './openai/createAssistant.js';
import { createThread } from './openai/createThread.js';
import { createRun } from './openai/createRun.js';
import { performRun } from './openai/performRun.js';
import { Thread } from 'openai/resources/beta/threads/threads';
import { Assistant } from 'openai/resources/beta/assistants';
const client = new OpenAI();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (query: string): Promise<string> => {
return new Promise((resolve) => rl.question(query, resolve));
};
async function chat(thread: Thread, assistant: Assistant): Promise<void> {
while (true) {
const userInput = await question('\nYou: ');
if (userInput.toLowerCase() === 'exit') {
rl.close();
break;
}
try {
await client.beta.threads.messages.create(thread.id, {
role: "user",
content: userInput
});
const run = await createRun(client, thread, assistant.id);
const result = await performRun(run, client, thread);
if (result?.type === 'text') {
console.log('\nAlt:', result.text.value);
}
} catch (error) {
console.error('Error during chat:', error instanceof Error ? error.message : 'Unknown error');
rl.close();
break;
}
}
}
async function main(): Promise<void> {
try {
const assistant = await createAssistant(client);
const thread = await createThread(client);
console.log('Chat started! Type "exit" to end the conversation.');
await chat(thread, assistant);
} catch (error) {
console.error('Error in main:', error instanceof Error ? error.message : 'Unknown error');
rl.close();
process.exit(1);
}
}
main().catch((error) => {
console.error('Unhandled error:', error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
});
Running Your AI Agent:
Spin up your agent with a simple command:bun run index.ts
Here's what it looks like in action:
Additional Resources and Ideas:
Dive deeper into AI agents with these resources:
Agent Development Kits
Tutorials
Community Insights
That's it! If you've followed me through this entire guide, it's now your turn to go out and build your own AI Agents.
The repository has some cool tools ready for you to test out. Feel free to try them and contribute if you create something interesting.
Always be shilling — Make sure you share this post on Twitter/X, and contribute to the repo if you create something cool.
Go share it with your friends — who knows, it might help them create their first AI agent!
This piece was hugely inspired by the "Build Your Own Onchain AI Agent!" video by Jarrod Watts.
Feel free to reach out to me on Twitter or GitHub if you need help or want to contribute!