§1 · The Architectural Gap
The Schema layer gets you found. The API layer gets you paid.
An AI agent hitting your storefront does not load a browser. It does not execute JavaScript bundles. It does not click through redirect chains. It calls endpoints directly, parses JSON, decides what to do, and calls more endpoints. If your only agent-facing surface is the HTML page — even one with clean Product and Offer markup — the agent can discover you but cannot transact. To transact, you need an API designed for the agent: stateless, typed, idempotent, and self-describing. That is what this page builds. By the end you will have a working FastAPI app, a verified Vercel deployment, and five core endpoints (eight total) that cover the full agent purchase flow from search to order confirmation — written once, deployed in a weekend, callable by Claude, ChatGPT, LangChain, Haystack, LlamaIndex, and any other framework that consumes OpenAPI.
§2 · Stack Choice
Why FastAPI + Vercel.
Why FastAPI over Flask, Django REST Framework, or Express
FastAPI wins on four counts specific to agent-facing APIs.
1 · OpenAPI
Automatic OpenAPI 3.x spec
FastAPI generates a live spec at /openapi.json from Pydantic models with zero configuration. Agent frameworks — LangChain, Haystack, LlamaIndex, OpenAI function calling — all consume this spec directly to generate callable tools at runtime, with no human-written wrapper code.
2 · Pydantic
Strict typed validation
Malformed payloads fail fast with structured errors the agent can parse and self-correct from. The same type declaration drives validation, serialization, and the public schema — one source of truth.
3 · Async + ergonomics
ASGI native, low boilerplate
Starlette and Uvicorn handle concurrent agent call chains efficiently. Declare parameters once, get validation, serialization, and docs from the same declaration. Less code than Flask. Less ceremony than Django.
Why Vercel
Vercel's Fluid Compute model — the default for all new projects since April 23, 2025 — eliminates most cold-start concerns. The key facts: 99.37% of requests experience zero cold starts (Vercel, Fluid Compute). The Hobby tier ships with 1,000,000 invocations per month, 4 CPU-hours, 360 GB-hours of provisioned memory, and a 60-second maximum function duration.
Critical · Hobby Is Not For Commerce
Vercel's Hobby tier explicitly prohibits commercial use (Fair Use Guidelines). If the API is generating revenue, Pro ($20/month) is required. One agent session completing a purchase makes 6–8 endpoint calls. At 50,000 agent sessions per month that is 300,000–400,000 invocations. Plan accordingly.
What breaks on Vercel serverless
| Issue |
Impact |
Workaround |
| No WebSockets |
No long-lived bidirectional connections |
Polling, or a managed pub/sub service like Pusher or Ably |
| No persistent in-memory state |
Each invocation starts fresh — no warm dictionaries, no in-process cache |
Vercel KV, Upstash Redis, or any external cache |
| 500 MB bundle limit |
Heavy dependencies (ML libs, large SDKs) fail at deploy |
Trim with excludeFiles; offload heavy compute to a separate service |
| 60-second max execution on Hobby |
Long chains of external calls (Stripe + DB + email) can time out |
Optimize queries; upgrade to Pro for 5-minute max |
| SIGTERM cleanup capped at 500 ms |
No room for heavy shutdown logic (flushing buffers, closing DB pools slowly) |
Keep shutdown minimal; rely on external connection pooling |
| No persistent DB connections |
Standard pooled connections do not survive across invocations |
Use a serverless-compatible driver, PgBouncer in transaction mode, or HTTP-based DB clients (Neon, PlanetScale, Supabase) |
§3 · The Surface
The eight endpoints that cover the full flow.
This is the minimum viable surface for an agent purchase: discovery, detail, availability, intent, rehydration, order status, and webhook push. Five are mandatory. Three (manifest, intent rehydration, webhooks) make the API resilient under real agent traffic. All eight fit in one app.py file.
| Endpoint |
Method |
Purpose |
Required Request Fields |
Required Response Fields |
Agent Notes |
/catalog/manifest |
GET |
Discovery metadata |
none |
api_version, catalog_version, endpoints[] |
Agents read this first to build context |
/products/search |
GET |
Product discovery by query + filters |
q (optional), category, price_max, in_stock_only, limit, cursor |
items[], next_cursor, total_estimate |
Support both natural-language q and structured filters; agents use this heavily |
/products/{sku} |
GET |
Full product detail |
sku (path) |
sku, title, price (integer cents), currency, available (bool), quantity, url, image_url, description |
Price must be integer cents per UCP spec; $19.99 = 1999 |
/products/{sku}/availability |
GET |
Live stock check |
sku (path) |
available, quantity, checked_at |
Re-check before every checkout intent; do not rely on cached search results |
/cart/intent |
POST |
Create checkout intent |
items[] (sku + quantity), customer_email, idempotency_key |
intent_id, status, items[], subtotal, total_cents, currency, checkout_url, expires_at |
The key agent-specific endpoint; stateless, no cookies |
/cart/intent/{intent_id} |
GET |
Rehydrate intent state |
intent_id (path) |
status, items[], totals, approval_state |
Lets the agent resume after interruption |
/orders/{order_id} |
GET |
Post-purchase status |
order_id (path) |
status, fulfillment_status, tracking, updated_at |
Status enum: pending, paid, failed, shipped |
/webhooks/order-events |
POST |
Push lifecycle updates |
signed webhook payload |
delivery / shipment / cancel events |
Use for cache invalidation and order lifecycle sync; preferred over polling for post-checkout |
Checkout Intent — request and response
The most agent-specific endpoint on the list. Three details below the JSON make this endpoint different from a browser cart.
POST /cart/intent — Request + Response
// POST /cart/intent — Request
{
"items": [
{"sku": "WOOL-SOCK-RED-M", "quantity": 2}
],
"customer_email": "agent-user@example.com",
"idempotency_key": "agent-session-abc123-attempt-1"
}
// Response (201 Created)
{
"intent_id": "intent_8j3kd9s2",
"status": "pending",
"items": [
{"sku": "WOOL-SOCK-RED-M", "quantity": 2, "unit_price": 1999, "line_total": 3998}
],
"subtotal": 3998,
"currency": "USD",
"checkout_url": "https://yourstore.com/checkout?token=8j3kd9s2",
"expires_at": "2026-05-19T14:27:00Z"
}
Three Non-Negotiables vs. a Browser Cart
1 · idempotency_key required. Prevents duplicate orders if the agent retries on timeout. The server returns the same intent_id for the same key.
2 · checkout_url is a pre-authenticated, time-limited URL. A human clicks it to pay. Agents cannot complete Stripe redirect flows — the URL is the handoff.
3 · expires_at declared explicitly. Agents must not submit stale intents. If the window passes, the agent calls /cart/intent again.
Availability — response
GET /products/{sku}/availability — Response
{
"sku": "WOOL-SOCK-RED-M",
"available": true,
"quantity": 47,
"restock_eta": null,
"checked_at": "2026-05-19T13:27:00Z"
}
§4 · The Centerpiece
The full code build.
Four code blocks below. Together they ship. No skeleton — the app.py is a complete FastAPI application you can copy into a fresh directory, install three dependencies, and deploy.
Project directory structure
project/ — file tree
project/
├─ app.py ← FastAPI app instance
├─ requirements.txt
├─ vercel.json ← optional; needed for maxDuration override
└─ README.md
app.py — the complete FastAPI application
app.py — copy-pasteable, all five core routes
"""
AgentMall Commerce API — minimal agent-ready surface.
Deploy to Vercel: a top-level `app` object in app.py is auto-detected.
OpenAPI is exposed automatically at /openapi.json — agents consume it.
"""
import os
import uuid
from datetime import datetime, timedelta, timezone
from typing import List, Optional, Literal
from fastapi import FastAPI, HTTPException, Header, Query
from pydantic import BaseModel, Field, EmailStr
app = FastAPI(
title="AgentMall Commerce API",
version="0.1.0",
description="Agent-ready commerce surface — search, intent, order.",
)
# -----------------------------------------------------------------------------
# Environment
# -----------------------------------------------------------------------------
DATABASE_URL = os.getenv("DATABASE_URL")
READ_API_KEY = os.getenv("READ_API_KEY", "dev-read-key")
WRITE_API_KEY = os.getenv("WRITE_API_KEY", "dev-write-key")
STRIPE_SECRET = os.getenv("STRIPE_SECRET_KEY")
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "https://yourstore.com")
# -----------------------------------------------------------------------------
# Pydantic models — drive OpenAPI; agent frameworks read these.
# -----------------------------------------------------------------------------
class Product(BaseModel):
sku: str = Field(..., description="Stable SKU; the agent's primary identifier.")
title: str
price: int = Field(..., description="Price in integer cents. $19.99 = 1999.")
currency: str = Field("USD", description="ISO 4217 currency code.")
available: bool
quantity: int = 0
url: str
image_url: Optional[str] = None
description: Optional[str] = None
class SearchResponse(BaseModel):
items: List[Product]
next_cursor: Optional[str] = None
total_estimate: int
class AvailabilityResponse(BaseModel):
sku: str
available: bool
quantity: int
restock_eta: Optional[datetime] = None
checked_at: datetime
class CartItemRequest(BaseModel):
sku: str
quantity: int = Field(..., ge=1, description="Whole units, at least one.")
class CartIntentRequest(BaseModel):
items: List[CartItemRequest]
customer_email: EmailStr
idempotency_key: str = Field(
..., min_length=8,
description="Agent-generated UUID per checkout attempt. Same key returns same intent_id.",
)
class CartIntentLineItem(BaseModel):
sku: str
quantity: int
unit_price: int
line_total: int
class CartIntentResponse(BaseModel):
intent_id: str
status: Literal["pending", "ready", "expired", "consumed"]
items: List[CartIntentLineItem]
subtotal: int
total_cents: int
currency: str = "USD"
checkout_url: str
expires_at: datetime
class OrderResponse(BaseModel):
order_id: str
status: Literal["pending", "paid", "failed", "shipped"]
fulfillment_status: Literal["unfulfilled", "in_transit", "delivered", "returned"]
tracking: Optional[str] = None
updated_at: datetime
class ApiError(BaseModel):
code: str
message: str
retry_recommended: bool = False
retry_after_seconds: Optional[int] = None
details: Optional[dict] = None
# -----------------------------------------------------------------------------
# Auth — Bearer API key; static credentials for agent compatibility.
# -----------------------------------------------------------------------------
def require_read(authorization: Optional[str] = Header(None)) -> None:
if not authorization or authorization != f"Bearer {READ_API_KEY}":
raise HTTPException(status_code=401, detail="Invalid or missing API key.")
def require_write(authorization: Optional[str] = Header(None)) -> None:
if not authorization or authorization != f"Bearer {WRITE_API_KEY}":
raise HTTPException(status_code=401, detail="Invalid or missing write API key.")
# -----------------------------------------------------------------------------
# Tiny in-memory catalog — replace with your DB layer.
# -----------------------------------------------------------------------------
CATALOG = {
"WOOL-SOCK-RED-M": Product(
sku="WOOL-SOCK-RED-M",
title="Merino Wool Socks — Red, Medium",
price=1999,
currency="USD",
available=True,
quantity=47,
url=f"{PUBLIC_BASE_URL}/products/wool-sock-red-m",
image_url=f"{PUBLIC_BASE_URL}/img/wool-sock-red-m.jpg",
description="80% merino wool, mid-calf height, machine washable.",
),
}
INTENTS: dict = {} # idempotency_key -> intent_id
INTENT_DB: dict = {} # intent_id -> CartIntentResponse
# -----------------------------------------------------------------------------
# Routes
# -----------------------------------------------------------------------------
@app.get("/catalog/manifest")
def catalog_manifest():
"""Discovery metadata. Agents read this first to build context."""
return {
"api_version": "0.1.0",
"catalog_version": "2026-05-19",
"endpoints": [
"/products/search",
"/products/{sku}",
"/products/{sku}/availability",
"/cart/intent",
"/cart/intent/{intent_id}",
"/orders/{order_id}",
],
"auth": {"type": "api_key", "header": "Authorization: Bearer <key>"},
"openapi_url": f"{PUBLIC_BASE_URL}/openapi.json",
}
@app.get("/products/search", response_model=SearchResponse)
def products_search(
q: Optional[str] = Query(None, description="Natural-language query."),
category: Optional[str] = None,
price_max: Optional[int] = Query(None, description="Max price in cents."),
in_stock_only: bool = False,
limit: int = Query(20, ge=1, le=100),
cursor: Optional[str] = None,
_: None = None, # auth optional for search; require_read for private catalogs
):
"""Product discovery. Supports natural-language q and structured filters."""
items = list(CATALOG.values())
if in_stock_only:
items = [p for p in items if p.available]
if price_max is not None:
items = [p for p in items if p.price <= price_max]
if q:
ql = q.lower()
items = [p for p in items if ql in p.title.lower() or ql in (p.description or "").lower()]
return SearchResponse(items=items[:limit], next_cursor=None, total_estimate=len(items))
@app.get("/products/{sku}", response_model=Product)
def products_detail(sku: str):
"""Full product detail. Price is integer cents."""
product = CATALOG.get(sku)
if not product:
raise HTTPException(status_code=404, detail=f"SKU {sku} not found.")
return product
@app.get("/products/{sku}/availability", response_model=AvailabilityResponse)
def products_availability(sku: str):
"""Live stock check. Call this before every checkout intent."""
product = CATALOG.get(sku)
if not product:
raise HTTPException(status_code=404, detail=f"SKU {sku} not found.")
return AvailabilityResponse(
sku=product.sku,
available=product.available,
quantity=product.quantity,
restock_eta=None,
checked_at=datetime.now(timezone.utc),
)
@app.post("/cart/intent", response_model=CartIntentResponse, status_code=201)
def cart_intent_create(req: CartIntentRequest):
"""
Create a checkout intent. Idempotent on idempotency_key — the same key
always returns the same intent_id. Re-validates inventory server-side.
"""
# Idempotency — return existing intent if key has been seen.
if req.idempotency_key in INTENTS:
return INTENT_DB[INTENTS[req.idempotency_key]]
line_items, subtotal = [], 0
for item in req.items:
product = CATALOG.get(item.sku)
if not product:
raise HTTPException(status_code=404, detail=f"SKU {item.sku} not found.")
if not product.available or product.quantity < item.quantity:
raise HTTPException(
status_code=409,
detail=f"SKU {item.sku} unavailable in requested quantity.",
)
line_total = product.price * item.quantity
subtotal += line_total
line_items.append(CartIntentLineItem(
sku=item.sku, quantity=item.quantity,
unit_price=product.price, line_total=line_total,
))
intent_id = f"intent_{uuid.uuid4().hex[:10]}"
intent = CartIntentResponse(
intent_id=intent_id,
status="pending",
items=line_items,
subtotal=subtotal,
total_cents=subtotal,
currency="USD",
checkout_url=f"{PUBLIC_BASE_URL}/checkout?token={intent_id}",
expires_at=datetime.now(timezone.utc) + timedelta(minutes=20),
)
INTENTS[req.idempotency_key] = intent_id
INTENT_DB[intent_id] = intent
return intent
@app.get("/cart/intent/{intent_id}", response_model=CartIntentResponse)
def cart_intent_get(intent_id: str):
"""Rehydrate intent state. Lets the agent resume after interruption."""
intent = INTENT_DB.get(intent_id)
if not intent:
raise HTTPException(status_code=404, detail=f"Intent {intent_id} not found.")
return intent
@app.get("/orders/{order_id}", response_model=OrderResponse)
def orders_get(order_id: str):
"""Post-purchase status. Status enum: pending, paid, failed, shipped."""
# Replace with your real lookup.
return OrderResponse(
order_id=order_id,
status="paid",
fulfillment_status="in_transit",
tracking="1Z999AA10123456784",
updated_at=datetime.now(timezone.utc),
)
@app.post("/webhooks/order-events", status_code=202)
def webhooks_order_events(payload: dict):
"""
Push lifecycle updates. Verify signature against your provider
(Stripe-Signature, Shopify HMAC, etc.) before trusting payload.
"""
# signature_check(payload, request.headers) # implement per provider
return {"received": True}
requirements.txt
requirements.txt
fastapi
uvicorn[standard]
pydantic
vercel.json — only for maxDuration override
vercel.json — optional
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/(.*)", "destination": "/app.py" }
],
"functions": {
"app.py": {
"maxDuration": 60
}
}
}
No vercel.json required for a basic deploy
FastAPI on Vercel deploys with zero configuration if your app exposes a top-level app object in app.py, index.py, server.py, or src/index.py. Add vercel.json only when you need to override maxDuration or add rewrites.
Environment variables — three patterns
os.getenv — the three keys you actually set
import os
DATABASE_URL = os.getenv("DATABASE_URL")
READ_API_KEY = os.getenv("READ_API_KEY")
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
Set these in Vercel Dashboard → Settings → Environment Variables. Changes apply to new deployments only — redeploy after you add them.
§5 · Authentication
Authentication agents can actually use.
OAuth Authorization Code flow is the default human-facing pattern. It also breaks agents. Below is the auth choice by use case, followed by the reason OAuth fails and the V1 default that works.
| Use Case |
Recommended Auth |
Notes |
| Simple solo-founder API |
API key in Authorization: Bearer <key> header |
Lowest friction; no redirect flows; rotate keys, not passwords |
| Multi-tenant per-user access |
OAuth 2.0 Client Credentials flow |
Machine-to-machine; no user redirect; short-lived tokens (1 hr) with rotation |
| MCP-based agent integration |
MCP auth layer |
Emerging standard as of late 2025; delegates auth to the protocol |
| Enterprise / regulated |
JWT with private-key assertion |
Asymmetric credentials; scope-limited tokens; no shared secret |
Critical · Why OAuth Breaks Agents
The standard OAuth Authorization Code flow requires a redirect to an authorization page, user consent, and a redirect back with a code. Agents cannot open browser tabs, complete CAPTCHA challenges, or receive redirect callbacks. Result: the agent stalls waiting for redirect completion, the token exchange times out, the retry loop exhausts rate limits. V1 decision: use API keys. Claude Desktop, Cursor, and LangChain all support injecting static Bearer tokens via configuration files.
§6 · Discovery
Making your API discoverable.
Three surfaces every agent-ready API must publish. The first is free with FastAPI. The second is one line of code. The third is a static JSON file at a known path.
1 · /openapi.json — automatic
FastAPI generates this from your Pydantic models with zero configuration. LangChain, Haystack, LlamaIndex, and OpenAI function calling all consume this spec to build tool definitions at runtime. Describe your Pydantic models with docstrings — FastAPI includes them verbatim in the spec, and agent frameworks use them as tool descriptions.
2 · FastAPI-MCP — one line of code
FastAPI-MCP is an open-source library by Tadata Inc., released April 2025. It converts all FastAPI routes to MCP tools automatically — with zero configuration — preserving Pydantic schemas. Once deployed, Claude can call your endpoints natively over the Model Context Protocol.
FastAPI-MCP — one-line integration
from fastapi_mcp import FastApiMCP
mcp = FastApiMCP(app) # `app` is your FastAPI instance
mcp.mount() # exposes the MCP transport on the same app
3 · /.well-known/agent.json — capability manifest
A lightweight static manifest at a known path, for any agent or service that does not consume OpenAPI directly. Serve it from your CDN or as a FastAPI route.
/.well-known/agent.json
{
"name": "Example Commerce API",
"version": "0.1.0",
"openapi_url": "https://example.com/openapi.json",
"auth": {"type": "api_key"},
"capabilities": [
"search_products", "get_product", "check_availability",
"create_checkout_intent", "get_order_status"
]
}
Agent framework consumption — what reads what
| Framework |
Mechanism |
Notes |
| LangChain |
create_openapi_agent() + reduce_openapi_spec() |
Generates a runnable agent from your live /openapi.json |
| Haystack |
OpenAPITool |
Official first-class support; one-line tool registration |
| FastAPI-MCP |
Auto-converts routes to MCP tools |
Pydantic schemas preserved as tool descriptions |
| LlamaIndex |
OpenAPIToolSpec |
Same OpenAPI consumption pattern as LangChain |
| Claude via MCP |
Native MCP protocol |
Works once FastAPI-MCP is deployed alongside FastAPI |
| OpenAI function calling |
Function definitions generated from OpenAPI schema |
Convert OpenAPI operations to OpenAI function tool definitions |
§8 · Errors For Agents
Errors agents can actually use.
Errors must be self-describing enough that the agent can decide whether and how to retry without human intervention. Four things every error must communicate: what failed, whether it is permanent or retryable, which field or state caused it, and what to do next.
Recommended error response structure
ApiError — uniform envelope on every non-2xx
{
"error": {
"code": "OUT_OF_STOCK",
"message": "SKU WOOL-SOCK-RED-M has 0 units available. Restock expected 2026-05-25.",
"retry_recommended": false,
"retry_after_seconds": null,
"details": {
"sku": "WOOL-SOCK-RED-M",
"requested_quantity": 2,
"available_quantity": 0
}
}
}
HTTP status taxonomy — what the agent should do
| HTTP Status |
When to Use |
Agent Behavior |
| 400 Bad Request |
Malformed request, missing field |
Do not retry; fix the request |
| 401 Unauthorized |
Missing or invalid API key |
Do not retry; rotate credentials |
| 403 Forbidden |
Authenticated but not allowed |
Do not retry; report to user |
| 404 Not Found |
SKU, intent, or order does not exist |
Do not retry; report to user |
| 409 Conflict |
Inventory changed, intent expired, state conflict |
Do not retry blindly; re-search or re-check availability |
| 422 Unprocessable |
Schema validation failure (FastAPI auto-generates these) |
Do not retry; fix the request shape |
| 429 Too Many Requests |
Rate limited; must include Retry-After |
Retry after the declared interval |
| 503 Service Unavailable |
Temporary upstream failure |
Retry with exponential backoff; retry_recommended: true |
Rate limit headers — what to send
Response headers on every request
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1716126000
Retry-After: 30 (429 responses only)
Agents parse these headers automatically. Recommended starting limits: 100 req/min for GET (read) endpoints, 20 req/min for POST (write) endpoints. If an agent makes more than 50 calls in a 60-second window it is likely in a retry loop — return 429 with Retry-After: 300.
§9 · FAQ
Eight questions builders actually ask.
1. Do I need all eight endpoints, or can I start with fewer?
Three endpoints make agents functional: search, product detail, and checkout intent. Availability and order status are required before going live with real purchases. Manifest and webhooks can follow. Ship the minimum, instrument it, add what agent behavior reveals you need.
2. Why can't I just use my Shopify storefront API instead of building this?
You can proxy Shopify's API through FastAPI — that is a valid pattern. Building the proxy gives you three things Shopify's raw API doesn't: exact field shaping for agent consumption (fewer tokens, cleaner types), rate-limit insulation between your agent traffic and Shopify's limits, and a stable internal schema that doesn't break when Shopify updates its API version.
3. Does the idempotency key actually matter?
Yes. Agents retry on timeout. Without an idempotency key, a slow Stripe call plus an agent timeout plus an agent retry equals a duplicate order. The pattern: the agent generates a UUID per checkout attempt and sends it as idempotency_key. Your API returns the same intent_id for the same key. One checkout, not two.
4. How do I prevent agents from buying out-of-stock items?
Three-layer defense: (1) include available: boolean in every search result so agents filter before selecting; (2) expose a dedicated /products/{sku}/availability endpoint agents call before intent creation; (3) validate inventory again server-side at intent creation time and return 409 Conflict if stock depleted between search and checkout.
5. When should I upgrade from Vercel Hobby to Pro?
The moment the API generates revenue — Vercel's Fair Use Guidelines explicitly prohibit commercial use on Hobby. Practically: a single agent session completing a purchase makes 6–8 endpoint calls. At 50,000 agent sessions per month that is 300,000–400,000 invocations. Pro is $20/month and adds a 5-minute max function duration, which matters once your checkout intent calls Stripe and a live database in sequence.
6. Do I need OAuth, or are API keys enough?
API keys are enough for V1. OAuth Authorization Code flow requires redirect callbacks agents cannot complete. Use API keys for read, search, and catalog access, and per-agent keys for write operations (checkout intent, order management). Add OAuth 2.0 Client Credentials only when you need per-user identity linking — loyalty accounts, saved addresses, purchase history.
7. How does FastAPI-MCP work and do I need it?
FastAPI-MCP is an open-source library that converts your FastAPI routes into MCP tools automatically — zero additional configuration. Once deployed, Claude and other MCP clients can call your commerce API natively, without an OpenAPI integration step. It is not required, but if Claude is one of your target agent clients, it is the fastest path from "working API" to "Claude can shop it."
8. What is the first thing that will break as traffic grows?
Not routing. It is state: inventory freshness between search and checkout, auth separation between read and write keys, webhook reliability for post-purchase events, and multi-step latency compounding across 6–8 sequential tool calls. Instrument those four things before anything else.