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:
- Server-side cart storage: Use widgetSessionId as the key
- Widget loads cart on mount: Fetch from server using sessionId
- All mutations go through server: Ensure single source of truth
- 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?
- Safety: Prevents harmful purchases
- Regulation: Avoids regulated products requiring licenses
- Age verification: OpenAI canโt verify user age reliably
- Financial risk: Reduces potential for financial fraud
- 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
- User indicates intent: โCheckoutโ, โPlace orderโ, โComplete purchaseโ
- ChatGPT sees destructiveHint: Recognizes this is irreversible
- ChatGPT asks for confirmation: Shows order summary and asks โConfirm?โ
- User explicitly confirms: โYesโ, โConfirmโ, โPlace the orderโ
- Tool executes: Order is created, payment processed
- 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:
- Pre-saved payment methods (OAuth-linked accounts)
- Redirect to external checkout pages
- 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.widgetStatehas 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:
- Show order summary
- Explicitly state it will charge the payment method
- Ask โShould I proceed?โ
- Wait for clear confirmation
- 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
- Apps SDK E-Commerce Examples - Shopping cart example
- Apps SDK Commerce Guidelines - What you can/canโt sell
- Widget State Management - widgetSessionId and widgetState
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
- Stripe Documentation - Payment processing
- Stripe Checkout - Pre-built checkout flow
- PCI Compliance Guide - Security requirements
Related Projects in This Series
- 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:
- Manages persistent cart state across conversation turns using widgetSessionId
- Handles product search and browsing with filters and pagination
- Implements multi-step checkout with shipping and payment collection
- Uses destructiveHint to require order confirmation before charging
- Integrates with payment processors (Stripe demo mode)
- Complies with OpenAI commerce restrictions (physical goods only)
- Calculates prices accurately using Decimal arithmetic
- 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!