Build Your On-Chain AI Agent
Complete Step-by-Step Tutorial: On-chain AI Agent
This comprehensive tutorial will guide you through building an AI Agent that can handle both general conversations and blockchain operations, specifically token balance checking on the LazAI testnet.
Prerequisites
Before starting, ensure you have:
Node.js 18+ installed
npm or yarn package manager
Basic knowledge of React, TypeScript, and Next.js
OpenAI API key (for AI conversations)
Code editor (VS Code recommended)
Project Setup
Step 1: Create Next.js Project
# Create a new Next.js project with TypeScript
npx create-next-app@latest ai-agent --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"
# Navigate to the project directory
cd ai-agent
Step 2: Verify Project Structure
Your project should look like this:
ai-blockchain-chatbot/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
├── next.config.ts
├── package.json
└── tsconfig.json
Dependencies Installation
Step 3: Install Required Packages
# Install core dependencies
npm install ethers alith
# Install development dependencies
npm install --save-dev node-loader
What each package does:
ethers
: Ethereum library for blockchain interactionsalith
: AI SDK for OpenAI integrationnode-loader
: Webpack loader for native modules
Next.js Configuration
Step 4: Configure next.config.ts
Create or update next.config.ts
:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack: (config, { isServer }) => {
if (isServer) {
// On the server side, handle native modules
config.externals = config.externals || [];
config.externals.push({
'@lazai-labs/alith-darwin-arm64': 'commonjs @lazai-labs/alith-darwin-arm64',
});
} else {
// On the client side, don't bundle native modules
config.resolve.fallback = {
...config.resolve.fallback,
'@lazai-labs/alith-darwin-arm64': false,
'alith': false,
};
}
return config;
},
// Mark packages as external for server components
serverExternalPackages: ['@lazai-labs/alith-darwin-arm64', 'alith'],
};
export default nextConfig;
Why this configuration is needed:
Handles native modules that can't be bundled by webpack
Prevents client-side bundling of server-only packages
Ensures proper module resolution
Token Balance API
Step 5: Create API Directory Structure
# Create the API directories
mkdir -p app/api/token-balance
mkdir -p app/api/chat
mkdir -p app/components
Step 6: Create Token Balance API
Create app/api/token-balance/route.ts
:
import { NextRequest, NextResponse } from 'next/server';
import { ethers } from 'ethers';
// ERC-20 Token ABI (minimal for balance checking)
const ERC20_ABI = [
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
}
];
// LazAI Testnet configuration
const LAZAI_RPC = 'https://lazai-testnet.metisdevops.link';
const LAZAI_CHAIN_ID = 133718;
export async function POST(request: NextRequest) {
try {
const { contractAddress, walletAddress } = await request.json();
// Validate inputs
if (!contractAddress || !walletAddress) {
return NextResponse.json(
{ error: 'Contract address and wallet address are required' },
{ status: 400 }
);
}
// Validate Ethereum addresses
if (!ethers.isAddress(contractAddress)) {
return NextResponse.json(
{ error: 'Invalid contract address format' },
{ status: 400 }
);
}
if (!ethers.isAddress(walletAddress)) {
return NextResponse.json(
{ error: 'Invalid wallet address format' },
{ status: 400 }
);
}
// Connect to LazAI testnet
const provider = new ethers.JsonRpcProvider(LAZAI_RPC);
// Create contract instance
const contract = new ethers.Contract(contractAddress, ERC20_ABI, provider);
try {
// Get token information with individual error handling
let balance, decimals, symbol, name;
try {
balance = await contract.balanceOf(walletAddress);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to get token balance. Contract might not be a valid ERC-20 token.' },
{ status: 400 }
);
}
try {
decimals = await contract.decimals();
} catch (error) {
// If decimals call fails, assume 18 decimals (most common)
decimals = 18;
}
try {
symbol = await contract.symbol();
} catch (error) {
symbol = 'UNKNOWN';
}
try {
name = await contract.name();
} catch (error) {
name = 'Unknown Token';
}
// Format balance - convert BigInt to string first
const formattedBalance = ethers.formatUnits(balance.toString(), decimals);
// Get LAZAI balance for comparison
const lazaiBalance = await provider.getBalance(walletAddress);
const formattedLazaiBalance = ethers.formatEther(lazaiBalance.toString());
return NextResponse.json({
success: true,
data: {
tokenName: name,
tokenSymbol: symbol,
contractAddress: contractAddress,
walletAddress: walletAddress,
balance: formattedBalance,
rawBalance: balance.toString(), // Convert BigInt to string
decimals: Number(decimals), // Convert BigInt to number
lazaiBalance: formattedLazaiBalance,
network: {
name: 'LazAI Testnet',
chainId: LAZAI_CHAIN_ID,
rpc: LAZAI_RPC,
explorer: 'https://lazai-testnet-explorer.metisdevops.link'
}
}
});
} catch (contractError) {
console.error('Contract interaction error:', contractError);
return NextResponse.json(
{ error: 'Contract not found or not a valid ERC-20 token on LazAI testnet' },
{ status: 400 }
);
}
} catch (error) {
console.error('Error checking token balance:', error);
// Handle specific errors
if (error instanceof Error) {
if (error.message.includes('execution reverted')) {
return NextResponse.json(
{ error: 'Contract not found or not a valid ERC-20 token' },
{ status: 400 }
);
}
if (error.message.includes('network') || error.message.includes('connection')) {
return NextResponse.json(
{ error: 'Network connection failed. Please try again.' },
{ status: 500 }
);
}
}
return NextResponse.json(
{ error: 'Failed to check token balance' },
{ status: 500 }
);
}
}
Key Features:
Validates Ethereum addresses
Handles BigInt serialization
Provides fallback values for missing token data
Comprehensive error handling
Returns both token and native currency balances
Step 7: Create Smart Chat API
Create app/api/chat/route.ts
:
import { NextRequest, NextResponse } from 'next/server';
import { Agent } from 'alith';
// Function to detect token balance requests
function isTokenBalanceRequest(message: string): { isRequest: boolean; contractAddress?: string; walletAddress?: string } {
const lowerMessage = message.toLowerCase();
// Check for common patterns
const balancePatterns = [
/check.*balance/i,
/token.*balance/i,
/balance.*check/i,
/how much.*token/i,
/token.*amount/i
];
const hasBalanceIntent = balancePatterns.some(pattern => pattern.test(lowerMessage));
if (!hasBalanceIntent) {
return { isRequest: false };
}
// Extract Ethereum addresses (basic pattern)
const addressPattern = /0x[a-fA-F0-9]{40}/g;
const addresses = message.match(addressPattern);
if (!addresses || addresses.length < 2) {
return { isRequest: false };
}
// Assume first address is contract, second is wallet
return {
isRequest: true,
contractAddress: addresses[0],
walletAddress: addresses[1]
};
}
export async function POST(request: NextRequest) {
try {
const { message } = await request.json();
if (!message || typeof message !== 'string') {
return NextResponse.json(
{ error: 'Message is required and must be a string' },
{ status: 400 }
);
}
// Check if this is a token balance request
const balanceRequest = isTokenBalanceRequest(message);
if (balanceRequest.isRequest && balanceRequest.contractAddress && balanceRequest.walletAddress) {
// Route to token balance API
try {
const balanceResponse = await fetch(`${request.nextUrl.origin}/api/token-balance`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contractAddress: balanceRequest.contractAddress,
walletAddress: balanceRequest.walletAddress
}),
});
const balanceData = await balanceResponse.json();
if (balanceData.success) {
const formattedResponse = `🔍 **Token Balance Check Results**
**Token Information:**
• Name: ${balanceData.data.tokenName}
• Symbol: ${balanceData.data.tokenSymbol}
• Contract: \`${balanceData.data.contractAddress}\`
**Wallet Information:**
• Address: \`${balanceData.data.walletAddress}\`
• Token Balance: **${balanceData.data.balance} ${balanceData.data.tokenSymbol}**
• LAZAI Balance: **${balanceData.data.lazaiBalance} LAZAI**
**Network:** ${balanceData.data.network.name} (Chain ID: ${balanceData.data.network.chainId})
You can view this transaction on the [block explorer](${balanceData.data.network.explorer}/address/${balanceData.data.walletAddress}).`;
return NextResponse.json({ response: formattedResponse });
} else {
let errorMessage = `❌ **Error checking token balance:** ${balanceData.error}`;
// Provide helpful suggestions based on the error
if (balanceData.error.includes('not a valid ERC-20 token')) {
errorMessage += `\n\n💡 **Suggestions:**
• Make sure the contract address is a valid ERC-20 token on LazAI testnet
• Verify the contract exists and is deployed on the network
• Check if the contract implements the standard ERC-20 interface`;
} else if (balanceData.error.includes('Invalid contract address')) {
errorMessage += `\n\n💡 **Suggestion:** Please provide a valid Ethereum address starting with 0x followed by 40 hexadecimal characters.`;
} else if (balanceData.error.includes('Invalid wallet address')) {
errorMessage += `\n\n💡 **Suggestion:** Please provide a valid Ethereum wallet address starting with 0x followed by 40 hexadecimal characters.`;
}
return NextResponse.json({ response: errorMessage });
}
} catch (error) {
console.error('Error calling token balance API:', error);
return NextResponse.json({
response: "❌ **Error:** Failed to check token balance. Please try again later.\n\n💡 **Possible causes:**\n• Network connection issues\n• Invalid contract or wallet addresses\n• Contract not deployed on LazAI testnet"
});
}
}
// Check if API key is configured for AI responses
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json(
{ error: 'OpenAI API key is not configured' },
{ status: 500 }
);
}
// Initialize the Alith agent with enhanced preamble
const agent = new Agent({
model: "gpt-4",
preamble: `Your name is Alith. You are a helpful AI assistant with blockchain capabilities.
**Available Features:**
1. **Token Balance Checker**: Users can check ERC-20 token balances on the LazAI testnet by providing a contract address and wallet address. The format should include both addresses in the message.
**Network Information:**
- Network: LazAI Testnet
- Chain ID: 133718
- RPC: https://lazai-testnet.metisdevops.link
- Explorer: https://lazai-testnet-explorer.metisdevops.link
**How to use token balance checker:**
Users can ask questions like:
- "Check token balance for contract 0x... and wallet 0x..."
- "What's the balance of token 0x... in wallet 0x..."
- "Check balance: contract 0x... wallet 0x..."
Provide clear, concise, and accurate responses. Be friendly and engaging in your conversations. If users ask about token balances, guide them to provide both contract and wallet addresses.`,
});
// Get response from the agent
const response = await agent.prompt(message);
return NextResponse.json({ response });
} catch (error) {
console.error('Error in chat API:', error);
return NextResponse.json(
{ error: 'Failed to get response from AI' },
{ status: 500 }
);
}
}
Key Features:
Smart message routing based on content analysis
Pattern recognition for balance requests
Automatic address extraction
Enhanced AI prompts with blockchain context
Comprehensive error handling
Chat Interface Component
Step 8: Create Chat Interface
Create app/components/ChatInterface.tsx
:
'use client';
import { useState, useRef, useEffect } from 'react';
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: Date;
}
// Simple markdown renderer for basic formatting
const renderMarkdown = (text: string) => {
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">$1</code>')
.replace(/\n/g, '<br>')
.replace(/•/g, '•');
};
export default function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputMessage, setInputMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Auto-scroll to bottom when new messages are added
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSendMessage = async () => {
if (!inputMessage.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
content: inputMessage.trim(),
role: 'user',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInputMessage('');
setIsLoading(true);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: inputMessage.trim() }),
});
if (!response.ok) {
throw new Error('Failed to get response');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
content: data.response,
role: 'assistant',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Error getting response:', error);
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
content: 'Sorry, I encountered an error. Please try again.',
role: 'assistant',
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">Alith AI Assistant</h1>
<p className="text-sm text-gray-600">Powered by Alith SDK & ChatGPT • Blockchain Capabilities</p>
</div>
{/* Messages Container */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-gray-500 mt-8">
<div className="text-6xl mb-4">🤖</div>
<h3 className="text-lg font-medium mb-2">Welcome to Alith AI!</h3>
<p className="text-sm mb-4">I can help you with general questions and blockchain operations.</p>
{/* Feature showcase */}
<div className="bg-white rounded-lg p-4 max-w-md mx-auto border border-gray-200">
<h4 className="font-medium text-gray-900 mb-2">Available Features:</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li>• 💬 General AI conversations</li>
<li>• 🔍 Token balance checking on LazAI testnet</li>
<li>• 📊 Blockchain data queries</li>
</ul>
<div className="mt-3 p-2 bg-blue-50 rounded text-xs text-blue-700">
<strong>Example:</strong> "Check token balance for contract 0x1234... and wallet 0x5678..."
</div>
</div>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[70%] rounded-lg px-4 py-3 ${
message.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-900 border border-gray-200'
}`}
>
<div
className={`text-sm whitespace-pre-wrap ${
message.role === 'assistant' ? 'prose prose-sm max-w-none' : ''
}`}
dangerouslySetInnerHTML={{
__html: message.role === 'assistant'
? renderMarkdown(message.content)
: message.content
}}
/>
<div
className={`text-xs mt-2 ${
message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
}`}
>
{formatTime(message.timestamp)}
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white text-gray-900 border border-gray-200 rounded-lg px-4 py-3">
<div className="flex items-center space-x-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
<span className="text-sm text-gray-600">Alith is thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Container */}
<div className="bg-white border-t border-gray-200 px-6 py-4">
<div className="flex space-x-4">
<div className="flex-1">
<textarea
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ask me anything or check token balances..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows={1}
disabled={isLoading}
/>
</div>
<button
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isLoading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
</button>
</div>
{/* Quick help */}
<div className="mt-2 text-xs text-gray-500">
💡 <strong>Tip:</strong> Include both contract and wallet addresses to check token balances on LazAI testnet
</div>
</div>
</div>
);
}
Key Features:
Real-time message updates
Markdown rendering for formatted responses
Auto-scroll to latest messages
Loading indicators
Responsive design
Feature showcase for new users
Environment Setup
Step 9: Configure Environment Variables
Create a .env.local
file in your project root:
# OpenAI API Key for AI conversations
OPENAI_API_KEY=your_openai_api_key_here
Step 10: Get OpenAI API Key
Go to OpenAI Platform
Sign up or log in to your account
Navigate to "API Keys" in the sidebar
Click "Create new secret key"
Copy the generated key
Paste it in your
.env.local
file
Security Note: Never commit your API key to version control!
Testing
Step 11: Start Development Server
npm run dev
Step 12: Test General Chat
Try these general questions:
"What is blockchain technology?"
"How does cryptocurrency work?"
"Tell me a joke"
"What are the benefits of decentralized systems?"
Step 13: Test Token Balance Checking
Try these formats:
"Check token balance for contract 0x1234567890123456789012345678901234567890 and wallet 0x0987654321098765432109876543210987654321"
"What's the balance of token 0x... in wallet 0x..."
"Check balance: contract 0x... wallet 0x..."
Note: You'll need valid contract and wallet addresses on the LazAI testnet for this to work.
