🛍️ Building an AI-Powered E-commerce Chatbot Using Vercel AI SDK and Gemini

E-commerce is rapidly transforming — and AI-powered shopping assistants are becoming the new default buying experience.
Think about it:
Instead of browsing 20 product pages, users simply ask:
“Show me red shoes under ₹1500.”Instead of navigating a 5-step checkout, they say:
“Add the second one to my cart and checkout.”
To explore this future, I built a fully functional AI E-Commerce Chatbot using Vercel AI SDK + Gemini, equipped with tools like:
Fetch Catalog
Add to Cart
Checkout Cart
And a UI that responds with card-style product previews, add-to-cart confirmation messages, and interactive checkout prompts.
This blog is a complete to-do guide to help developers build the same chatbot from scratch.
Let’s start building it
🧩 Define Tools for Gemini
Gemini supports function calling, so we define our tools. We will create functions that accept some parameters and return a result based on them. Here, we are using an API call to achieve this, but you can implement any business logic in these functions.
Catalog tool
export async function fetchCatalog({query, maxPrice}: { query: string; maxPrice: number }) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/catalog?query=${query}&maxPrice=${maxPrice}`
);
if (!res.ok) throw new Error("Failed to fetch catalog");
return res.json();
}
Cart tool
export async function addToCart(productId: string, quantity: number) {
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/cart`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({product_id: productId, qty: quantity}),
});
if (!res.ok) throw new Error("Failed to add to cart");
return res.json();
}
Checkout Tool
export async function checkoutCart() {
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/checkout`, {
method: "POST",
});
if (!res.ok) throw new Error("Failed to checkout");
return res.json();
}
🛠️ Create the Tool-Enabled Chat API
This is the heart of the chatbot.
Define the prompt for our AI agent
const SYSTEM_PROMPT = `
You are a shopping assistant AI integrated with tools.
You can help the user browse products, add them to a cart, and checkout.
### Available tools:
1. **fetchCatalog()**
- Retrieves all products from the catalog.
- Always call this tool to see what products are available before suggesting items.
2. **addToCart(productId: string)**
- Adds the specified product to the user's cart.
- Call this when the user asks to "add", "buy", or "I want this".
3. **checkoutCart()**
- Processes the order and simulates checkout.
- Only call this if the user explicitly says "checkout", "place order", or "buy now".
---
### Rules of behavior:
- Never invent products. Only use the results returned by 'fetchCatalog'.
- Always confirm product details (name, price, stock) when suggesting items.
- If the user is vague (e.g., "show me something cool"), fetch the catalog and then suggest a few options.
- Be conversational and friendly, but clearly indicate actions you’re taking.
- After a successful checkout, thank the user and end the flow.
---
### Examples:
**User:** "Show me headphones under $150"
**Assistant reasoning:** Call 'fetchCatalog', filter results by category + price, then present matching products.
**User:** "Yes, add the headphones to my cart"
**Assistant reasoning:** Call 'addToCart(productId)' for that item then show the current cart.
**User:** "Checkout now"
**Assistant reasoning:** Call 'checkoutCart' and return the order confirmation.
`
API endpoint to use our chatbot
import {convertToModelMessages, streamText, UIMessage} from "ai";
import {addToCart, checkoutCart, fetchCatalog} from "@/lib/tools";
import {google} from "@ai-sdk/google";
import {NextRequest, NextResponse} from "next/server";
import {z} from 'zod';
export const runtime = "edge";
const gemini = google("models/gemini-2.5-flash-lite");
export async function POST(req: NextRequest) {
try {
const {messages}: { messages: UIMessage[] } = await req.json()
const result = await streamText({
model: gemini,
system: SYSTEM_PROMPT,
messages: convertToModelMessages(messages),
tools: {
fetchCatalog: {
description: "Retrieves all products from the catalog.",
inputSchema: z.object({
query: z.string().optional().default(""),
maxPrice: z.number().optional().default(1000)
}),
execute: async ({query, maxPrice}) => {
return await fetchCatalog({query, maxPrice});
}
},
addToCart: {
description: "Adds the specified product to the user's cart.",
inputSchema: z.object({
productId: z.string(),
quantity: z.number().optional().default(1)
}),
execute: async ({productId, quantity}) => {
return await addToCart(productId, quantity);
}
},
checkoutCart: {
description: "Process checkout and initiate payment",
inputSchema: z.object({}),
execute: async () => {
return await checkoutCart();
}
}
}
});
return result.toUIMessageStreamResponse({
onError: errorHandler
});
} catch (err) {
console.error("Error : ", err);
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
function errorHandler(error: unknown) {
if (error == null) {
return 'unknown error';
}
if (typeof error === 'string') {
return error;
}
if (error instanceof Error) {
return error.message;
}
return JSON.stringify(error);
}
🎨 Build UI With Card Components
When the LLM returns tool results, your UI displays them as cards so we will implement these components in react
🟥 Product Card
// components/ProductCard.tsx
"use client";
import {useState} from "react";
export default function ProductCard({product, addToCart}: { product: any, addToCart: any }) {
const [adding, setAdding] = useState(false);
const [added, setAdded] = useState(false);
const handleAddToCart = async () => {
setAdding(true);
addToCart(product?.id, product?.name)
setAdded(true)
setAdding(false);
};
return (
<div className="border rounded-xl p-4 shadow-md flex flex-col items-center gap-2 bg-white">
<img
src={product.image}
alt={product.name}
className="w-32 h-32 object-cover rounded-lg"
/>
<h3 className="text-lg font-semibold">{product.name}</h3>
<p className="text-gray-600">₹{product?.price}</p>
<button
onClick={handleAddToCart}
disabled={adding || added}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{adding ? "Adding..." : added ? "Added" : "Add to Cart"}
</button>
</div>
);
}
🟦 Cart Card
"use client";
import {CartItem} from "@/lib/cartStore";
export default function Cart({items = []}: { items?: any }) {
const total = items?.reduce((sum:number, i:CartItem) => sum + i.price * i.qty, 0);
return (
<div className="p-4 bg-white rounded-xl shadow-md w-[320px]">
<h2 className="text-lg font-bold mb-3">🛒 Your Cart</h2>
{items.length === 0 && <p className="text-gray-500">Cart is empty</p>}
<ul className="space-y-3">
{items.map((item:CartItem) => (
<li
key={item.productId}
className="flex items-center justify-between gap-2 border-b pb-2"
>
<div className="flex items-center gap-2">
{item.image && (
<img
src={item.image}
alt={item.name}
className="w-10 h-10 rounded"
/>
)}
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-gray-500">
₹{item?.price} × {item.qty} = ${item?.price * item.qty}
</p>
</div>
</div>
</li>
))}
</ul>
{items.length > 0 && (
<div className="pt-3 flex justify-between font-bold">
<span>Total - </span>
<span>₹{total ? total.toFixed(2) : "0.00"}</span>
</div>
)}
</div>
);
}
🟩 Checkout Card
"use client";
import { useState } from "react";
import {OrderItem} from "@/lib/orderStore";
export default function Checkout({order}: { order: OrderItem | null }) {
const [step, setStep] = useState<"details" | "payment" | "success">("details");
// mock "processing payment"
const handlePayment = () => {
setStep("payment");
setTimeout(() => setStep("success"), 2000);
};
return (
<div className="p-6 max-w-md mx-auto bg-white rounded-xl shadow-lg">
<h2 className="text-xl font-bold mb-4">Checkout</h2>
{step === "details" && (
<div>
<h3 className="font-semibold mb-2">Shipping Info</h3>
<form className="space-y-3">
<input
type="text"
placeholder="Full Name"
className="w-full border rounded px-3 py-2"
/>
<input
type="text"
placeholder="Address"
className="w-full border rounded px-3 py-2"
/>
<input
type="text"
placeholder="City"
className="w-full border rounded px-3 py-2"
/>
<input
type="text"
placeholder="ZIP Code"
className="w-full border rounded px-3 py-2"
/>
</form>
<div className="mt-4 border-t pt-3">
<p className="flex justify-between">
<span className="font-medium">Total</span>
<span>₹{order?.totalAmount}</span>
</p>
</div>
<button
onClick={handlePayment}
className="mt-4 w-full bg-blue-500 text-white py-2 rounded"
>
Proceed to Payment
</button>
</div>
)}
{step === "payment" && (
<div className="text-center">
<p className="text-gray-600">Processing Payment...</p>
<div className="mt-3 animate-spin rounded-full h-10 w-10 border-4 border-blue-500 border-t-transparent mx-auto"></div>
</div>
)}
{step === "success" && (
<div className="text-center">
<h3 className="text-green-600 font-bold text-lg">✅ Payment Successful!</h3>
<p className="mt-2 text-gray-600">
Thank you for your purchase. Your order will be shipped soon.
</p>
</div>
)}
</div>
);
}
💬Integrate Tool Responses in UI
Your chat page processes three types of messages:
normal model text
tool calls
tool responses
// app/page.tsx
"use client";
import {useChat} from "@ai-sdk/react";
import {DefaultChatTransport} from "ai";
import {useState} from "react";
import {Bot, User} from "lucide-react";
import ProductCard from "@/components/ProductCard";
import Cart from "@/components/CartCard";
import Checkout from "@/components/Checkout";
import {OrderItem} from "@/lib/orderStore";
export default function Home() {
const {messages, sendMessage, status} = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
});
const [input, setInput] = useState('');
return (
<main className="flex flex-col items-center pt-4 min-h-screen bg-white">
<div className="w-[70vw] bg-white rounded-lg p-6">
<h1 className="text-2xl font-bold mb-4 text-center">
🛍️ Shopping Assistant
</h1>
{/* Chat messages */}
<div className="space-y-4 h-[calc(100vh-200px)] w-full overflow-y-auto p-4 rounded-lg mb-4">
{messages.map((m) => (
<div
key={m.id}
className={`flex flex-start items-start gap-3 items-center`}
>
{m.role === "user" ? (
<div className="flex items-center gap-2">
<div className="bg-blue-500 text-white p-2 rounded-full">
<User className="w-5 h-5"/>
</div>
</div>) : (
<div className="flex items-center gap-2">
<div className="bg-gray-600 text-white p-2 rounded-full">
<Bot className="w-5 h-5"/>
</div>
</div>
)}
<div
className={`w-full flex py-[5px] px-[10px] rounded-[10px] ${m.role === "user" ? "bg-blue-100" : "bg-gray-200"}`}>
{m.parts.map((part, index) => {
switch (part?.type) {
case 'step-start':
// show step boundaries as horizontal lines:
return index > 0 ? (
<div key={index} className="text-gray-500">
<hr className="my-2 border-gray-300"/>
</div>
) : null;
case 'tool-fetchCatalog':
switch (part?.state) {
case 'input-streaming':
return <span className="italic text-gray-600"
key={`tool-${index}`}> 🛍️ Fetching products… </span>;
case 'input-available':
return <span className="italic text-gray-600"
key={`tool-${index}`}> 🛍️ Fetching products… </span>;
case 'output-available':
let products = part.output as any
return products?.length > 0 ?
<div className={"flex flex-wrap gap-4"} key={"products-list"}>
{products?.map((product: any) => {
return <ProductCard product={product} addToCart={async (productId:string, productName:string) => {
await sendMessage({text: `Add ${productName} to my cart`})
}}
key={product.id}/>
})}
</div>
:
<div key={`tool-${index}`} className="text-gray-600">No
products found.</div>
case 'output-error':
return <div key={`tool-${index}`}>Error: {part.errorText}</div>;
}
break;
case 'tool-addToCart':
switch (part?.state) {
case 'input-streaming':
return <span className="italic text-gray-600"
key={`tool-${index}`}>
➕ Adding item to cart…
</span>;
case 'input-available':
return <span className="italic text-gray-600"
key={`tool-${index}`}>
➕ Adding item to cart…
</span>;
case 'output-available':
return <Cart key={`tool-${index}}` } items={part.output as any[]}/>;
case 'output-error':
return <div key={`tool-${index}`}>Error: {part.errorText}</div>;
}
break;
case 'tool-checkoutCart':
const order = part?.output as OrderItem
switch (part?.state) {
case 'input-streaming':
return <span className="italic text-gray-600"
key={`tool-${index}`}>💳 Processing checkout…</span>;
case 'input-available':
return <span className="italic text-gray-600"
key={`tool-${index}`}>💳 Processing checkout…</span>;
case 'output-available':
return <Checkout order={order} key={`tool-${index}`}/>;
case 'output-error':
return <div key={`tool-${index}`}>Error: {part.errorText}</div>;
}
break;
case 'text':
return <span key={index}>{part.text}</span>;
}
}
)}
</div>
</div>
))}
{status === 'submitted' && <div className="text-gray-500">Thinking...</div>}
</div>
{/* Input box */}
<form onSubmit={e => {
e.preventDefault();
if (input.trim()) {
sendMessage({text: input});
setInput('');
}
}} className="flex gap-2">
<input
className="flex-1 border border-gray-300 shadow-lg rounded-lg px-4 py-2 focus:outline-none focus:ring-1 focus:ring-gray-400"
value={input}
disabled={status !== 'ready'}
placeholder="Ask about products... e.g. 'Show me shoes under 200 rupees'"
onChange={e => setInput(e.target.value)}
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 shadow-lg disabled:opacity-50"
disabled={status !== 'ready' || !input.trim()}
>
Ask
</button>
</form>
</div>
</main>
);
}
Result
List all products

Filter products based on price

Add to cart using text

Checkout feature using text that allows user to pay using preferred method

🎯 What We Just Built
With Gemini tool calling + Vercel AI SDK + Next.js, you now have an:
AI-powered shopping assistant
Interactive product catalog chatbot
Cart-enabled conversational checkout
Real-time streaming chat UI
Modern e-commerce experience using React card components
🚀 Next Steps
Add more advanced features:
🔍 Vector search (Supabase, Pinecone)
🔧 Tool-based filtering (by price, brand, ratings)
👤 Personalized recommendations
💳 Real checkout integration
📦 Track order status
🏁 Conclusion
Using Vercel AI SDK + Google Gemini, we've created a fully interactive e-commerce chatbot that transforms the shopping experience. This innovation allows users to discover products, add items to their cart, and complete the checkout process all within a conversational interface. This development is a significant advancement in the tech world, offering a seamless and engaging way to shop online.
Here is the live version: DEMO
Source code - Github






