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 TypeScriptnpxcreate-next-app@latestai-agent--typescript--tailwind--eslint--app--src-dir=false--import-alias="@/*"# Navigate to the project directorycdai-agent
Step 2: Verify Project Structure
Your project should look like this:
Dependencies Installation
Step 3: Install Required Packages
What each package does:
ethers: Ethereum library for blockchain interactions
alith: AI SDK for OpenAI integration
node-loader: Webpack loader for native modules
Next.js Configuration
Step 4: Configure next.config.ts
Create or update next.config.ts:
Why this configuration is needed:
Handles native modules that can't be bundled by webpack
Prevents client-side bundling of server-only packages
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;
# Create the API directories
mkdir -p app/api/token-balance
mkdir -p app/api/chat
mkdir -p app/components
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://testnet.lazai.network';
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://testnet-explorer.lazai.network'
}
}
});
} 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 }
);
}
}
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://testnet.lazai.network
- Explorer: https://testnet-explorer.lazai.network
**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 }
);
}
}