Building an AI-Powered E-commerce Chatbot:From Concept to Production
Learn how to build a conversational shopping assistant using Vercel AI SDK and Claude. This comprehensive guide covers the complete implementation of an AI chatbot that handles user registration, product discovery, checkout, and payment processing - all through natural conversation. Includes database schema, API implementation, React components, and deployment strategies.
AI-Powered E-commerce Chatbot - Complete Implementation Guide
📋 App Overview
Name: ChatShop Assistant
Concept: A conversational AI shopping assistant built with Vercel AI SDK's Agent architecture that guides customers through the entire purchase journey - from product discovery to payment - using natural language chat. The agent autonomously handles multi-step workflows, making intelligent decisions about tool usage while maintaining conversation context.
Key Value Propositions:
- Intelligent Agent Workflow: Uses AI SDK's Agent class for autonomous multi-step reasoning
- Dynamic Tool Orchestration: Agent decides which tools to use and when, without hardcoded flows
- Context-Aware Shopping: Maintains conversation state across the entire purchase journey
- Structured Outputs: Type-safe responses with guaranteed data schemas
- Production-Ready: Built-in loop control, error handling, and token management
🏗️ Technical Architecture
Stack Recommendation
Frontend:
- Next.js 15+ (App Router) - for the web interface
- React 19 - UI components
- Tailwind CSS - styling
- Vercel AI SDK (
aipackage) - Agent architecture anduseChathook - TypeScript - end-to-end type safety
Backend:
- Next.js API Routes - backend logic
- Vercel AI SDK Agent Class - autonomous agent orchestration
- Anthropic Claude Sonnet 4.5 (or OpenAI GPT-4) - the AI brain
- PostgreSQL - database (users, products, orders, sessions)
- Prisma - ORM with full TypeScript support
- Stripe - payment processing
- Resend - transactional emails
Hosting:
- Vercel - deployment (frontend + API routes)
- Neon - serverless PostgreSQL
🗄️ Database Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User
model User {
id String @id @default(cuid())
name String
email String @unique
phone String
createdAt DateTime @default(now())
orders Order[]
sessions Session[]
}
// Product
model Product {
id String @id @default(cuid())
name String
description String
price Decimal @db.Decimal(10, 2)
category String
imageUrl String
stock Int
tags String[] // For better search matching
createdAt DateTime @default(now())
orderItems OrderItem[]
@@index([category])
@@index([name])
}
// Session (tracks agent conversation state)
model Session {
id String @id @default(cuid())
userId String?
user User? @relation(fields: [userId], references: [id])
stage String @default("registration") // registration, product_discovery, selection, checkout, payment, completed
selectedProducts Json @default("[]") // Array of {productId, quantity}
location String?
totalAmount Decimal? @db.Decimal(10, 2)
conversationHistory Json @default("[]") // Store messages for context
metadata Json @default("{}") // Flexible field for additional data
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// Order
model Order {
id String @id @default(cuid())
orderNumber String @unique @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
totalAmount Decimal @db.Decimal(10, 2)
location String
status String @default("pending") // pending, paid, processing, shipped, delivered, cancelled
paymentId String? // Stripe/Paystack payment ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
@@index([userId])
@@index([status])
}
// OrderItem
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
quantity Int @default(1)
price Decimal @db.Decimal(10, 2) // Price at time of purchase
@@index([orderId])
@@index([productId])
}🤖 Agent Architecture with AI SDK
Why Use the Agent Class?
Instead of manually managing conversation state and tool calls, the AI SDK's Agent class provides:
- Autonomous Decision-Making: Agent decides which tools to use and when
- Multi-Step Reasoning: Automatically handles tool call loops
- Type Safety: Full TypeScript inference for tools and outputs
- Built-in Loop Control: Configure stopping conditions and step limits
- Reusability: Define once, use across multiple routes
🔧 Implementation - Step by Step
Phase 1: Project Setup
Step 1.1: Initialize Next.js Project
pnpm create next-app@latest chatshop-assistant --typescript --tailwind --app
cd chatshop-assistant
# Install AI SDK and dependencies
npm install ai @ai-sdk/anthropic @ai-sdk/openai zod
npm install prisma @prisma/client stripe resend
npm install -D @types/node tsx
Step 1.2: Setup Database
# Initialize Prisma
npx prisma init
# Copy the schema from above into prisma/schema.prisma
# Create and apply migration
npx prisma migrate dev --name init
# Generate Prisma Client
npx prisma generateStep 1.3: Environment Variables
# .env.local
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/chatshop"
# AI Provider (choose one)
ANTHROPIC_API_KEY="sk-ant-..."
# OR
OPENAI_API_KEY="sk-..."
# Payment
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Email
RESEND_API_KEY="re_..."
# App
NEXT_PUBLIC_APP_URL="http://localhost:3000"Step 1.4: Create Prisma Client Singleton
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;Phase 2: Define the Shopping Agent
Step 2.1: Create Agent Tools
// lib/agent/tools.ts
import { tool } from "ai";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
import { Resend } from "resend";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
});
const resend = new Resend(process.env.RESEND_API_KEY);
export const registerUserTool = tool({
description:
"Register a new user with their name, email, and phone number. Call this first when a user provides their registration details.",
parameters: z.object({
name: z.string().describe("Full name of the user"),
email: z.string().email().describe("Email address"),
phone: z.string().describe("Phone number"),
sessionId: z.string().describe("Current session ID"),
}),
execute: async ({ name, email, phone, sessionId }) => {
try {
// Check if user exists
let user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
user = await prisma.user.create({
data: { name, email, phone },
});
}
// Update session with user
await prisma.session.update({
where: { id: sessionId },
data: {
userId: user.id,
stage: "product_discovery",
},
});
return {
success: true,
userId: user.id,
message: `Welcome ${name}! You're all set. What product are you looking for today?`,
};
} catch (error) {
return {
success: false,
error: "Failed to register user. Please try again.",
};
}
},
});
export const searchProductsTool = tool({
description:
"Search for products based on user query. Always returns exactly 4 products. Use this when user describes what they want to buy.",
parameters: z.object({
query: z.string().describe("Search query from user description"),
category: z.string().optional().describe("Optional category filter"),
}),
execute: async ({ query, category }) => {
try {
const products = await prisma.product.findMany({
where: {
AND: [
{
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
{ tags: { hasSome: [query.toLowerCase()] } },
],
},
category ? { category } : {},
],
stock: { gt: 0 }, // Only in-stock products
},
take: 4,
orderBy: { createdAt: "desc" },
});
if (products.length === 0) {
return {
success: false,
message:
"I couldn't find any products matching your search. Could you describe what you're looking for differently?",
};
}
return {
success: true,
products: products.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
price: p.price.toString(),
imageUrl: p.imageUrl,
stock: p.stock,
})),
message: `I found ${products.length} great options for you!`,
};
} catch (error) {
return {
success: false,
error: "Failed to search products. Please try again.",
};
}
},
});
export const saveProductSelectionTool = tool({
description:
"Save the products user has selected (1-2 products max). Call this after user confirms their selection.",
parameters: z.object({
sessionId: z.string().describe("Current session ID"),
productIds: z
.array(z.string())
.min(1)
.max(2)
.describe("Array of selected product IDs"),
}),
execute: async ({ sessionId, productIds }) => {
try {
// Get products with current prices
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});
if (products.length !== productIds.length) {
return {
success: false,
error: "One or more selected products are not available.",
};
}
const total = products.reduce((sum, p) => sum + Number(p.price), 0);
// Update session
await prisma.session.update({
where: { id: sessionId },
data: {
selectedProducts: productIds.map((id) => ({
productId: id,
quantity: 1,
})),
totalAmount: total,
stage: "checkout",
},
});
return {
success: true,
products: products.map((p) => ({
name: p.name,
price: p.price.toString(),
})),
total: total.toFixed(2),
message: "Great choice! Where would you like these delivered?",
};
} catch (error) {
return {
success: false,
error: "Failed to save selection. Please try again.",
};
}
},
});
export const saveDeliveryLocationTool = tool({
description: "Save the delivery address provided by the user.",
parameters: z.object({
sessionId: z.string().describe("Current session ID"),
location: z.string().describe("Full delivery address"),
}),
execute: async ({ sessionId, location }) => {
try {
const session = await prisma.session.update({
where: { id: sessionId },
data: {
location,
stage: "payment",
},
include: {
user: true,
},
});
return {
success: true,
location,
totalAmount: session.totalAmount?.toString(),
userName: session.user?.name,
message: "Perfect! Ready to confirm your order?",
};
} catch (error) {
return {
success: false,
error: "Failed to save location. Please try again.",
};
}
},
});
export const createPaymentIntentTool = tool({
description:
"Create a payment intent for the order. Call this when user confirms they want to proceed with payment.",
parameters: z.object({
sessionId: z.string().describe("Current session ID"),
}),
execute: async ({ sessionId }) => {
try {
const session = await prisma.session.findUnique({
where: { id: sessionId },
});
if (!session?.totalAmount) {
return {
success: false,
error: "No order total found. Please start over.",
};
}
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(Number(session.totalAmount) * 100), // Convert to cents
currency: "usd",
metadata: {
sessionId,
userId: session.userId!,
},
automatic_payment_methods: { enabled: true },
});
return {
success: true,
clientSecret: paymentIntent.client_secret,
amount: session.totalAmount.toString(),
};
} catch (error) {
return {
success: false,
error: "Failed to create payment. Please try again.",
};
}
},
});
export const createOrderTool = tool({
description:
"Create the final order after successful payment. This should be called after payment is confirmed.",
parameters: z.object({
sessionId: z.string().describe("Current session ID"),
paymentId: z.string().describe("Stripe payment intent ID"),
}),
execute: async ({ sessionId, paymentId }) => {
try {
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { user: true },
});
if (!session?.userId || !session.location) {
return {
success: false,
error: "Missing order information. Please start over.",
};
}
const selectedProducts = session.selectedProducts as Array<{
productId: string;
quantity: number;
}>;
const products = await prisma.product.findMany({
where: { id: { in: selectedProducts.map((p) => p.productId) } },
});
// Create order with items
const order = await prisma.order.create({
data: {
userId: session.userId,
totalAmount: session.totalAmount!,
location: session.location,
status: "paid",
paymentId,
items: {
create: products.map((p) => ({
productId: p.id,
quantity: 1,
price: p.price,
})),
},
},
include: {
items: {
include: { product: true },
},
},
});
// Send confirmation email
await resend.emails.send({
from: "ChatShop <orders@chatshop.example.com>",
to: session.user!.email,
subject: `Order Confirmation #${order.orderNumber}`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #2563eb;">Thank you for your order!</h1>
<p>Hi ${session.user!.name},</p>
<p>Your order has been confirmed and is being processed.</p>
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h2 style="margin-top: 0;">Order Details</h2>
<p><strong>Order Number:</strong> ${order.orderNumber}</p>
<p><strong>Total:</strong> $${order.totalAmount}</p>
<p><strong>Delivery Address:</strong><br>${order.location}</p>
</div>
<h3>Items Ordered:</h3>
<ul>
${order.items
.map(
(item) => `
<li>
<strong>${item.product.name}</strong><br>
Quantity: ${item.quantity} × $${item.price}
</li>
`
)
.join("")}
</ul>
<p>We'll send you another email when your order ships.</p>
<p>Thanks for shopping with ChatShop!</p>
</div>
`,
});
// Mark session as completed
await prisma.session.update({
where: { id: sessionId },
data: { stage: "completed" },
});
return {
success: true,
order: {
orderNumber: order.orderNumber,
total: order.totalAmount.toString(),
items: order.items.map((item) => ({
name: item.product.name,
price: item.price.toString(),
quantity: item.quantity,
})),
},
message: `🎉 Order ${order.orderNumber} created successfully! Check your email for confirmation.`,
};
} catch (error) {
console.error("Order creation error:", error);
return {
success: false,
error: "Failed to create order. Please contact support.",
};
}
},
});Step 2.2: Create the Shopping Agent
// lib/agent/shopping-agent.ts
import { Experimental_Agent as Agent, stepCountIs } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import {
registerUserTool,
searchProductsTool,
saveProductSelectionTool,
saveDeliveryLocationTool,
createPaymentIntentTool,
createOrderTool,
} from './tools';
export const shoppingAgent = new Agent({
model: anthropic('claude-sonnet-4-20250514'),
system: `You are ChatShop Assistant, a friendly and efficient AI shopping companion.
Your goal is to guide customers through a complete purchase journey:
1. **REGISTRATION PHASE**:
- Greet the user warmly
- Collect their name, email, and phone number
- Call registerUserTool once you have all three pieces of information
2. **PRODUCT DISCOVERY PHASE**:
- Ask what they're looking for
- Use searchProductsTool to find 4 relevant products
- Present products clearly with name, description, and price
- Let them know they can choose 1-2 products
3. **SELECTION PHASE**:
- Help them choose between options
- Once they decide, call saveProductSelectionTool
- Show the selected products with total price
4. **CHECKOUT PHASE**:
- Ask for their delivery address
- Call saveDeliveryLocationTool
- Confirm the order details (items, total, address)
5. **PAYMENT PHASE**:
- When they confirm, call createPaymentIntentTool
- Guide them through payment
- After payment succeeds, call createOrderTool
6. **COMPLETION PHASE**:
- Share order confirmation details
- Thank them and mention the confirmation email
- Offer to start a new order if they want
**Important Guidelines**:
- Be conversational and friendly, not robotic
- Only move forward when you have the required information
- Always confirm before moving to the next phase
- Present products in a clear, scannable format
- Keep responses concise but helpful
- If a tool call fails, explain the issue and suggest next steps
- Never make up product details or prices
- Always pass the sessionId to tools that require it`,
tools: {
registerUser: registerUserTool,
searchProducts: searchProductsTool,
saveSelection: saveProductSelectionTool,
saveLocation: saveDeliveryLocationTool,
createPayment: createPaymentIntentTool,
createOrder: createOrderTool,
},
// Allow up to 15 steps for the complete workflow
stopWhen: stepCountIs(15),
// Optional: Dynamic behavior based on conversation progress
prepareStep: async ({ stepNumber, messages }) => {
// After 10 steps, provide additional context
if (stepNumber === 10) {
return {
system: `You've been helping this customer for a while.
Check if they're still engaged or need assistance completing their order.
Be proactive in resolving any concerns.`,
};
}
// Manage context window for long conversations
if (messages.length > 20) {
return {
messages: [
messages[0], // Keep system message
...messages.slice(-15), // Keep last 15 messages
],
};
}
return {}; // Use default settings
},
});
// Export the inferred message type for use in components
export type ShoppingAgentMessage = typeof shoppingAgent extends Agent
infer _Model,
infer _Tools,
infer _Options
>
? Experimental_InferAgentUIMessage<typeof shoppingAgent>
: never;Phase 3: Create API Route
Step 3.1: Chat API with Agent
// app/api/chat/route.ts
import { shoppingAgent } from "@/lib/agent/shopping-agent";
import { prisma } from "@/lib/prisma";
export const maxDuration = 60; // Allow up to 60 seconds for complex workflows
export async function POST(request: Request) {
try {
const { messages, sessionId } = await request.json();
// Ensure session exists
let session = await prisma.session.findUnique({
where: { id: sessionId },
});
if (!session) {
// Create new session
session = await prisma.session.create({
data: {
id: sessionId,
stage: "registration",
},
});
}
// Use the agent's respond method for streaming responses
// The agent will autonomously decide which tools to call
return shoppingAgent.respond({
messages,
});
} catch (error) {
console.error("Chat API error:", error);
return new Response(
JSON.stringify({ error: "Failed to process message" }),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}Phase 4: Build Frontend
Step 4.1: Chat Interface with Type Safety
// app/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useEffect, useState } from 'react';
import { Send, ShoppingBag, Sparkles } from 'lucide-react';
import type { ShoppingAgentMessage } from '@/lib/agent/shopping-agent';
export default function ChatShop() {
const [sessionId, setSessionId] = useState<string>('');
useEffect(() => {
// Generate or retrieve session ID
const storedSession = localStorage.getItem('chatshop-session');
if (storedSession) {
setSessionId(storedSession);
} else {
const newSession = crypto.randomUUID();
setSessionId(newSession);
localStorage.setItem('chatshop-session', newSession);
}
}, []);
const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
useChat<ShoppingAgentMessage>({
api: '/api/chat',
body: { sessionId },
onError: error => {
console.error('Chat error:', error);
},
});
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
handleSubmit(e);
};
return (
<div className="flex flex-col h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center">
<ShoppingBag className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900">
ChatShop Assistant
</h1>
<p className="text-sm text-gray-600">
Your AI shopping companion
</p>
</div>
</div>
<button
onClick={() => {
localStorage.removeItem('chatshop-session');
window.location.reload();
}}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Start Over
</button>
</div>
</header>
{/* Messages */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto px-4 py-6 space-y-6">
{messages.length === 0 && (
<div className="text-center py-12">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Sparkles className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Welcome to ChatShop!
</h2>
<p className="text-gray-600 max-w-md mx-auto">
I'm your personal shopping assistant. Let's get started by
getting to know you, then I'll help you find exactly what you
need.
</p>
</div>
)}
{messages.map(message => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[85%] rounded-2xl px-6 py-4 ${
message.role === 'user'
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg'
: 'bg-white text-gray-900 shadow-md border border-gray-100'
}`}
>
<div className="whitespace-pre-wrap break-words">
{message.content}
</div>
{/* Tool calls visualization (optional) */}
{message.toolInvocations?.map((tool, idx) => (
<div
key={idx}
className="mt-3 pt-3 border-t border-gray-200 text-sm"
>
<div className="flex items-center gap-2 text-gray-600">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span className="font-medium">
{tool.toolName === 'searchProducts' && 'Searching products...'}
{tool.toolName === 'registerUser' && 'Registering user...'}
{tool.toolName === 'saveSelection' && 'Saving selection...'}
{tool.toolName === 'createOrder' && 'Creating order...'}
</span>
</div>
</div>
))}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white rounded-2xl px-6 py-4 shadow-md border border-gray-100">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" />
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
/>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.4s' }}
/>
</div>
</div>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-red-800">
<p className="font-medium">Something went wrong</p>
<p className="text-sm mt-1">{error.message}</p>
</div>
)}
</div>
</div>
{/* Input */}
<div className="border-t border-gray-200 bg-white">
<form
onSubmit={handleFormSubmit}
className="max-w-4xl mx-auto px-4 py-4"
>
<div className="flex gap-3">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
disabled={isLoading}
className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl hover:from-blue-700 hover:to-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg hover:shadow-xl flex items-center gap-2 font-medium"
>
<Send className="w-4 h-4" />
Send
</button>
</div>
</form>
</div>
</div>
);
}Phase 5: Seed Database & Test
Step 5.1: Seed Products
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
console.log("Seeding database...");
const products = [
{
name: "Wireless Noise-Cancelling Headphones",
description:
"Premium over-ear headphones with active noise cancellation, 30-hour battery life, and superior sound quality.",
price: 299.99,
category: "Electronics",
imageUrl: "/products/headphones.jpg",
stock: 45,
tags: ["headphones", "audio", "wireless", "noise-cancelling"],
},
{
name: 'Ultra HD 4K Smart TV 55"',
description:
"55-inch 4K UHD Smart TV with HDR, built-in streaming apps, and voice control.",
price: 699.99,
category: "Electronics",
imageUrl: "/products/tv.jpg",
stock: 20,
tags: ["tv", "smart tv", "4k", "entertainment"],
},
{
name: "Professional Running Shoes",
description:
"Lightweight running shoes with advanced cushioning, breathable mesh upper, and superior grip.",
price: 129.99,
category: "Sports",
imageUrl: "/products/running-shoes.jpg",
stock: 100,
tags: ["shoes", "running", "sports", "fitness"],
},
{
name: "Stainless Steel Coffee Maker",
description:
"Programmable 12-cup coffee maker with thermal carafe, auto-brew timer, and brew strength selector.",
price: 89.99,
category: "Home",
imageUrl: "/products/coffee-maker.jpg",
stock: 35,
tags: ["coffee", "kitchen", "appliance"],
},
{
name: "Yoga Mat Premium",
description:
"Extra thick 6mm yoga mat with non-slip surface, carrying strap, and eco-friendly materials.",
price: 39.99,
category: "Sports",
imageUrl: "/products/yoga-mat.jpg",
stock: 75,
tags: ["yoga", "fitness", "exercise", "mat"],
},
{
name: "Wireless Gaming Mouse",
description:
"High-precision gaming mouse with 16000 DPI, programmable buttons, and RGB lighting.",
price: 79.99,
category: "Electronics",
imageUrl: "/products/gaming-mouse.jpg",
stock: 60,
tags: ["mouse", "gaming", "wireless", "rgb"],
},
{
name: "Portable Bluetooth Speaker",
description:
"Waterproof portable speaker with 360° sound, 20-hour battery, and powerful bass.",
price: 119.99,
category: "Electronics",
imageUrl: "/products/bluetooth-speaker.jpg",
stock: 50,
tags: ["speaker", "bluetooth", "portable", "waterproof"],
},
{
name: "Ergonomic Office Chair",
description:
"Adjustable office chair with lumbar support, breathable mesh back, and tilt mechanism.",
price: 249.99,
category: "Furniture",
imageUrl: "/products/office-chair.jpg",
stock: 30,
tags: ["chair", "office", "ergonomic", "furniture"],
},
{
name: "Smart Watch Fitness Tracker",
description:
"Advanced smartwatch with heart rate monitoring, GPS, sleep tracking, and 7-day battery life.",
price: 199.99,
category: "Electronics",
imageUrl: "/products/smartwatch.jpg",
stock: 55,
tags: ["smartwatch", "fitness", "tracker", "wearable"],
},
{
name: "Stainless Steel Water Bottle",
description:
"Insulated 32oz water bottle that keeps drinks cold for 24 hours or hot for 12 hours.",
price: 29.99,
category: "Sports",
imageUrl: "/products/water-bottle.jpg",
stock: 120,
tags: ["water bottle", "insulated", "sports", "hydration"],
},
{
name: "LED Desk Lamp",
description:
"Adjustable LED desk lamp with touch control, USB charging port, and 5 brightness levels.",
price: 44.99,
category: "Home",
imageUrl: "/products/desk-lamp.jpg",
stock: 40,
tags: ["lamp", "led", "desk", "lighting"],
},
{
name: "Wireless Mechanical Keyboard",
description:
"RGB mechanical keyboard with tactile switches, wireless connectivity, and aluminum frame.",
price: 149.99,
category: "Electronics",
imageUrl: "/products/keyboard.jpg",
stock: 45,
tags: ["keyboard", "mechanical", "wireless", "gaming"],
},
];
for (const product of products) {
await prisma.product.create({
data: product,
});
}
console.log(`✅ Created ${products.length} products`);
}
main()
.catch((e) => {
console.error("Error seeding database:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});Add seed script to package.json:
{
"scripts": {
"seed": "tsx prisma/seed.ts"
}
}Run the seed:
pnpm seed
🎯 How the Agent Works
Autonomous Workflow Example
Let's trace how the agent handles a complete purchase:
1. User starts conversation:
User: "Hi"
Agent's autonomous reasoning:
- No tools called yet (registration needed)
- Responds conversationally asking for registration details
2. User provides details:
User: "I'm John Doe, email john@example.com, phone 0700123456"
Agent autonomously:
- Recognizes all registration data is present
- Calls
registerUsertool with extracted info - Receives success response
- Transitions to product discovery phase
3. User describes need:
User: "I need headphones for work"
Agent autonomously:
- Calls
searchProductstool with query "headphones work" - Receives 4 products
- Presents them clearly with formatting
- Asks user to choose
4. User selects:
User: "I'll take the wireless noise-cancelling ones"
Agent autonomously:
- Identifies which product was selected
- Calls
saveSelectiontool with product ID - Receives total price
- Asks for delivery location
5. And so on...
The agent continues making autonomous decisions about:
- Which tool to call next
- What information to extract from user messages
- When to ask clarifying questions
- How to format responses
Key Agent Features in Action
Loop Control:
stopWhen: stepCountIs(15);- Allows up to 15 tool calls in sequence
- Prevents infinite loops
- Agent can search products → save selection → get location → create payment → create order all autonomously
Dynamic Behavior:
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber === 10) {
// Inject helpful guidance after many steps
return { system: "..." };
}
return {};
};- Adapts system prompt based on progress
- Manages context window for long conversations
Type Safety:
const { messages } = useChat<ShoppingAgentMessage>();- Full TypeScript inference from agent to UI
- Catch errors at compile time
- Autocomplete for tool results
🚀 Deployment Guide
Step 1: Prepare for Production
# Build the application
npm run build
# Test production build locally
npm startStep 2: Deploy to Vercel
# Install Vercel CLI
npm i -g vercel
# Login
vercel login
# Deploy
vercel --prodOr use Vercel Dashboard:
- Push code to GitHub
- Import repository in Vercel
- Add environment variables
- Deploy
Step 3: Setup Production Database
Option A: Supabase
- Create project at supabase.com
- Copy connection string
- Add to Vercel environment variables
- Run migrations:
npx prisma migrate deploy
Option B: Neon
- Create database at neon.tech
- Copy connection string
- Add to Vercel environment variables
- Run migrations
Step 4: Configure Webhooks
For Stripe payment confirmations:
// app/api/webhooks/stripe/route.ts
import { headers } from "next/headers";
import { prisma } from "@/lib/prisma";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
return new Response("Webhook signature verification failed", {
status: 400,
});
}
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
// Update order status
await prisma.order.updateMany({
where: { paymentId: paymentIntent.id },
data: { status: "processing" },
});
}
return new Response(JSON.stringify({ received: true }), { status: 200 });
}📊 Monitoring & Analytics
Add Logging
// lib/agent/shopping-agent.ts
export const shoppingAgent = new Agent({
// ... existing config
onStepFinish: async ({ step, stepNumber }) => {
// Log each step for debugging
console.log(`Step ${stepNumber}:`, {
toolCalls: step.toolCalls?.map((t) => t.toolName),
text: step.text?.substring(0, 100),
usage: step.usage,
});
// Send to analytics service
// await analytics.track('agent_step', { ... });
},
});Track Conversions
// In createOrderTool execute function
await analytics.track("order_completed", {
orderId: order.id,
userId: session.userId,
total: order.totalAmount,
itemCount: order.items.length,
});🔄 Advanced Patterns
1. Multi-Agent Orchestration
For complex workflows, create specialized agents:
// lib/agent/product-recommendation-agent.ts
export const recommendationAgent = new Agent({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You are an expert product recommendation specialist...',
tools: { analyzePreferences, findSimilarProducts },
});
// lib/agent/customer-support-agent.ts
export const supportAgent = new Agent({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You handle customer support queries...',
tools: { checkOrderStatus, processReturn, escalateToHuman },
});
// Main orchestrator routes to specialized agents
export const orchestratorAgent = new Agent({
model: anthropic('claude-sonnet-4-20250514'),
system: 'Route requests to appropriate specialized agents...',
tools: {
delegateToRecommendation: ...,
delegateToSupport: ...,
delegateToShopping: ...,
},
});2. Evaluation Loop for Quality
Add quality checks:
export const shoppingAgent = new Agent({
// ... config
prepareStep: async ({ messages, stepNumber }) => {
// Every 5 steps, evaluate conversation quality
if (stepNumber % 5 === 0) {
const lastMessages = messages.slice(-5);
// Use a separate model call to evaluate
const evaluation = await generateObject({
model: anthropic("claude-sonnet-4-20250514"),
schema: z.object({
userSatisfaction: z.number().min(1).max(10),
needsClarification: z.boolean(),
suggestedAction: z.string(),
}),
prompt: `Evaluate this conversation: ${JSON.stringify(lastMessages)}`,
});
if (evaluation.object.userSatisfaction < 7) {
return {
system: `User seems dissatisfied. ${evaluation.object.suggestedAction}`,
};
}
}
return {};
},
});3. Parallel Product Analysis
// tools.ts - Enhanced search with parallel analysis
export const searchProductsWithAnalysisTool = tool({
description: "Search and analyze products in parallel",
parameters: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
// Search products
const products = await prisma.product.findMany({
where: {
/* search criteria */
},
take: 4,
});
// Analyze each product in parallel
const analyses = await Promise.all(
products.map(async (product) => {
const analysis = await generateObject({
model: anthropic("claude-sonnet-4-20250514"),
schema: z.object({
strengths: z.array(z.string()),
bestFor: z.string(),
valueRating: z.number().min(1).max(10),
}),
prompt: `Analyze this product: ${JSON.stringify(product)}`,
});
return {
...product,
analysis: analysis.object,
};
})
);
return { products: analyses };
},
});🎓 Key Concepts Summary
Tools vs Agents
Tools (What we built):
- Individual functions the AI can call
registerUserTool,searchProductsTool, etc.- Reactive - called when needed
Agent (What orchestrates):
- The
Agentclass that decides which tools to use - Autonomous - makes multi-step decisions
- Maintains conversation context
- Handles the complete workflow
Agent Loop Control
stopWhen: stepCountIs(15); // Max 15 tool calls
prepareStep: async ({ stepNumber }) => {
// Modify behavior between steps
};Workflow Patterns Used
- Sequential Processing: Registration → Discovery → Selection → Checkout → Payment → Order
- Routing: Agent decides which tool to call based on conversation stage
- Loop Control: Autonomous multi-step execution with stopping conditions
🔮 Future Enhancements
- Voice Integration: Add speech-to-text for voice orders
- Image Search: Upload product images to find similar items
- Order Tracking Agent: Separate agent for post-purchase support
- Personalization: Learn user preferences over time
- Multi-language: Support international customers
- Admin Dashboard: Manage products, view analytics, monitor agent performance
- A/B Testing: Test different agent prompts and workflows
- Human Handoff: Escalate complex issues to human support
📚 Additional Resources
This implementation gives you a production-ready AI shopping agent that autonomously handles the complete e-commerce workflow using the latest Vercel AI SDK patterns!

