Project 8: E-Commerce Shopping App

Project 8: E-Commerce Shopping App

Complete Shopping Experience: Browse โ†’ Cart โ†’ Checkout โ†’ Order Confirmation

Why This Matters: E-commerce is a primary ChatGPT Apps use case. This project teaches cart state management, multi-step flows, commerce restrictions, and session persistenceโ€”essential patterns for building transactional apps.


Project Metadata

  • Main Programming Language: TypeScript/React
  • Alternative Programming Languages: Vue, Next.js
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The โ€œOpen Coreโ€ Infrastructure
  • Difficulty: Level 3: Advanced
  • Time Estimate: 3-4 weeks
  • Prerequisites: Projects 1-7, e-commerce experience helpful
  • Knowledge Area: E-Commerce / Cart State / Checkout Flows
  • Software/Tools: Stripe (demo), React, widgetSessionId, MCP
  • Main Book: โ€œDesigning Web Usabilityโ€ by Jakob Nielsen

1. Theoretical Foundation

E-Commerce Flow Architecture

The core flow of any e-commerce application follows a predictable pattern:

Browse โ†’ Search โ†’ View Details โ†’ Add to Cart โ†’ View Cart โ†’
Checkout โ†’ Payment โ†’ Confirmation โ†’ Order Tracking

Each step involves distinct UI patterns, data requirements, and user interactions.

Browse Phase

  • Product catalog display: Grid/list views with filtering
  • Search functionality: Text search + faceted navigation
  • Category browsing: Hierarchical navigation
  • Quick view: Modal previews without leaving the list

Cart Phase

  • Session persistence: Cart survives page refreshes and conversation turns
  • Quantity management: Add, remove, update quantities
  • Pricing calculation: Subtotals, taxes, shipping, discounts
  • Stock validation: Real-time availability checks

Checkout Phase

  • Address collection: Shipping and billing addresses
  • Shipping method selection: Speed vs. cost tradeoffs
  • Payment information: Secure tokenized payment handling
  • Order review: Final confirmation before submission

Post-Purchase Phase

  • Order confirmation: Receipt, order number, tracking info
  • Email notifications: Confirmation, shipping updates
  • Order history: Access to past purchases

Cart State Management with widgetSessionId

The critical challenge in ChatGPT Apps e-commerce is maintaining cart state across multiple widget instances.

Unlike traditional web apps where state lives in the browser session, ChatGPT Apps:

  • Destroy and recreate widgets on each conversation turn
  • Run in iframes with limited storage access
  • Need to survive conversation pauses and resumptions

The widgetSessionId Pattern

// Available in window.openai
const sessionId = window.openai.widgetSessionId;

// Properties:
// - Unique per conversation
// - Persists across widget instances
// - Remains constant throughout the conversation
// - Used as a server-side session identifier

Implementation Strategy:

  1. Server-side cart storage: Use widgetSessionId as the key
  2. Widget loads cart on mount: Fetch from server using sessionId
  3. All mutations go through server: Ensure single source of truth
  4. Optimistic updates: Update UI immediately, sync with server
// Cart Architecture Pattern
interface Cart {
  sessionId: string;
  items: CartItem[];
  subtotal: number;
  tax: number;
  shipping: number;
  total: number;
  createdAt: Date;
  updatedAt: Date;
}

interface CartItem {
  productId: string;
  quantity: number;
  variant: {
    size?: string;
    color?: string;
  };
  price: number;
  subtotal: number;
}

Why Not Use widgetState?

window.openai.widgetState stores data client-side but:

  • Has size limitations (~100KB)
  • Only persists what you explicitly save
  • Doesnโ€™t survive across different widgets
  • Not suitable for sensitive cart data

widgetSessionId enables server-side storage that:

  • Has no size limits
  • Persists automatically
  • Survives all widget instances
  • Keeps sensitive data server-side

OpenAI Commerce Restrictions

ChatGPT Apps must comply with OpenAIโ€™s commerce guidelines:

ALLOWED:

  • Physical goods (clothing, electronics, furniture)
  • Physical services (salon appointments, home repairs)
  • Event tickets (concerts, sports, theater)
  • Restaurant reservations and food delivery
  • Travel bookings (flights, hotels, car rentals)

PROHIBITED:

  • Digital products for direct sale (ebooks, software licenses, NFTs)
  • Financial products (stocks, crypto, insurance)
  • Age-restricted items (alcohol, tobacco, adult content)
  • Prescription medications
  • Weapons and explosives
  • Illegal or regulated substances
  • Multi-level marketing products

Why These Restrictions?

  1. Safety: Prevents harmful purchases
  2. Regulation: Avoids regulated products requiring licenses
  3. Age verification: OpenAI canโ€™t verify user age reliably
  4. Financial risk: Reduces potential for financial fraud
  5. Legal compliance: Meets international commerce laws

Multi-Step Checkout Flows

Checkout requires multiple steps with different data and validation:

Step 1: Cart Review
โ”œโ”€โ”€ View items
โ”œโ”€โ”€ Adjust quantities
โ””โ”€โ”€ Apply coupon codes

Step 2: Shipping Information
โ”œโ”€โ”€ Delivery address
โ”œโ”€โ”€ Shipping method selection
โ”œโ”€โ”€ Delivery date preferences
โ””โ”€โ”€ Special instructions

Step 3: Payment
โ”œโ”€โ”€ Payment method selection
โ”œโ”€โ”€ Billing address (if different)
โ”œโ”€โ”€ Payment details (tokenized)
โ””โ”€โ”€ Save for future use checkbox

Step 4: Review & Confirm
โ”œโ”€โ”€ Order summary
โ”œโ”€โ”€ Terms acceptance
โ””โ”€โ”€ Final total display

Step 5: Confirmation
โ”œโ”€โ”€ Order number
โ”œโ”€โ”€ Estimated delivery
โ”œโ”€โ”€ Confirmation email sent
โ””โ”€โ”€ Order tracking link

State Management Across Steps

interface CheckoutState {
  currentStep: 1 | 2 | 3 | 4 | 5;
  completedSteps: number[];

  cartData: Cart;

  shippingAddress?: Address;
  shippingMethod?: ShippingMethod;

  billingAddress?: Address;
  paymentMethod?: PaymentMethod;

  orderSummary?: OrderSummary;
  orderId?: string;
}

// Validation per step
const stepValidation = {
  1: () => cart.items.length > 0,
  2: () => shippingAddress && shippingMethod,
  3: () => paymentMethod && billingAddress,
  4: () => termsAccepted,
  5: () => true // Confirmation has no validation
};

Order Confirmation Patterns

Order confirmation requires special handling due to destructive nature.

@mcp.tool(
    annotations={
        "destructiveHint": True  # โ† Forces ChatGPT to ask for confirmation
    }
)
def place_order(session_id: str, ...) -> dict:
    """
    Use this when user explicitly confirms they want to place their order.
    This action charges the payment method and is irreversible.
    """

Confirmation UX Flow

  1. User indicates intent: โ€œCheckoutโ€, โ€œPlace orderโ€, โ€œComplete purchaseโ€
  2. ChatGPT sees destructiveHint: Recognizes this is irreversible
  3. ChatGPT asks for confirmation: Shows order summary and asks โ€œConfirm?โ€
  4. User explicitly confirms: โ€œYesโ€, โ€œConfirmโ€, โ€œPlace the orderโ€
  5. Tool executes: Order is created, payment processed
  6. Confirmation displayed: Order number, tracking, receipt

Why Destructive Hints Matter

Without destructiveHint: true, ChatGPT might:

  • Place orders based on ambiguous phrasing
  • Execute multiple times due to conversation context
  • Not give users a chance to review before charging

With destructiveHint: true, ChatGPT will:

  • Always ask for explicit confirmation
  • Show clear order summary before proceeding
  • Give users a chance to cancel
  • Log the confirmation in conversation history

Price Calculation and Accuracy

E-commerce requires precise decimal arithmetic:

from decimal import Decimal, ROUND_HALF_UP

class PriceCalculator:
    def calculate_cart_total(self, items: List[CartItem]) -> CartTotals:
        # Use Decimal to avoid floating point errors
        subtotal = Decimal('0.00')

        for item in items:
            price = Decimal(str(item.price))
            quantity = Decimal(str(item.quantity))
            subtotal += price * quantity

        # Tax calculation
        tax_rate = Decimal('0.0875')  # 8.75%
        tax = (subtotal * tax_rate).quantize(
            Decimal('0.01'),
            rounding=ROUND_HALF_UP
        )

        # Shipping (could be dynamic)
        shipping = self.calculate_shipping(subtotal, items)

        total = subtotal + tax + shipping

        return CartTotals(
            subtotal=float(subtotal),
            tax=float(tax),
            shipping=float(shipping),
            total=float(total)
        )

Common Price Calculation Errors:

# WRONG: Floating point errors
subtotal = 0.0
for item in items:
    subtotal += item.price * item.quantity  # Can drift

# RIGHT: Decimal arithmetic
from decimal import Decimal
subtotal = Decimal('0.00')
for item in items:
    subtotal += Decimal(str(item.price)) * item.quantity

Inventory Management

Real-time stock validation prevents overselling:

def validate_cart_availability(cart: Cart) -> ValidationResult:
    """Check if all cart items are still in stock."""
    out_of_stock = []
    insufficient_quantity = []

    for item in cart.items:
        product = get_product(item.product_id)

        if product.stock == 0:
            out_of_stock.append(item)
        elif product.stock < item.quantity:
            insufficient_quantity.append({
                'item': item,
                'available': product.stock,
                'requested': item.quantity
            })

    return ValidationResult(
        valid=len(out_of_stock) == 0 and len(insufficient_quantity) == 0,
        out_of_stock=out_of_stock,
        insufficient_quantity=insufficient_quantity
    )

Session Expiry and Cart Abandonment

Carts should eventually expire to:

  • Free up inventory holds
  • Clean up orphaned data
  • Encourage purchase completion
CART_EXPIRY_HOURS = 24

def clean_expired_carts():
    """Remove carts older than 24 hours."""
    expiry_time = datetime.now() - timedelta(hours=CART_EXPIRY_HOURS)

    expired = Cart.query.filter(
        Cart.updated_at < expiry_time,
        Cart.status == 'active'
    ).all()

    for cart in expired:
        # Release inventory holds
        release_inventory_holds(cart)
        # Mark as abandoned
        cart.status = 'abandoned'
        # Trigger abandonment email (optional)
        send_abandonment_email(cart)

2. Solution Architecture

System Components Overview

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          CHATGPT INTERFACE                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  User: "Show me running shoes under $100"                          โ”‚
โ”‚                                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚          Product List Widget (iframe)                      โ”‚    โ”‚
โ”‚  โ”‚  - Renders search results                                  โ”‚    โ”‚
โ”‚  โ”‚  - Handles filters, pagination                             โ”‚    โ”‚
โ”‚  โ”‚  - Product cards with "Add to Cart" buttons                โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                              โ†•                                      โ”‚
โ”‚                    window.openai.callTool()                         โ”‚
โ”‚                              โ†•                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚                   MCP Server (Python)                      โ”‚    โ”‚
โ”‚  โ”‚  Tools:                                                     โ”‚    โ”‚
โ”‚  โ”‚  - search_products()                                       โ”‚    โ”‚
โ”‚  โ”‚  - get_product_details()                                   โ”‚    โ”‚
โ”‚  โ”‚  - add_to_cart()                                           โ”‚    โ”‚
โ”‚  โ”‚  - view_cart()                                             โ”‚    โ”‚
โ”‚  โ”‚  - update_cart_item()                                      โ”‚    โ”‚
โ”‚  โ”‚  - remove_from_cart()                                      โ”‚    โ”‚
โ”‚  โ”‚  - start_checkout()                                        โ”‚    โ”‚
โ”‚  โ”‚  - place_order() [destructive]                             โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                              โ†•                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚              Backend Services (FastAPI)                    โ”‚    โ”‚
โ”‚  โ”‚  - Product catalog API                                     โ”‚    โ”‚
โ”‚  โ”‚  - Cart service (session-based)                            โ”‚    โ”‚
โ”‚  โ”‚  - Order management                                        โ”‚    โ”‚
โ”‚  โ”‚  - Payment processing (Stripe)                             โ”‚    โ”‚
โ”‚  โ”‚  - Inventory management                                    โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                              โ†•                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚                    Database (PostgreSQL)                   โ”‚    โ”‚
โ”‚  โ”‚  Tables: products, carts, cart_items, orders, customers    โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Shopping Cart Component Architecture

ShoppingCartWidget
โ”œโ”€โ”€ CartHeader
โ”‚   โ”œโ”€โ”€ ItemCount badge
โ”‚   โ””โ”€โ”€ Session indicator
โ”‚
โ”œโ”€โ”€ CartItems (List)
โ”‚   โ””โ”€โ”€ CartItem (repeated)
โ”‚       โ”œโ”€โ”€ ProductImage
โ”‚       โ”œโ”€โ”€ ProductDetails
โ”‚       โ”‚   โ”œโ”€โ”€ Name
โ”‚       โ”‚   โ”œโ”€โ”€ Variant (size/color)
โ”‚       โ”‚   โ””โ”€โ”€ Price
โ”‚       โ”œโ”€โ”€ QuantitySelector
โ”‚       โ”‚   โ”œโ”€โ”€ Decrease button
โ”‚       โ”‚   โ”œโ”€โ”€ Quantity input
โ”‚       โ”‚   โ””โ”€โ”€ Increase button
โ”‚       โ””โ”€โ”€ RemoveButton
โ”‚
โ”œโ”€โ”€ CartSummary
โ”‚   โ”œโ”€โ”€ Subtotal row
โ”‚   โ”œโ”€โ”€ Shipping row
โ”‚   โ”œโ”€โ”€ Tax row
โ”‚   โ”œโ”€โ”€ Discount row (if applicable)
โ”‚   โ””โ”€โ”€ Total row (emphasized)
โ”‚
โ””โ”€โ”€ CartActions
    โ”œโ”€โ”€ ContinueShopping button
    โ”œโ”€โ”€ ApplyCoupon input
    โ””โ”€โ”€ ProceedToCheckout button (primary)

Implementation:

// ShoppingCartWidget.tsx
import { useEffect, useState } from 'react';
import { Button, Badge } from '@openai/apps-sdk-ui';

interface CartItem {
  id: string;
  productId: string;
  name: string;
  image: string;
  price: number;
  quantity: number;
  variant: {
    size?: string;
    color?: string;
  };
  subtotal: number;
}

interface Cart {
  items: CartItem[];
  subtotal: number;
  tax: number;
  shipping: number;
  discount: number;
  total: number;
}

export function ShoppingCartWidget() {
  const sessionId = window.openai?.widgetSessionId;
  const [cart, setCart] = useState<Cart | null>(null);
  const [loading, setLoading] = useState(true);
  const [updating, setUpdating] = useState<string | null>(null);

  // Load cart from server on mount
  useEffect(() => {
    const loadCart = async () => {
      try {
        const response = await fetch(
          `https://api.your-store.com/cart/${sessionId}`
        );
        const data = await response.json();
        setCart(data);
      } catch (error) {
        console.error('Failed to load cart:', error);
      } finally {
        setLoading(false);
      }
    };

    if (sessionId) {
      loadCart();
    }
  }, [sessionId]);

  const updateQuantity = async (itemId: string, newQuantity: number) => {
    if (newQuantity < 1) return;

    setUpdating(itemId);

    // Optimistic update
    setCart(prev => prev && {
      ...prev,
      items: prev.items.map(item =>
        item.id === itemId
          ? { ...item, quantity: newQuantity, subtotal: item.price * newQuantity }
          : item
      )
    });

    try {
      // Call MCP tool to update server
      await window.openai?.callTool('update_cart_item', {
        sessionId,
        itemId,
        quantity: newQuantity
      });
    } catch (error) {
      // Revert on error
      console.error('Failed to update quantity:', error);
      // Reload cart to get accurate state
      window.location.reload();
    } finally {
      setUpdating(null);
    }
  };

  const removeItem = async (itemId: string) => {
    setUpdating(itemId);

    try {
      await window.openai?.callTool('remove_from_cart', {
        sessionId,
        itemId
      });

      // Remove from local state
      setCart(prev => prev && {
        ...prev,
        items: prev.items.filter(item => item.id !== itemId)
      });
    } catch (error) {
      console.error('Failed to remove item:', error);
    } finally {
      setUpdating(null);
    }
  };

  const proceedToCheckout = () => {
    window.openai?.callTool('start_checkout', { sessionId });
  };

  if (loading) {
    return <div className="p-8 text-center">Loading your cart...</div>;
  }

  if (!cart || cart.items.length === 0) {
    return (
      <div className="p-8 text-center">
        <p className="text-gray-500 mb-4">Your cart is empty</p>
        <Button onClick={() => window.openai?.sendFollowUpMessage('Show me products')}>
          Continue Shopping
        </Button>
      </div>
    );
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      {/* Header */}
      <div className="flex items-center justify-between mb-6">
        <h2 className="text-2xl font-bold">Shopping Cart</h2>
        <Badge>{cart.items.length} {cart.items.length === 1 ? 'item' : 'items'}</Badge>
      </div>

      {/* Cart Items */}
      <div className="space-y-4 mb-6">
        {cart.items.map(item => (
          <div
            key={item.id}
            className="flex gap-4 p-4 border rounded-lg"
          >
            {/* Product Image */}
            <img
              src={item.image}
              alt={item.name}
              className="w-24 h-24 object-cover rounded"
            />

            {/* Product Details */}
            <div className="flex-1">
              <h3 className="font-semibold">{item.name}</h3>
              {item.variant.size && (
                <p className="text-sm text-gray-600">Size: {item.variant.size}</p>
              )}
              {item.variant.color && (
                <p className="text-sm text-gray-600">Color: {item.variant.color}</p>
              )}
              <p className="text-sm font-medium mt-2">${item.price.toFixed(2)}</p>
            </div>

            {/* Quantity Selector */}
            <div className="flex items-center gap-2">
              <Button
                variant="secondary"
                size="sm"
                onClick={() => updateQuantity(item.id, item.quantity - 1)}
                disabled={updating === item.id || item.quantity <= 1}
              >
                โˆ’
              </Button>
              <span className="w-12 text-center">{item.quantity}</span>
              <Button
                variant="secondary"
                size="sm"
                onClick={() => updateQuantity(item.id, item.quantity + 1)}
                disabled={updating === item.id}
              >
                +
              </Button>
            </div>

            {/* Subtotal */}
            <div className="text-right">
              <p className="font-semibold">${item.subtotal.toFixed(2)}</p>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => removeItem(item.id)}
                disabled={updating === item.id}
                className="text-red-600 hover:text-red-700"
              >
                Remove
              </Button>
            </div>
          </div>
        ))}
      </div>

      {/* Cart Summary */}
      <div className="border-t pt-4 space-y-2">
        <div className="flex justify-between text-sm">
          <span>Subtotal:</span>
          <span>${cart.subtotal.toFixed(2)}</span>
        </div>
        <div className="flex justify-between text-sm">
          <span>Shipping:</span>
          <span>${cart.shipping.toFixed(2)}</span>
        </div>
        <div className="flex justify-between text-sm">
          <span>Tax:</span>
          <span>${cart.tax.toFixed(2)}</span>
        </div>
        {cart.discount > 0 && (
          <div className="flex justify-between text-sm text-green-600">
            <span>Discount:</span>
            <span>-${cart.discount.toFixed(2)}</span>
          </div>
        )}
        <div className="flex justify-between text-lg font-bold pt-2 border-t">
          <span>Total:</span>
          <span>${cart.total.toFixed(2)}</span>
        </div>
      </div>

      {/* Actions */}
      <div className="flex gap-3 mt-6">
        <Button
          variant="secondary"
          onClick={() => window.openai?.sendFollowUpMessage('Show me more products')}
        >
          Continue Shopping
        </Button>
        <Button
          variant="primary"
          onClick={proceedToCheckout}
          className="flex-1"
        >
          Proceed to Checkout โ†’
        </Button>
      </div>
    </div>
  );
}

Session-Based Cart Persistence

Server-side cart management:

# cart_service.py
from datetime import datetime, timedelta
from typing import Optional
from decimal import Decimal

class CartService:
    def __init__(self, db_session):
        self.db = db_session

    def get_or_create_cart(self, session_id: str) -> Cart:
        """Get existing cart or create a new one."""
        cart = self.db.query(Cart).filter(
            Cart.session_id == session_id,
            Cart.status == 'active'
        ).first()

        if not cart:
            cart = Cart(
                session_id=session_id,
                status='active',
                created_at=datetime.now(),
                updated_at=datetime.now()
            )
            self.db.add(cart)
            self.db.commit()

        return cart

    def add_item(
        self,
        session_id: str,
        product_id: str,
        quantity: int,
        variant: dict
    ) -> CartItem:
        """Add item to cart or update quantity if exists."""
        cart = self.get_or_create_cart(session_id)

        # Check if item already in cart
        existing = self.db.query(CartItem).filter(
            CartItem.cart_id == cart.id,
            CartItem.product_id == product_id,
            CartItem.size == variant.get('size'),
            CartItem.color == variant.get('color')
        ).first()

        if existing:
            existing.quantity += quantity
            existing.updated_at = datetime.now()
            item = existing
        else:
            product = self.get_product(product_id)

            item = CartItem(
                cart_id=cart.id,
                product_id=product_id,
                name=product.name,
                image=product.image,
                price=product.price,
                quantity=quantity,
                size=variant.get('size'),
                color=variant.get('color'),
                created_at=datetime.now(),
                updated_at=datetime.now()
            )
            self.db.add(item)

        cart.updated_at = datetime.now()
        self.db.commit()

        return item

    def update_item_quantity(
        self,
        session_id: str,
        item_id: str,
        quantity: int
    ) -> CartItem:
        """Update cart item quantity."""
        cart = self.get_or_create_cart(session_id)

        item = self.db.query(CartItem).filter(
            CartItem.id == item_id,
            CartItem.cart_id == cart.id
        ).first()

        if not item:
            raise ValueError(f"Item {item_id} not found in cart")

        if quantity < 1:
            raise ValueError("Quantity must be at least 1")

        # Validate stock availability
        product = self.get_product(item.product_id)
        if product.stock < quantity:
            raise ValueError(
                f"Only {product.stock} units available, requested {quantity}"
            )

        item.quantity = quantity
        item.updated_at = datetime.now()
        cart.updated_at = datetime.now()

        self.db.commit()

        return item

    def remove_item(self, session_id: str, item_id: str) -> None:
        """Remove item from cart."""
        cart = self.get_or_create_cart(session_id)

        item = self.db.query(CartItem).filter(
            CartItem.id == item_id,
            CartItem.cart_id == cart.id
        ).first()

        if item:
            self.db.delete(item)
            cart.updated_at = datetime.now()
            self.db.commit()

    def get_cart_with_totals(self, session_id: str) -> dict:
        """Get cart with calculated totals."""
        cart = self.get_or_create_cart(session_id)

        items = self.db.query(CartItem).filter(
            CartItem.cart_id == cart.id
        ).all()

        # Calculate totals using Decimal for precision
        subtotal = Decimal('0.00')
        for item in items:
            price = Decimal(str(item.price))
            quantity = Decimal(str(item.quantity))
            item.subtotal = float(price * quantity)
            subtotal += price * quantity

        # Calculate shipping
        shipping = self.calculate_shipping(subtotal, items)

        # Calculate tax (example: 8.75%)
        tax_rate = Decimal('0.0875')
        tax = (subtotal * tax_rate).quantize(
            Decimal('0.01'),
            rounding=ROUND_HALF_UP
        )

        # Apply discounts if any
        discount = self.calculate_discounts(cart, subtotal)

        # Calculate total
        total = subtotal + shipping + tax - discount

        return {
            'sessionId': session_id,
            'items': [self.item_to_dict(item) for item in items],
            'subtotal': float(subtotal),
            'shipping': float(shipping),
            'tax': float(tax),
            'discount': float(discount),
            'total': float(total),
            'itemCount': len(items)
        }

    def calculate_shipping(
        self,
        subtotal: Decimal,
        items: list
    ) -> Decimal:
        """Calculate shipping cost based on subtotal and items."""
        # Free shipping over $50
        if subtotal >= Decimal('50.00'):
            return Decimal('0.00')

        # Flat rate shipping
        return Decimal('5.99')

    def calculate_discounts(self, cart: Cart, subtotal: Decimal) -> Decimal:
        """Calculate applicable discounts."""
        discount = Decimal('0.00')

        # Check for coupon codes
        if cart.coupon_code:
            coupon = self.get_coupon(cart.coupon_code)
            if coupon and coupon.is_valid():
                if coupon.type == 'percentage':
                    discount = subtotal * (Decimal(str(coupon.value)) / Decimal('100'))
                elif coupon.type == 'fixed':
                    discount = Decimal(str(coupon.value))

        return discount

Product Catalog and Search Patterns

Product search with filters:

# search_service.py
from typing import List, Optional
from sqlalchemy import or_, and_

class ProductSearchService:
    def __init__(self, db_session):
        self.db = db_session

    def search_products(
        self,
        query: str = "",
        category: Optional[str] = None,
        min_price: Optional[float] = None,
        max_price: Optional[float] = None,
        brands: Optional[List[str]] = None,
        sizes: Optional[List[str]] = None,
        colors: Optional[List[str]] = None,
        in_stock_only: bool = True,
        sort_by: str = "relevance",
        page: int = 1,
        per_page: int = 20
    ) -> dict:
        """
        Search products with filters and pagination.
        """
        # Base query
        q = self.db.query(Product).filter(Product.active == True)

        # Text search
        if query:
            search_filter = or_(
                Product.name.ilike(f"%{query}%"),
                Product.description.ilike(f"%{query}%"),
                Product.tags.contains([query.lower()])
            )
            q = q.filter(search_filter)

        # Category filter
        if category:
            q = q.filter(Product.category == category)

        # Price range
        if min_price is not None:
            q = q.filter(Product.price >= min_price)
        if max_price is not None:
            q = q.filter(Product.price <= max_price)

        # Brand filter
        if brands:
            q = q.filter(Product.brand.in_(brands))

        # Size filter
        if sizes:
            q = q.filter(Product.available_sizes.overlap(sizes))

        # Color filter
        if colors:
            q = q.filter(Product.available_colors.overlap(colors))

        # Stock filter
        if in_stock_only:
            q = q.filter(Product.stock > 0)

        # Sorting
        if sort_by == "price_asc":
            q = q.order_by(Product.price.asc())
        elif sort_by == "price_desc":
            q = q.order_by(Product.price.desc())
        elif sort_by == "rating":
            q = q.order_by(Product.rating.desc())
        elif sort_by == "newest":
            q = q.order_by(Product.created_at.desc())
        else:  # relevance (default)
            # Could use full-text search ranking here
            q = q.order_by(Product.popularity.desc())

        # Get total count before pagination
        total = q.count()

        # Pagination
        offset = (page - 1) * per_page
        products = q.offset(offset).limit(per_page).all()

        return {
            'products': [self.product_to_dict(p) for p in products],
            'total': total,
            'page': page,
            'perPage': per_page,
            'totalPages': (total + per_page - 1) // per_page,
            'hasMore': offset + len(products) < total
        }

    def get_facets(self, query: str = "", category: Optional[str] = None) -> dict:
        """
        Get available filter options based on current search.
        """
        q = self.db.query(Product).filter(Product.active == True)

        if query:
            search_filter = or_(
                Product.name.ilike(f"%{query}%"),
                Product.description.ilike(f"%{query}%")
            )
            q = q.filter(search_filter)

        if category:
            q = q.filter(Product.category == category)

        products = q.all()

        # Extract unique values for each facet
        brands = sorted(set(p.brand for p in products if p.brand))
        categories = sorted(set(p.category for p in products if p.category))

        all_sizes = []
        all_colors = []
        for p in products:
            if p.available_sizes:
                all_sizes.extend(p.available_sizes)
            if p.available_colors:
                all_colors.extend(p.available_colors)

        sizes = sorted(set(all_sizes))
        colors = sorted(set(all_colors))

        # Price range
        prices = [p.price for p in products if p.price]
        price_min = min(prices) if prices else 0
        price_max = max(prices) if prices else 0

        return {
            'brands': brands,
            'categories': categories,
            'sizes': sizes,
            'colors': colors,
            'priceRange': {
                'min': price_min,
                'max': price_max
            }
        }

Product List Widget with Filters:

// ProductListWidget.tsx
import { useState, useEffect } from 'react';
import { Button, Input, Badge } from '@openai/apps-sdk-ui';

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  image: string;
  rating: number;
  reviewCount: number;
  inStock: boolean;
}

export function ProductListWidget() {
  const data = window.openai?.toolOutput;
  const [filters, setFilters] = useState({
    priceMin: null,
    priceMax: null,
    brands: [],
    inStockOnly: true
  });

  const applyFilter = (key: string, value: any) => {
    const newFilters = { ...filters, [key]: value };
    setFilters(newFilters);

    // Trigger new search with filters
    window.openai?.callTool('search_products', {
      query: data.query,
      ...newFilters,
      page: 1
    });
  };

  const addToCart = (productId: string) => {
    window.openai?.callTool('add_to_cart', {
      sessionId: window.openai.widgetSessionId,
      productId,
      quantity: 1
    });
  };

  return (
    <div className="max-w-6xl mx-auto p-6">
      {/* Filters */}
      <div className="flex gap-4 mb-6 pb-4 border-b">
        <div className="flex-1">
          <Input
            placeholder="Min price"
            type="number"
            value={filters.priceMin || ''}
            onChange={(e) => setFilters({ ...filters, priceMin: e.target.value })}
          />
        </div>
        <div className="flex-1">
          <Input
            placeholder="Max price"
            type="number"
            value={filters.priceMax || ''}
            onChange={(e) => setFilters({ ...filters, priceMax: e.target.value })}
          />
        </div>
        <Button onClick={() => applyFilter('inStockOnly', !filters.inStockOnly)}>
          {filters.inStockOnly ? 'Show All' : 'In Stock Only'}
        </Button>
      </div>

      {/* Results Count */}
      <div className="mb-4">
        <p className="text-sm text-gray-600">
          Showing {data.products.length} of {data.total} results
        </p>
      </div>

      {/* Product Grid */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {data.products.map((product: Product) => (
          <div key={product.id} className="border rounded-lg overflow-hidden hover:shadow-lg transition">
            <img
              src={product.image}
              alt={product.name}
              className="w-full h-48 object-cover"
            />
            <div className="p-4">
              <h3 className="font-semibold mb-2">{product.name}</h3>
              <p className="text-sm text-gray-600 mb-2 line-clamp-2">
                {product.description}
              </p>

              <div className="flex items-center gap-2 mb-2">
                <span className="text-yellow-500">โ˜…</span>
                <span className="text-sm">{product.rating}</span>
                <span className="text-xs text-gray-500">
                  ({product.reviewCount} reviews)
                </span>
              </div>

              <div className="flex items-center justify-between mt-4">
                <span className="text-lg font-bold">
                  ${product.price.toFixed(2)}
                </span>

                {product.inStock ? (
                  <Button
                    onClick={() => addToCart(product.id)}
                    size="sm"
                  >
                    Add to Cart
                  </Button>
                ) : (
                  <Badge variant="secondary">Out of Stock</Badge>
                )}
              </div>
            </div>
          </div>
        ))}
      </div>

      {/* Pagination */}
      {data.totalPages > 1 && (
        <div className="flex justify-center gap-2 mt-8">
          <Button
            variant="secondary"
            disabled={data.page === 1}
            onClick={() => window.openai?.callTool('search_products', {
              ...filters,
              page: data.page - 1
            })}
          >
            โ† Previous
          </Button>

          <span className="flex items-center px-4">
            Page {data.page} of {data.totalPages}
          </span>

          <Button
            variant="secondary"
            disabled={!data.hasMore}
            onClick={() => window.openai?.callTool('search_products', {
              ...filters,
              page: data.page + 1
            })}
          >
            Next โ†’
          </Button>
        </div>
      )}
    </div>
  );
}

Checkout Flow Design

Multi-step checkout widget:

// CheckoutWidget.tsx
import { useState, useEffect } from 'react';
import { Button, Input } from '@openai/apps-sdk-ui';

const STEPS = {
  SHIPPING: 1,
  PAYMENT: 2,
  REVIEW: 3,
  CONFIRMATION: 4
};

export function CheckoutWidget() {
  const sessionId = window.openai?.widgetSessionId;
  const [currentStep, setCurrentStep] = useState(STEPS.SHIPPING);
  const [checkoutData, setCheckoutData] = useState({
    shippingAddress: null,
    shippingMethod: null,
    paymentMethod: null,
    billingAddress: null
  });

  const ShippingStep = () => (
    <div className="space-y-4">
      <h2 className="text-xl font-bold">Shipping Information</h2>

      <Input label="Full Name" required />
      <Input label="Address Line 1" required />
      <Input label="Address Line 2" />

      <div className="grid grid-cols-2 gap-4">
        <Input label="City" required />
        <Input label="State" required />
      </div>

      <div className="grid grid-cols-2 gap-4">
        <Input label="ZIP Code" required />
        <Input label="Country" required />
      </div>

      <Input label="Phone Number" type="tel" required />

      <h3 className="font-semibold mt-6">Shipping Method</h3>
      <div className="space-y-2">
        <label className="flex items-center gap-2 p-3 border rounded cursor-pointer hover:bg-gray-50">
          <input type="radio" name="shipping" value="standard" />
          <div className="flex-1">
            <div className="font-medium">Standard Shipping</div>
            <div className="text-sm text-gray-600">5-7 business days</div>
          </div>
          <div className="font-semibold">$5.99</div>
        </label>

        <label className="flex items-center gap-2 p-3 border rounded cursor-pointer hover:bg-gray-50">
          <input type="radio" name="shipping" value="express" />
          <div className="flex-1">
            <div className="font-medium">Express Shipping</div>
            <div className="text-sm text-gray-600">2-3 business days</div>
          </div>
          <div className="font-semibold">$12.99</div>
        </label>
      </div>

      <Button
        onClick={() => setCurrentStep(STEPS.PAYMENT)}
        className="w-full mt-6"
      >
        Continue to Payment โ†’
      </Button>
    </div>
  );

  const PaymentStep = () => (
    <div className="space-y-4">
      <h2 className="text-xl font-bold">Payment Information</h2>

      <div className="p-4 bg-yellow-50 border border-yellow-200 rounded">
        <p className="text-sm">
          <strong>Note:</strong> For security, I cannot enter credit card details.
          You'll need to enter your payment information directly.
        </p>
      </div>

      <h3 className="font-semibold">Payment Method</h3>
      <div className="space-y-2">
        <label className="flex items-center gap-2 p-3 border rounded cursor-pointer hover:bg-gray-50">
          <input type="radio" name="payment" value="card" />
          <span>Credit/Debit Card</span>
        </label>

        <label className="flex items-center gap-2 p-3 border rounded cursor-pointer hover:bg-gray-50">
          <input type="radio" name="payment" value="paypal" />
          <span>PayPal</span>
        </label>
      </div>

      <div className="flex gap-3 mt-6">
        <Button
          variant="secondary"
          onClick={() => setCurrentStep(STEPS.SHIPPING)}
        >
          โ† Back
        </Button>
        <Button
          onClick={() => setCurrentStep(STEPS.REVIEW)}
          className="flex-1"
        >
          Continue to Review โ†’
        </Button>
      </div>
    </div>
  );

  const ReviewStep = () => {
    const [cart, setCart] = useState(null);

    useEffect(() => {
      fetch(`https://api.your-store.com/cart/${sessionId}`)
        .then(r => r.json())
        .then(setCart);
    }, []);

    if (!cart) return <div>Loading...</div>;

    return (
      <div className="space-y-6">
        <h2 className="text-xl font-bold">Review Your Order</h2>

        {/* Order Items */}
        <div>
          <h3 className="font-semibold mb-3">Items ({cart.itemCount})</h3>
          <div className="space-y-2">
            {cart.items.map(item => (
              <div key={item.id} className="flex justify-between text-sm">
                <span>{item.name} x{item.quantity}</span>
                <span>${item.subtotal.toFixed(2)}</span>
              </div>
            ))}
          </div>
        </div>

        {/* Shipping Address */}
        <div>
          <h3 className="font-semibold mb-2">Shipping Address</h3>
          <p className="text-sm text-gray-600">
            {checkoutData.shippingAddress?.name}<br />
            {checkoutData.shippingAddress?.address1}<br />
            {checkoutData.shippingAddress?.city}, {checkoutData.shippingAddress?.state} {checkoutData.shippingAddress?.zip}
          </p>
        </div>

        {/* Order Total */}
        <div className="border-t pt-4">
          <div className="flex justify-between mb-1">
            <span>Subtotal:</span>
            <span>${cart.subtotal.toFixed(2)}</span>
          </div>
          <div className="flex justify-between mb-1">
            <span>Shipping:</span>
            <span>${cart.shipping.toFixed(2)}</span>
          </div>
          <div className="flex justify-between mb-1">
            <span>Tax:</span>
            <span>${cart.tax.toFixed(2)}</span>
          </div>
          <div className="flex justify-between font-bold text-lg mt-2 pt-2 border-t">
            <span>Total:</span>
            <span>${cart.total.toFixed(2)}</span>
          </div>
        </div>

        <div className="flex gap-3">
          <Button
            variant="secondary"
            onClick={() => setCurrentStep(STEPS.PAYMENT)}
          >
            โ† Back
          </Button>
          <Button
            onClick={() => {
              // This will trigger ChatGPT to ask for confirmation
              // because place_order has destructiveHint: true
              window.openai?.callTool('place_order', {
                sessionId,
                shippingAddress: checkoutData.shippingAddress,
                shippingMethod: checkoutData.shippingMethod,
                paymentMethod: checkoutData.paymentMethod
              });
            }}
            className="flex-1"
          >
            Place Order
          </Button>
        </div>
      </div>
    );
  };

  const steps = {
    [STEPS.SHIPPING]: <ShippingStep />,
    [STEPS.PAYMENT]: <PaymentStep />,
    [STEPS.REVIEW]: <ReviewStep />
  };

  return (
    <div className="max-w-2xl mx-auto p-6">
      {/* Progress Indicator */}
      <div className="flex items-center justify-between mb-8">
        {[
          { num: 1, label: 'Shipping' },
          { num: 2, label: 'Payment' },
          { num: 3, label: 'Review' }
        ].map((step, idx) => (
          <div key={step.num} className="flex items-center flex-1">
            <div className={`
              w-8 h-8 rounded-full flex items-center justify-center
              ${currentStep >= step.num ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600'}
            `}>
              {step.num}
            </div>
            <span className="ml-2 text-sm">{step.label}</span>
            {idx < 2 && (
              <div className={`flex-1 h-1 mx-2 ${currentStep > step.num ? 'bg-blue-600' : 'bg-gray-200'}`} />
            )}
          </div>
        ))}
      </div>

      {/* Current Step Content */}
      {steps[currentStep]}
    </div>
  );
}

Integration with Payment Processors (Stripe Demo)

Note: ChatGPT Apps cannot enter credit card details. Payment must be handled through:

  1. Pre-saved payment methods (OAuth-linked accounts)
  2. Redirect to external checkout pages
  3. Demo/test mode for development
# payment_service.py
import stripe
from typing import Optional

stripe.api_key = os.getenv('STRIPE_SECRET_KEY')

class PaymentService:
    def create_payment_intent(
        self,
        amount: int,  # in cents
        currency: str = 'usd',
        customer_id: Optional[str] = None,
        metadata: dict = None
    ) -> dict:
        """
        Create a Stripe PaymentIntent for checkout.
        """
        try:
            intent = stripe.PaymentIntent.create(
                amount=amount,
                currency=currency,
                customer=customer_id,
                metadata=metadata or {},
                automatic_payment_methods={
                    'enabled': True,
                }
            )

            return {
                'success': True,
                'clientSecret': intent.client_secret,
                'paymentIntentId': intent.id
            }
        except stripe.error.StripeError as e:
            return {
                'success': False,
                'error': str(e)
            }

    def confirm_payment(self, payment_intent_id: str) -> dict:
        """
        Confirm a payment and retrieve status.
        """
        try:
            intent = stripe.PaymentIntent.retrieve(payment_intent_id)

            return {
                'success': intent.status == 'succeeded',
                'status': intent.status,
                'amount': intent.amount,
                'currency': intent.currency
            }
        except stripe.error.StripeError as e:
            return {
                'success': False,
                'error': str(e)
            }

    def create_checkout_session(
        self,
        line_items: list,
        success_url: str,
        cancel_url: str,
        customer_email: Optional[str] = None
    ) -> dict:
        """
        Create a Stripe Checkout session (redirect-based).
        """
        try:
            session = stripe.checkout.Session.create(
                payment_method_types=['card'],
                line_items=line_items,
                mode='payment',
                success_url=success_url,
                cancel_url=cancel_url,
                customer_email=customer_email
            )

            return {
                'success': True,
                'sessionId': session.id,
                'url': session.url  # Redirect user here
            }
        except stripe.error.StripeError as e:
            return {
                'success': False,
                'error': str(e)
            }

MCP tool for checkout:

@mcp.tool(
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True  # Requires confirmation
    }
)
def place_order(
    session_id: str,
    shipping_address_id: str,
    shipping_method_id: str,
    payment_method_id: str
) -> dict:
    """
    Use this when user explicitly confirms they want to place their order.
    This charges the payment method and creates the order.
    """
    cart_service = CartService(db)
    payment_service = PaymentService()
    order_service = OrderService(db)

    # Get cart
    cart = cart_service.get_cart_with_totals(session_id)

    if not cart['items']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Cart is empty"
            }
        }

    # Validate inventory
    validation = cart_service.validate_availability(cart)
    if not validation['valid']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Some items are out of stock",
                "details": validation
            }
        }

    # Create payment intent
    amount_cents = int(cart['total'] * 100)
    payment_result = payment_service.create_payment_intent(
        amount=amount_cents,
        metadata={
            'session_id': session_id,
            'order_type': 'chatgpt_app'
        }
    )

    if not payment_result['success']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Payment processing failed"
            }
        }

    # Create order
    order = order_service.create_order(
        cart=cart,
        shipping_address_id=shipping_address_id,
        shipping_method_id=shipping_method_id,
        payment_intent_id=payment_result['paymentIntentId']
    )

    # Clear cart
    cart_service.clear_cart(session_id)

    # Send confirmation email
    send_order_confirmation_email(order)

    return {
        "structuredContent": {
            "success": True,
            "orderId": order.id,
            "orderNumber": order.order_number,
            "total": cart['total'],
            "estimatedDelivery": order.estimated_delivery_date.isoformat(),
            "trackingUrl": f"https://your-store.com/track/{order.id}"
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/order-confirmation.html?orderId={order.id}"
        }
    }

3. Real-World Outcome

When complete, your e-commerce app enables this user experience:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ChatGPT                                                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚ You: Show me running shoes under $100                               โ”‚
โ”‚                                                                     โ”‚
โ”‚ ChatGPT: Here are running shoes under $100:                         โ”‚
โ”‚                                                                     โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚  [Product Grid with 12 shoes, filters, sort options]           โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Nike Air Zoom Pegasus 40    Adidas Ultraboost Light           โ”‚ โ”‚
โ”‚ โ”‚  $89.99 โญ 4.5 (2,341)        $94.99 โญ 4.7 (1,892)             โ”‚ โ”‚
โ”‚ โ”‚  [View] [Add to Cart]         [View] [Add to Cart]             โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚ You: Add the Nike Pegasus to my cart in size 10                     โ”‚
โ”‚                                                                     โ”‚
โ”‚ ChatGPT: Added Nike Air Zoom Pegasus 40 (Size 10) to your cart!     โ”‚
โ”‚                                                                     โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚  Shopping Cart (1 item)                                         โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [IMG] Nike Air Zoom Pegasus 40                                 โ”‚ โ”‚
โ”‚ โ”‚        Size: 10 โ€ข Black                                         โ”‚ โ”‚
โ”‚ โ”‚        $89.99                    [- 1 +] [Remove]               โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Subtotal:  $89.99                                              โ”‚ โ”‚
โ”‚ โ”‚  Shipping:   $5.99                                              โ”‚ โ”‚
โ”‚ โ”‚  Tax:        $7.92                                              โ”‚ โ”‚
โ”‚ โ”‚  Total:    $103.90                                              โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [Continue Shopping]  [Proceed to Checkout โ†’]                   โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚ You: Proceed to checkout                                            โ”‚
โ”‚                                                                     โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚  Checkout - Step 1 of 3: Shipping                               โ”‚ โ”‚
โ”‚ โ”‚  โ—โ”โ”โ”โ”โ”โ—‹โ”โ”โ”โ”โ”โ—‹                                                  โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [Shipping address form]                                        โ”‚ โ”‚
โ”‚ โ”‚  [Shipping method selection: Standard $5.99 vs Express $12.99]  โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [Continue to Payment โ†’]                                        โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚ [After filling shipping info...]                                    โ”‚
โ”‚                                                                     โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚  Checkout - Step 3 of 3: Review & Confirm                       โ”‚ โ”‚
โ”‚ โ”‚  โ—โ”โ”โ”โ”โ”โ—โ”โ”โ”โ”โ”โ—                                                  โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Order Summary:                                                 โ”‚ โ”‚
โ”‚ โ”‚  โ€ข Nike Air Zoom Pegasus 40 (Size 10) x1 - $89.99              โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Shipping to:                                                   โ”‚ โ”‚
โ”‚ โ”‚  John Doe, 123 Main St, San Francisco, CA 94102                 โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Total: $103.90                                                 โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [โ† Back]  [Place Order]                                        โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚ ChatGPT: Before I place this order, please confirm:                 โ”‚
โ”‚ - Total charge: $103.90                                             โ”‚
โ”‚ - Shipping to: 123 Main St, San Francisco, CA 94102                โ”‚
โ”‚ - This will charge your payment method on file.                     โ”‚
โ”‚                                                                     โ”‚
โ”‚ Should I proceed with this order?                                   โ”‚
โ”‚                                                                     โ”‚
โ”‚ You: Yes, place the order                                           โ”‚
โ”‚                                                                     โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚  Order Confirmed!                                               โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Order #: ORD-2025-12345                                        โ”‚ โ”‚
โ”‚ โ”‚  Total: $103.90                                                 โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  Estimated Delivery: Dec 28-30, 2025                            โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  A confirmation email has been sent to your email.              โ”‚ โ”‚
โ”‚ โ”‚                                                                 โ”‚ โ”‚
โ”‚ โ”‚  [Track Order]  [View Order Details]                            โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚ ChatGPT: Your order has been placed successfully! You'll receive    โ”‚
โ”‚ tracking information once it ships.                                 โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4. Core Challenges Youโ€™ll Face

Challenge 1: Cart State Persistence Across Widget Instances

The Problem:

  • Widgets are destroyed and recreated on each conversation turn
  • window.openai.widgetState has size limits and doesnโ€™t persist across different widget types
  • User expects cart to survive โ€œShow me more productsโ€ โ†’ โ€œView cartโ€ transitions

The Solution: Use widgetSessionId as a server-side session key:

// In every widget
const sessionId = window.openai?.widgetSessionId;

useEffect(() => {
  // Load cart from server
  fetch(`/api/cart/${sessionId}`)
    .then(r => r.json())
    .then(setCart);
}, [sessionId]);

Challenge 2: Multi-Widget Checkout Flow Management

The Problem: Checkout requires multiple steps (shipping, payment, review), but each step might be a different widget instance.

The Solution: Store checkout state on the server keyed by sessionId:

# Server-side checkout state
checkout_sessions = {}

def get_checkout_state(session_id: str) -> dict:
    if session_id not in checkout_sessions:
        checkout_sessions[session_id] = {
            'step': 1,
            'shipping_address': None,
            'shipping_method': None,
            'payment_method': None
        }
    return checkout_sessions[session_id]

def update_checkout_step(session_id: str, data: dict):
    state = get_checkout_state(session_id)
    state.update(data)
    checkout_sessions[session_id] = state

Challenge 3: Commerce Restrictions Compliance

The Problem: OpenAI prohibits selling digital products, but what counts as โ€œdigitalโ€?

Gray Areas:

  • โŒ Ebooks (digital product)
  • โœ… Physical books (physical product)
  • โŒ Software licenses (digital product)
  • โœ… Software on physical media (physical product)
  • โŒ NFTs (digital asset)
  • โœ… Event tickets (physical experience)

The Solution:

  • Filter your product catalog to exclude prohibited items
  • Add validation in MCP tools
  • Include disclaimers in product descriptions
PROHIBITED_CATEGORIES = [
    'digital_downloads',
    'cryptocurrency',
    'financial_products',
    'prescription_medications',
    'weapons',
    'adult_content'
]

def validate_product_allowed(product: Product) -> bool:
    """Check if product can be sold through ChatGPT Apps."""
    if product.category in PROHIBITED_CATEGORIES:
        return False

    if product.type == 'digital' and not product.physical_component:
        return False

    return True

Challenge 4: Order Confirmation with destructiveHint

The Problem: Users might say โ€œbuy those shoesโ€ without realizing it will charge their card immediately.

The Solution: Mark place_order with destructiveHint: true:

@mcp.tool(
    annotations={
        "destructiveHint": True  # โ† ChatGPT will ALWAYS ask for confirmation
    }
)
def place_order(...):
    """
    Use this when user EXPLICITLY CONFIRMS they want to place their order.
    This action charges the payment method and is irreversible.
    """

This forces ChatGPT to:

  1. Show order summary
  2. Explicitly state it will charge the payment method
  3. Ask โ€œShould I proceed?โ€
  4. Wait for clear confirmation
  5. Only then call the tool

Challenge 5: Payment Method Handling

The Problem: ChatGPT Apps cannot enter credit card details for security reasons.

Solutions:

Option A: OAuth-Linked Payment Methods

# User links their account via OAuth
# You store their payment methods server-side
# ChatGPT can use pre-saved payment methods

@mcp.tool()
def list_saved_payment_methods(user_id: str) -> dict:
    """Show user's saved payment methods."""
    methods = get_saved_payment_methods(user_id)
    return {
        "structuredContent": {
            "paymentMethods": [
                {
                    "id": "pm_123",
                    "type": "card",
                    "last4": "4242",
                    "brand": "Visa"
                }
            ]
        }
    }

Option B: Redirect to External Checkout

@mcp.tool()
def create_checkout_link(session_id: str) -> dict:
    """Generate a secure checkout link."""
    checkout = create_stripe_checkout_session(session_id)

    return {
        "structuredContent": {
            "message": "Please complete payment at this secure link",
            "checkoutUrl": checkout['url'],
            "expiresAt": checkout['expires_at']
        }
    }

Option C: Demo/Test Mode

# For development and demos
DEMO_MODE = os.getenv('DEMO_MODE') == 'true'

def process_payment(amount: int):
    if DEMO_MODE:
        return {
            'success': True,
            'payment_id': 'demo_' + str(uuid.uuid4())
        }
    else:
        return stripe.charge(amount)

5. Implementation Hints

MCP Server Structure

# ecommerce_mcp_server.py
from fastmcp import FastMCP
from typing import Optional, List
import os

mcp = FastMCP("E-Commerce Shopping App")

# ============================================================================
# PRODUCT SEARCH & BROWSING
# ============================================================================

@mcp.tool(
    annotations={"readOnlyHint": True}
)
def search_products(
    query: str = "",
    category: Optional[str] = None,
    min_price: Optional[float] = None,
    max_price: Optional[float] = None,
    page: int = 1,
    per_page: int = 20
) -> dict:
    """
    Use this when user wants to search for or browse products.
    Supports text search, category filtering, and price ranges.
    """
    search_service = ProductSearchService(db)
    results = search_service.search_products(
        query=query,
        category=category,
        min_price=min_price,
        max_price=max_price,
        page=page,
        per_page=per_page
    )

    return {
        "structuredContent": results,
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/products.html?q={query}&page={page}"
        }
    }

@mcp.tool(
    annotations={"readOnlyHint": True}
)
def get_product_details(product_id: str) -> dict:
    """
    Use this when user wants to see detailed information about a specific product.
    """
    product = get_product_by_id(product_id)

    return {
        "structuredContent": {
            "product": product_to_dict(product),
            "relatedProducts": get_related_products(product_id)
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/product/{product_id}.html"
        }
    }

# ============================================================================
# CART MANAGEMENT
# ============================================================================

@mcp.tool(
    annotations={"readOnlyHint": False}
)
def add_to_cart(
    session_id: str,
    product_id: str,
    quantity: int = 1,
    size: Optional[str] = None,
    color: Optional[str] = None
) -> dict:
    """
    Use this when user wants to add a product to their shopping cart.
    """
    cart_service = CartService(db)

    # Validate product exists and is in stock
    product = get_product_by_id(product_id)
    if not product:
        return {
            "structuredContent": {
                "success": False,
                "error": "Product not found"
            }
        }

    if product.stock < quantity:
        return {
            "structuredContent": {
                "success": False,
                "error": f"Only {product.stock} units available"
            }
        }

    # Add to cart
    cart_service.add_item(
        session_id=session_id,
        product_id=product_id,
        quantity=quantity,
        variant={'size': size, 'color': color}
    )

    # Get updated cart
    cart = cart_service.get_cart_with_totals(session_id)

    return {
        "structuredContent": {
            "success": True,
            "message": f"Added {product.name} to cart",
            "cart": cart
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/cart.html?session={session_id}"
        }
    }

@mcp.tool(
    annotations={"readOnlyHint": True}
)
def view_cart(session_id: str) -> dict:
    """
    Use this when user wants to see their shopping cart.
    """
    cart_service = CartService(db)
    cart = cart_service.get_cart_with_totals(session_id)

    return {
        "structuredContent": cart,
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/cart.html?session={session_id}"
        }
    }

@mcp.tool(
    annotations={"readOnlyHint": False}
)
def update_cart_item(
    session_id: str,
    item_id: str,
    quantity: int
) -> dict:
    """
    Use this when user wants to change the quantity of an item in their cart.
    """
    cart_service = CartService(db)

    try:
        cart_service.update_item_quantity(session_id, item_id, quantity)
        cart = cart_service.get_cart_with_totals(session_id)

        return {
            "structuredContent": {
                "success": True,
                "cart": cart
            }
        }
    except ValueError as e:
        return {
            "structuredContent": {
                "success": False,
                "error": str(e)
            }
        }

@mcp.tool(
    annotations={"readOnlyHint": False}
)
def remove_from_cart(session_id: str, item_id: str) -> dict:
    """
    Use this when user wants to remove an item from their cart.
    """
    cart_service = CartService(db)
    cart_service.remove_item(session_id, item_id)
    cart = cart_service.get_cart_with_totals(session_id)

    return {
        "structuredContent": {
            "success": True,
            "message": "Item removed from cart",
            "cart": cart
        }
    }

# ============================================================================
# CHECKOUT
# ============================================================================

@mcp.tool(
    annotations={"readOnlyHint": False}
)
def start_checkout(session_id: str) -> dict:
    """
    Use this when user wants to proceed to checkout.
    Validates cart and begins checkout flow.
    """
    cart_service = CartService(db)
    cart = cart_service.get_cart_with_totals(session_id)

    if not cart['items']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Cart is empty"
            }
        }

    # Validate inventory
    validation = cart_service.validate_availability(cart)
    if not validation['valid']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Some items are no longer available",
                "details": validation
            }
        }

    # Initialize checkout session
    checkout = initialize_checkout(session_id)

    return {
        "structuredContent": {
            "success": True,
            "checkout": checkout,
            "cart": cart
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/checkout.html?session={session_id}"
        }
    }

@mcp.tool(
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True  # โ† REQUIRES CONFIRMATION
    }
)
def place_order(
    session_id: str,
    shipping_address_id: str,
    shipping_method_id: str,
    payment_method_id: str
) -> dict:
    """
    Use this when user EXPLICITLY CONFIRMS they want to place their order.
    This action charges the payment method and creates the order.
    THIS ACTION IS IRREVERSIBLE.
    """
    cart_service = CartService(db)
    order_service = OrderService(db)
    payment_service = PaymentService()

    # Get cart
    cart = cart_service.get_cart_with_totals(session_id)

    if not cart['items']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Cart is empty"
            }
        }

    # Final inventory check
    validation = cart_service.validate_availability(cart)
    if not validation['valid']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Some items are out of stock",
                "details": validation
            }
        }

    # Process payment
    amount_cents = int(cart['total'] * 100)
    payment_result = payment_service.create_payment_intent(
        amount=amount_cents,
        metadata={'session_id': session_id}
    )

    if not payment_result['success']:
        return {
            "structuredContent": {
                "success": False,
                "error": "Payment failed",
                "details": payment_result
            }
        }

    # Create order
    order = order_service.create_order(
        cart=cart,
        shipping_address_id=shipping_address_id,
        shipping_method_id=shipping_method_id,
        payment_intent_id=payment_result['paymentIntentId']
    )

    # Clear cart
    cart_service.clear_cart(session_id)

    # Send confirmation email
    send_order_confirmation_email(order)

    return {
        "structuredContent": {
            "success": True,
            "orderId": order.id,
            "orderNumber": order.order_number,
            "total": cart['total'],
            "estimatedDelivery": order.estimated_delivery_date.isoformat(),
            "trackingUrl": f"https://your-store.com/track/{order.id}"
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/order-confirmation.html?orderId={order.id}"
        }
    }

# ============================================================================
# ORDER TRACKING
# ============================================================================

@mcp.tool(
    annotations={"readOnlyHint": True}
)
def get_order_status(order_id: str) -> dict:
    """
    Use this when user wants to check the status of their order.
    """
    order = get_order_by_id(order_id)

    if not order:
        return {
            "structuredContent": {
                "success": False,
                "error": "Order not found"
            }
        }

    return {
        "structuredContent": {
            "success": True,
            "order": order_to_dict(order),
            "tracking": get_tracking_info(order)
        },
        "uiComponent": {
            "type": "iframe",
            "url": f"https://your-app.com/order/{order_id}.html"
        }
    }

# Run server
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(mcp.app, host="0.0.0.0", port=8000)

Database Schema

-- products table
CREATE TABLE products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100),
    brand VARCHAR(100),
    image_url VARCHAR(500),
    stock INTEGER DEFAULT 0,
    available_sizes TEXT[], -- Array of sizes
    available_colors TEXT[], -- Array of colors
    rating DECIMAL(3, 2),
    review_count INTEGER DEFAULT 0,
    active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- carts table
CREATE TABLE carts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id VARCHAR(255) UNIQUE NOT NULL, -- widgetSessionId
    status VARCHAR(50) DEFAULT 'active', -- active, abandoned, converted
    coupon_code VARCHAR(50),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- cart_items table
CREATE TABLE cart_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    cart_id UUID REFERENCES carts(id) ON DELETE CASCADE,
    product_id UUID REFERENCES products(id),
    name VARCHAR(255) NOT NULL, -- Denormalized for order history
    image VARCHAR(500),
    price DECIMAL(10, 2) NOT NULL,
    quantity INTEGER NOT NULL,
    size VARCHAR(50),
    color VARCHAR(50),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(cart_id, product_id, size, color) -- Prevent duplicates
);

-- orders table
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_number VARCHAR(50) UNIQUE NOT NULL,
    session_id VARCHAR(255),
    status VARCHAR(50) DEFAULT 'pending', -- pending, confirmed, shipped, delivered, cancelled
    subtotal DECIMAL(10, 2) NOT NULL,
    tax DECIMAL(10, 2) NOT NULL,
    shipping DECIMAL(10, 2) NOT NULL,
    discount DECIMAL(10, 2) DEFAULT 0,
    total DECIMAL(10, 2) NOT NULL,

    shipping_address_id UUID,
    shipping_method_id UUID,
    payment_intent_id VARCHAR(255),

    estimated_delivery_date DATE,
    tracking_number VARCHAR(255),

    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- order_items table
CREATE TABLE order_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
    product_id UUID REFERENCES products(id),
    name VARCHAR(255) NOT NULL,
    image VARCHAR(500),
    price DECIMAL(10, 2) NOT NULL,
    quantity INTEGER NOT NULL,
    size VARCHAR(50),
    color VARCHAR(50),
    subtotal DECIMAL(10, 2) NOT NULL
);

-- indexes
CREATE INDEX idx_carts_session_id ON carts(session_id);
CREATE INDEX idx_cart_items_cart_id ON cart_items(cart_id);
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_active_stock ON products(active, stock);
CREATE INDEX idx_orders_session_id ON orders(session_id);
CREATE INDEX idx_orders_status ON orders(status);

6. Testing Strategy

Unit Tests

# test_cart_service.py
import pytest
from decimal import Decimal
from cart_service import CartService

def test_add_item_to_cart():
    """Test adding an item to a new cart."""
    cart_service = CartService(test_db)

    item = cart_service.add_item(
        session_id="test-session-1",
        product_id="product-123",
        quantity=2,
        variant={'size': 'M', 'color': 'Blue'}
    )

    assert item.quantity == 2
    assert item.size == 'M'
    assert item.color == 'Blue'

def test_add_existing_item_increments_quantity():
    """Test that adding an existing item updates quantity."""
    cart_service = CartService(test_db)

    # Add initially
    cart_service.add_item(
        session_id="test-session-2",
        product_id="product-123",
        quantity=1,
        variant={'size': 'M'}
    )

    # Add again
    cart_service.add_item(
        session_id="test-session-2",
        product_id="product-123",
        quantity=2,
        variant={'size': 'M'}
    )

    cart = cart_service.get_cart_with_totals("test-session-2")
    assert len(cart['items']) == 1
    assert cart['items'][0]['quantity'] == 3

def test_price_calculation_precision():
    """Test that price calculations use proper decimal precision."""
    cart_service = CartService(test_db)

    # Add item with price that could cause floating point errors
    cart_service.add_item(
        session_id="test-session-3",
        product_id="product-456",
        quantity=3,
        variant={}
    )

    cart = cart_service.get_cart_with_totals("test-session-3")

    # Verify Decimal precision (no floating point drift)
    assert isinstance(Decimal(str(cart['subtotal'])), Decimal)
    assert cart['subtotal'] == 29.97  # 3 * 9.99

def test_validate_stock_availability():
    """Test inventory validation."""
    cart_service = CartService(test_db)

    # Mock product with limited stock
    product = create_test_product(stock=2)

    # Add more than available
    with pytest.raises(ValueError, match="Only 2 units available"):
        cart_service.update_item_quantity(
            session_id="test-session-4",
            item_id="item-123",
            quantity=5
        )

Integration Tests

# test_checkout_flow.py
import pytest
from mcp_server import mcp

def test_full_checkout_flow():
    """Test complete purchase flow from cart to order."""
    session_id = "integration-test-session"

    # 1. Add product to cart
    result = mcp.call_tool('add_to_cart', {
        'session_id': session_id,
        'product_id': 'test-product-1',
        'quantity': 1
    })
    assert result['structuredContent']['success'] == True

    # 2. View cart
    cart = mcp.call_tool('view_cart', {'session_id': session_id})
    assert len(cart['structuredContent']['items']) == 1

    # 3. Start checkout
    checkout = mcp.call_tool('start_checkout', {'session_id': session_id})
    assert checkout['structuredContent']['success'] == True

    # 4. Place order
    order = mcp.call_tool('place_order', {
        'session_id': session_id,
        'shipping_address_id': 'test-address-1',
        'shipping_method_id': 'standard',
        'payment_method_id': 'test-payment-1'
    })

    assert order['structuredContent']['success'] == True
    assert 'orderId' in order['structuredContent']

    # 5. Verify cart is cleared
    cart_after = mcp.call_tool('view_cart', {'session_id': session_id})
    assert len(cart_after['structuredContent']['items']) == 0

E2E Tests (Simulated)

# test_chatgpt_interaction.py
def test_shopping_conversation():
    """Simulate a complete shopping conversation."""
    session = ChatGPTTestSession()

    # User searches for products
    response = session.send("Show me running shoes under $100")
    assert 'search_products' in response.tools_called
    assert response.widget_displayed

    # User adds to cart
    response = session.send("Add the Nike Pegasus to my cart in size 10")
    assert 'add_to_cart' in response.tools_called
    assert response.cart_item_count == 1

    # User views cart
    response = session.send("Show my cart")
    assert 'view_cart' in response.tools_called
    assert 'Nike' in response.text

    # User checks out
    response = session.send("Checkout")
    assert 'start_checkout' in response.tools_called

    # ChatGPT should ask for confirmation before charging
    response = session.send("Place the order")
    assert response.asks_for_confirmation
    assert 'charge' in response.text.lower()

    # User confirms
    response = session.send("Yes, confirm")
    assert 'place_order' in response.tools_called
    assert response.order_created

7. Common Mistakes to Avoid

Mistake 1: Using Floating Point for Money

# WRONG โŒ
subtotal = 0.0
for item in cart_items:
    subtotal += item.price * item.quantity  # Floating point errors!

# RIGHT โœ…
from decimal import Decimal
subtotal = Decimal('0.00')
for item in cart_items:
    subtotal += Decimal(str(item.price)) * item.quantity

Mistake 2: Not Validating Stock Before Checkout

# WRONG โŒ
def place_order(session_id):
    cart = get_cart(session_id)
    # Directly create order without checking stock
    order = create_order(cart)

# RIGHT โœ…
def place_order(session_id):
    cart = get_cart(session_id)

    # Validate inventory AGAIN (race condition protection)
    validation = validate_stock_availability(cart)
    if not validation['valid']:
        return {'error': 'Items out of stock', 'details': validation}

    order = create_order(cart)

Mistake 3: Forgetting destructiveHint on place_order

# WRONG โŒ
@mcp.tool()
def place_order(...):
    # Without destructiveHint, ChatGPT might place orders
    # without explicit confirmation!
    pass

# RIGHT โœ…
@mcp.tool(
    annotations={"destructiveHint": True}
)
def place_order(...):
    """
    Use this when user EXPLICITLY CONFIRMS they want to place their order.
    This charges the payment method and is irreversible.
    """
    pass

Mistake 4: Storing Sensitive Payment Data

# WRONG โŒ
def save_credit_card(card_number, cvv, expiry):
    # NEVER store raw card data!
    db.insert({
        'card_number': card_number,
        'cvv': cvv
    })

# RIGHT โœ…
def save_payment_method(stripe_payment_method_id):
    # Store only tokenized payment method IDs
    db.insert({
        'stripe_payment_method_id': stripe_payment_method_id,
        'last4': stripe_payment_method.card.last4,
        'brand': stripe_payment_method.card.brand
    })

Mistake 5: Not Handling Cart Abandonment

# WRONG โŒ
# Carts live forever, filling up database

# RIGHT โœ…
from datetime import datetime, timedelta

CART_EXPIRY_HOURS = 24

def cleanup_abandoned_carts():
    """Run this periodically (cronjob)."""
    expiry_time = datetime.now() - timedelta(hours=CART_EXPIRY_HOURS)

    abandoned = Cart.query.filter(
        Cart.updated_at < expiry_time,
        Cart.status == 'active'
    ).all()

    for cart in abandoned:
        cart.status = 'abandoned'
        # Optionally: send abandonment email

    db.commit()

Mistake 6: Not Pre-filling from ChatGPT Context

# WRONG โŒ
# User says "add Nike Pegasus to cart" but widget shows empty form

# RIGHT โœ…
# Use toolInput to pre-fill from user's message
const prefill = window.openai?.toolInput;

<Input
  defaultValue={prefill?.product_name || ''}
  disabled
/>
<Input
  label="Size"
  defaultValue={prefill?.size || ''}
/>

8. Debugging Techniques

Debug Cart State Issues

// Add debugging overlay to widget
function DebugPanel() {
  const sessionId = window.openai?.widgetSessionId;
  const toolInput = window.openai?.toolInput;
  const toolOutput = window.openai?.toolOutput;

  if (process.env.NODE_ENV !== 'development') return null;

  return (
    <div className="fixed bottom-0 left-0 right-0 bg-black text-white p-2 text-xs">
      <div>Session ID: {sessionId}</div>
      <div>Tool Input: {JSON.stringify(toolInput)}</div>
      <div>Tool Output: {JSON.stringify(toolOutput)}</div>
    </div>
  );
}

Debug MCP Tool Calls

# Add logging to MCP server
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

@mcp.tool()
def add_to_cart(session_id: str, product_id: str, quantity: int):
    logger.debug(f"add_to_cart called: session={session_id}, product={product_id}, qty={quantity}")

    try:
        result = cart_service.add_item(session_id, product_id, quantity)
        logger.debug(f"add_to_cart success: {result}")
        return result
    except Exception as e:
        logger.error(f"add_to_cart failed: {e}", exc_info=True)
        raise

Debug Price Calculations

def debug_price_calculation(cart):
    """Print step-by-step price calculation."""
    print("=== Price Calculation Debug ===")

    subtotal = Decimal('0.00')
    for item in cart.items:
        item_total = Decimal(str(item.price)) * item.quantity
        print(f"{item.name}: ${item.price} x {item.quantity} = ${item_total}")
        subtotal += item_total

    print(f"\nSubtotal: ${subtotal}")

    shipping = calculate_shipping(subtotal)
    print(f"Shipping: ${shipping}")

    tax_rate = Decimal('0.0875')
    tax = (subtotal * tax_rate).quantize(Decimal('0.01'))
    print(f"Tax (8.75%): ${tax}")

    total = subtotal + shipping + tax
    print(f"Total: ${total}")

    return float(total)

Test with MCP Inspector

# Start MCP Inspector
npx @modelcontextprotocol/inspector

# Connect to your server
# URL: http://localhost:8000

# Manually test tools:
> invoke add_to_cart {
    "session_id": "test-123",
    "product_id": "prod-456",
    "quantity": 2
  }

> invoke view_cart {
    "session_id": "test-123"
  }

> invoke place_order {
    "session_id": "test-123",
    "shipping_address_id": "addr-789",
    "shipping_method_id": "standard",
    "payment_method_id": "pm-abc"
  }

9. Production Deployment Checklist

Infrastructure

  • Database: PostgreSQL with proper indexes
  • MCP Server: Deployed on Railway/Render/AWS
  • Widget hosting: Vercel/Cloudflare Pages (CDN)
  • Payment processor: Stripe account configured
  • Email service: SendGrid/Mailgun for order confirmations
  • Monitoring: Sentry for error tracking
  • Logging: Structured logs for debugging

Security

  • Environment variables: All secrets in environment (not code)
  • HTTPS only: All endpoints use TLS
  • CORS configured: Widget domain whitelisted
  • Rate limiting: Prevent abuse of MCP endpoints
  • Input validation: All tool parameters validated
  • SQL injection prevention: Use parameterized queries
  • XSS protection: Sanitize user input in widgets

Commerce Compliance

  • Product filtering: No prohibited items in catalog
  • Age verification disclaimer: If selling age-restricted items
  • Terms of service: Link to terms displayed in checkout
  • Privacy policy: Link to privacy policy displayed
  • Refund policy: Clear refund/return policy stated
  • Shipping policy: Delivery timeframes clearly stated

Performance

  • Database indexing: Indexes on session_id, product_id, etc.
  • Caching: Product catalog cached (Redis/Memcached)
  • Image optimization: Product images compressed and CDN-hosted
  • Widget bundle size: < 200KB gzipped
  • API response times: < 500ms for most endpoints
  • Pagination: Never load all products at once

Testing

  • Unit tests: 80%+ coverage on cart/order logic
  • Integration tests: Full checkout flow tested
  • Load testing: Can handle 100+ concurrent users
  • Mobile testing: Widget works on mobile ChatGPT
  • Edge case testing: Out of stock, invalid coupon, etc.

10. Learning Milestones

Milestone 1: Basic Product Browsing

Goal: Display a product list with search and filters

You understand:

  • โœ… How to structure product search tools
  • โœ… How to render product grids in widgets
  • โœ… How to use toolOutput to display data
  • โœ… Basic pagination patterns

Test: User can search โ€œrunning shoesโ€ and see results


Milestone 2: Cart State Persistence

Goal: Cart survives across conversation turns

You understand:

  • โœ… widgetSessionId pattern
  • โœ… Server-side session storage
  • โœ… Loading cart state on widget mount
  • โœ… Optimistic updates vs. server sync

Test: Add item to cart, ask โ€œShow me more productsโ€, then โ€œView cartโ€ โ€” item is still there


Milestone 3: Cart Mutations

Goal: Add, update, remove cart items

You understand:

  • โœ… Implementing cart CRUD operations
  • โœ… Quantity validation
  • โœ… Stock availability checks
  • โœ… Price calculations with Decimal

Test: Can add items, change quantities, remove items, see accurate totals


Milestone 4: Multi-Step Checkout

Goal: Complete checkout flow with shipping and payment steps

You understand:

  • โœ… Multi-step form design
  • โœ… Progress indicators
  • โœ… Checkout state management
  • โœ… Form validation

Test: Can navigate through shipping โ†’ payment โ†’ review steps


Milestone 5: Order Placement with Confirmation

Goal: Place order with destructiveHint confirmation

You understand:

  • โœ… destructiveHint annotation
  • โœ… ChatGPT confirmation flow
  • โœ… Payment processing (Stripe demo)
  • โœ… Order confirmation display

Test: ChatGPT asks for explicit confirmation before placing order


Milestone 6: Complete E-Commerce Flow

Goal: Search โ†’ Add to Cart โ†’ Checkout โ†’ Order Confirmation

You understand:

  • โœ… How all components work together
  • โœ… Session persistence across widget types
  • โœ… Commerce compliance requirements
  • โœ… Production-ready patterns

Test: Can complete full purchase flow end-to-end


11. Advanced Extensions

Once youโ€™ve mastered the core e-commerce app, extend it with:

Extension 1: Wishlists

@mcp.tool()
def add_to_wishlist(session_id: str, product_id: str):
    """Save product for later."""
    pass

Extension 2: Product Recommendations

@mcp.tool()
def get_recommendations(session_id: str):
    """Get personalized product recommendations based on cart and browsing history."""
    # Use collaborative filtering or ML model
    pass

Extension 3: Coupon Codes

@mcp.tool()
def apply_coupon(session_id: str, coupon_code: str):
    """Apply a discount coupon to the cart."""
    pass

Extension 4: Order History & Reordering

@mcp.tool()
def list_orders(user_id: str):
    """Show user's past orders."""
    pass

@mcp.tool()
def reorder(order_id: str):
    """Reorder all items from a previous order."""
    pass

Extension 5: Inventory Notifications

@mcp.tool()
def notify_when_in_stock(product_id: str, email: str):
    """Send email when out-of-stock item is back in stock."""
    pass

Extension 6: Reviews & Ratings

@mcp.tool()
def submit_review(product_id: str, rating: int, review_text: str):
    """Submit a product review."""
    pass

12. Resources & Further Reading

Official Documentation

E-Commerce Design Patterns

  • โ€œDesigning Web Usabilityโ€ by Jakob Nielsen - UX principles
  • โ€œE-Commerce UXโ€ by Baymard Institute - Research-backed patterns
  • Shopify Polaris - E-commerce design system

Payment Integration

  • Project 3: List & Search App โ†’ Builds product catalog skills
  • Project 5: Form-Based Data Entry โ†’ Builds checkout form skills
  • Project 6: OAuth Integration โ†’ Builds user account linking for saved payment methods
  • Project 9: App Store Submission โ†’ Builds production deployment skills

13. Summary

This project taught you how to build a complete e-commerce shopping app that:

  1. Manages persistent cart state across conversation turns using widgetSessionId
  2. Handles product search and browsing with filters and pagination
  3. Implements multi-step checkout with shipping and payment collection
  4. Uses destructiveHint to require order confirmation before charging
  5. Integrates with payment processors (Stripe demo mode)
  6. Complies with OpenAI commerce restrictions (physical goods only)
  7. Calculates prices accurately using Decimal arithmetic
  8. Validates inventory before checkout to prevent overselling

Key Patterns Learned

  • widgetSessionId for server-side sessions: Cart persists across widget instances
  • Optimistic updates with server sync: Immediate UI feedback + server validation
  • Multi-widget state management: Checkout state spans multiple widgets
  • destructiveHint for irreversible actions: Forces ChatGPT to ask for confirmation
  • Decimal arithmetic for money: Prevents floating-point errors
  • Stock validation at checkout: Race condition protection

What Makes This Advanced

  • Complex state management: Session-based cart spanning multiple widgets
  • Multi-step flows: Checkout requires coordinating 3+ steps
  • Payment integration: Real payment processor (Stripe) in demo mode
  • Commerce compliance: Understanding and adhering to platform restrictions
  • Production considerations: Security, PCI compliance, inventory management

Youโ€™ve now built one of the most complex ChatGPT App patternsโ€”transactional e-commerce with persistent state, multi-step flows, and payment processing. These skills transfer directly to building any transactional app: booking systems, ticketing platforms, subscription services, and more.

Next: Take your best project and submit it to the ChatGPT App Store (Project 9) to reach 800+ million users!