Spoke 5 / 8
AgentMall · Spoke 5 of 8

Build an Agent-Ready Commerce API in a Weekend — FastAPI, Vercel, and the Endpoints AI Agents Actually Call.

A product page with perfect Schema.org markup is still invisible to an agent making a direct API call. The Schema layer gets your catalog discovered. This API layer gets it purchased. By the end of this page you will have a working FastAPI app, a verified Vercel deployment, and five endpoints that cover the full agent purchase flow from search to order confirmation.

8
Endpoints to Ship
2 days
Weekend Build
99.37%
Zero Cold Starts on Vercel
0
Config Lines Required
§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
§7 · Where to Run It

Deployment stack — what fits your traffic shape.

Vercel is the weekend default. The table below is the honest tradeoff matrix once you start measuring real agent latency.

Platform Cold Start Free Tier Limit FastAPI Compatible Agent Traffic Notes Best For
Vercel Fluid Compute: zero cold starts for 99.37% of requests Hobby: 1M invocations, 360 GB-hrs, 60 s max; commercial use requires Pro ($20/mo) Yes (native) Excellent for bursty / low-volume; watch 60 s timeout on complex flows Weekend deploy, zero DevOps
Render ~1 minute spin-up after 15 min idle Free web service + paid Starter $7/mo Yes (Docker) 1-min cold start can break agent connections Persistent-style app ergonomics
Railway No persistent cold start Trial: $5 credit; Hobby $5/mo minimum Yes (Docker) Good when you want fewer serverless constraints Fast move to persistent
Fly.io ~61 ms average (OpenStatus 6-region benchmark) No true free tier Yes Strong geo / control; billing has sharp edges Persistent low-latency global
AWS Lambda No single official figure 1M requests + 400,000 GB-seconds free Yes (with adapter) Most flexible long-term; most setup overhead When you outgrow platform abstraction
Cloudflare Workers Very low; edge native Generous free tier Not directly (Python story is not standard FastAPI) Great for edge handlers; poor fit for a Python FastAPI weekend build Edge-first APIs
Multi-Step Latency Math

A single endpoint at 300 ms is acceptable. A 6-step agent purchase flow — search → detail → availability → intent → approval → order confirmation — at 300 ms each is 1.8 seconds of API latency before model overhead. The same flow on a warm persistent server at 50 ms is 0.3 seconds. Once the flow is stable, measure end-to-end multi-step tool latency, not just single-endpoint p50.

§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.

§10 · The Window

The agent commerce stack is being written this quarter.

The agent commerce infrastructure stack is being written right now — FastAPI, Vercel, MCP, and UCP are all post-2024 developments. The window between "possible to build" and "every platform already ships it" is the same window mobile developers had in 2010 and API developers had in 2008. The founders who build the agent-ready commerce surface this quarter own the traffic before it becomes a checkbox on a platform migration guide.

Get the AgentMall 30-Day Roadmap →
AgentMall · 8 Spokes

Get the next spoke when it drops.

AgentMall is an 8-part build guide for selling to AI agents. New spokes ship every few weeks. Drop your email and we'll tell you when the next one is live — no pitch, no sequence, just the update.