Skip to main content

Command Palette

Search for a command to run...

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

Published
9 min read
🛍️ Building an AI-Powered E-commerce Chatbot Using Vercel AI SDK and Gemini
G
Backend engineer building ShiftMailer, an AI-powered email tool. Sharing lessons from shipping AI agents and scalable systems. Follow for real-world AI, product, and engineering insights—no fluff.

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 -&nbsp;</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