For background on how these systems fit into the broader landscape, see our primer on what is agentic commerce and the technical deep-dive on how AI shopping agents work.


Prerequisites

Before writing a single line of agent code, confirm you have three things in place.

1. An LLM with Native Tool Use

Your agent needs a model that supports structured tool/function calling — not just chat completion. Models that work well for this pattern as of mid-2026:

Model Tool Use Context Window Notes
Claude Sonnet 4.x Native 200K tokens Strong instruction-following; good for complex system prompts
GPT-4o Native 128K tokens Wide ecosystem support
Gemini 1.5 Pro Native 1M tokens Useful for long product catalogs in-context
Llama 3.1 70B (self-hosted) Via prompt 128K tokens No API cost, but tool parsing is less reliable

Avoid using models that require prompt-engineering hacks to simulate tool calls in production — the failure rate under edge cases is too high.

2. Payment Credentials

You need:

Do not attempt to build checkout execution without idempotency keys. Every Payment Intent creation must include one. See AI agent payment APIs for a detailed treatment of the payment layer.

3. Product API Access

You need at least one product data source. The three most common:

Source Access Method Cost Best For
Shopify Storefront API OAuth or public token Free (requires merchant) Your own or partner stores
Amazon Product Advertising API Approved affiliate account Free (affiliate model) Broad catalog coverage
Google Shopping / Merchant Center API Google Cloud project Free quota, then pay-as-go Price comparison, availability

Start with one source. Multi-source federation adds complexity that should come after the core loop is working.


Architecture Overview

A minimal AI shopping agent has five runtime components and two infrastructure dependencies.

User Request
     │
     ▼
┌─────────────────┐
│  Intent Parser  │  ← LLM with system prompt + tool definitions
└────────┬────────┘
         │ structured intent object
         ▼
┌─────────────────┐
│  Product Search │  ← Tool call → external product API
└────────┬────────┘
         │ raw results array
         ▼
┌─────────────────┐
│ Eval & Ranking  │  ← LLM call or deterministic scoring function
└────────┬────────┘
         │ ranked shortlist (top 3-5 items)
         ▼
┌─────────────────┐
│Checkout Execute │  ← Stripe Payment Intents API
└────────┬────────┘
         │ payment_intent object
         ▼
┌─────────────────┐
│  Confirmation   │  ← Email/webhook/push notification
└─────────────────┘

Infrastructure dependencies:
- State store (Redis or DynamoDB) for session continuity
- Secret manager (AWS Secrets Manager, Vault) for API keys

The agent loop is synchronous within a single turn but the overall transaction can span multiple turns if confirmation is required before checkout. Plan for both single-shot (fully automated) and human-in-the-loop variants.


Step 1: Intent Parsing — System Prompt Design

Intent parsing is where most build AI shopping agent tutorial projects go wrong. Developers underinvest in the system prompt and then paper over ambiguity with code. The LLM should do the heavy lifting here.

System Prompt Structure

SYSTEM:
You are a shopping agent. Your job is to convert user shopping requests
into structured search parameters. You have access to the following tools:
  - search_products(query, filters, sort_by, limit)
  - get_product_details(product_id)
  - execute_checkout(product_id, quantity, payment_method_id, idempotency_key)
  - send_confirmation(order_id, channel)

Rules:
1. Never call execute_checkout without explicit user confirmation
   unless the session flag `auto_purchase_enabled` is true.
2. If the user's request is ambiguous (no size, no color, no price ceiling),
   call search_products first and ask the user to select from results
   before proceeding.
3. Extract budget constraints as numeric values. "under fifty dollars"
   → max_price: 50. "cheap" → sort_by: "price_asc", no price cap.
4. If the user mentions a specific brand, add brand as a filter.
   Do not infer brand preference from context.
5. Always return a structured intent object before calling any tool.

Intent Object Schema

Define this as a JSON schema and pass it in your tool definition:

{
  "intent": {
    "product_type": "string",
    "keywords": ["string"],
    "filters": {
      "max_price": "number | null",
      "min_price": "number | null",
      "brand": "string | null",
      "color": "string | null",
      "size": "string | null",
      "condition": "new | used | refurbished | null"
    },
    "sort_by": "relevance | price_asc | price_desc | rating | null",
    "quantity": "integer",
    "urgency": "immediate | flexible | null"
  }
}

Handling Ambiguity

The agent must distinguish between three states:

Add a confidence_score to your intent object (0.0–1.0). Any intent with confidence below 0.7 should trigger a clarification turn rather than proceeding to search.


The product search tool is a standard function your LLM calls via its tool-use interface. The implementation is straightforward; the tricky parts are normalization and error handling.

Tool Definition (OpenAI/Anthropic compatible)

{
  "name": "search_products",
  "description": "Search for products matching the user's intent. Returns a list of product objects with price, availability, and metadata.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "max_price": { "type": "number" },
      "min_price": { "type": "number" },
      "brand": { "type": "string" },
      "sort_by": {
        "type": "string",
        "enum": ["relevance", "price_asc", "price_desc", "rating"]
      },
      "limit": { "type": "integer", "default": 20 }
    },
    "required": ["query"]
  }
}

Shopify Storefront API

POST https://{shop}.myshopify.com/api/2024-04/graphql.json
X-Shopify-Storefront-Access-Token: {token}

query SearchProducts($query: String!, $first: Int) {
  search(query: $query, first: $first, types: PRODUCT) {
    edges {
      node {
        ... on Product {
          id
          title
          handle
          priceRange { minVariantPrice { amount currencyCode } }
          availableForSale
          vendor
          rating: metafield(namespace: "reviews", key: "rating") { value }
        }
      }
    }
  }
}

Amazon Product Advertising API

GET https://webservices.amazon.com/paapi5/searchitems
Headers: {
  "X-Amz-Target": "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems"
}
Body: {
  "Keywords": "{query}",
  "Resources": [
    "ItemInfo.Title",
    "Offers.Listings.Price",
    "CustomerReviews.StarRating"
  ],
  "PartnerTag": "{your-associate-tag}",
  "PartnerType": "Associates",
  "Marketplace": "www.amazon.com",
  "SortBy": "Price:LowToHigh"
}

Note: Amazon's PA API requires a minimum of one sale every 30 days to keep access active. Build this constraint into your production runbook.

Google Shopping via Content API

GET https://shoppingcontent.googleapis.com/content/v2.1/{merchant_id}/products
Authorization: Bearer {oauth_token}
?q={query}&maxResults=20&country=US&language=en

Normalizing Results

All three APIs return different schemas. Normalize to a common product object before returning to the LLM:

{
  "product_id": "string",
  "source": "shopify | amazon | google",
  "title": "string",
  "price_cents": "integer",
  "currency": "string",
  "availability": "in_stock | out_of_stock | limited",
  "url": "string",
  "image_url": "string",
  "rating": "float | null",
  "review_count": "integer | null",
  "seller": "string"
}

Normalization happens in your tool handler, not in the LLM. Pass only normalized objects back to the model.


Step 3: Evaluation and Ranking Logic

Raw search results from a product API are not in the optimal order for your user's stated intent. A user asking for "the best value AA batteries" needs a different ranking than one asking for "the highest-rated AA batteries from a brand I trust."

Scoring Function

A deterministic scoring function is more predictable than asking the LLM to rank — use it as the default, with LLM re-ranking as an optional second pass for complex preference matching.

def score_product(product, intent):
    score = 0.0

    # Price fit (40% weight)
    if intent.max_price:
        price_ratio = product.price_cents / (intent.max_price * 100)
        if price_ratio <= 1.0:
            score += 0.4 * (1.0 - price_ratio * 0.5)  # reward lower prices
        else:
            return -1  # exclude over-budget items entirely

    # Rating signal (30% weight)
    if product.rating and product.review_count > 10:
        score += 0.3 * (product.rating / 5.0)

    # Availability (20% weight)
    availability_scores = {"in_stock": 1.0, "limited": 0.6, "out_of_stock": 0.0}
    score += 0.2 * availability_scores.get(product.availability, 0.0)

    # Brand match (10% weight)
    if intent.brand and intent.brand.lower() in product.seller.lower():
        score += 0.1

    return score

Return the top 5 results to the LLM after scoring. More than 5 risks overwhelming the context and leading to poor selection.

When to Use LLM Re-ranking

LLM re-ranking adds latency (one additional inference call) but earns its cost when:

Pass the top 10 scored results to the LLM with the original user message and ask it to select and explain its top 3 picks.


Step 4: Checkout Execution — Stripe Payment Intents

This is the highest-stakes step. Read our full treatment of AI checkout automation before going to production. The summary: use Payment Intents, not Charges, and always pass an idempotency key.

Payment Intent Creation Pattern

POST https://api.stripe.com/v1/payment_intents
Authorization: Bearer {STRIPE_SECRET_KEY}
Idempotency-Key: {session_id}:{product_id}:{timestamp_minute}
Content-Type: application/x-www-form-urlencoded

amount={price_in_cents}
&currency=usd
&customer={stripe_customer_id}
&payment_method={saved_payment_method_id}
&confirm=true
&return_url=https://agenticcommerce.report/order-complete
&metadata[agent_session_id]={session_id}
&metadata[product_id]={product_id}
&metadata[product_source]={source}

Idempotency Key Construction

import hashlib
import time

def build_idempotency_key(session_id: str, product_id: str) -> str:
    # Bucket by minute so retries within the same minute reuse the same key
    minute_bucket = int(time.time() / 60)
    raw = f"{session_id}:{product_id}:{minute_bucket}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]

This prevents double-charges when the agent retries a failed network call.

Authorization vs. Capture

For agent-initiated purchases where the user isn't watching in real time, consider capture-later flow:

POST /v1/payment_intents
capture_method=manual

This authorizes the card but holds capture until your fulfillment system confirms inventory. Capture within 7 days or the authorization expires.

Handling Decline Codes

RETRYABLE_DECLINE_CODES = {"insufficient_funds", "card_velocity_exceeded"}
TERMINAL_DECLINE_CODES = {"card_not_supported", "do_not_honor", "lost_card", "stolen_card"}

def handle_stripe_error(error):
    code = error.get("decline_code") or error.get("code")
    if code in RETRYABLE_DECLINE_CODES:
        return {"action": "notify_user", "message": "Payment declined. Please check your balance."}
    elif code in TERMINAL_DECLINE_CODES:
        return {"action": "block_and_notify", "message": "Card permanently declined."}
    else:
        return {"action": "log_and_escalate", "message": "Unexpected payment error."}

Never silently retry terminal declines. Never expose raw Stripe error messages to end users.


Step 5: Confirmation and Notification

A completed payment is not a completed agent run. The user needs to know what happened, and your system needs a durable record.

Order Record Structure

{
  "order_id": "uuid",
  "agent_session_id": "string",
  "payment_intent_id": "pi_...",
  "product_id": "string",
  "product_source": "shopify | amazon | google",
  "amount_cents": "integer",
  "currency": "string",
  "status": "confirmed | failed | refunded",
  "created_at": "ISO 8601",
  "user_id": "string",
  "notification_sent": "boolean"
}

Write this record to your database before sending any notification. If the notification fails, you have a record to retry from. If you write it after and the process crashes, you have a phantom charge with no record.

Notification Channels

async def send_confirmation(order, channel="email"):
    message = f"""
    Order confirmed: {order.product_title}
    Amount: ${order.amount_cents / 100:.2f}
    Order ID: {order.order_id}
    Payment: {order.payment_intent_id}
    """

    if channel == "email":
        await send_email(
            to=order.user_email,
            subject=f"Your order #{order.order_id} is confirmed",
            body=message
        )
    elif channel == "webhook":
        await post_webhook(order.webhook_url, {"event": "order.confirmed", "data": order.dict()})
    elif channel == "push":
        await send_push(order.device_token, title="Order confirmed", body=message[:100])

Always include the payment intent ID in confirmation messages. Users will need it for disputes.


Common Mistakes and Failure Modes

Mistake Consequence Fix
No idempotency key on Payment Intents Double-charges on retry Always generate and pass idempotency keys
LLM ranking without score bounds Agent recommends out-of-budget items Filter before passing to LLM, never after
Missing confirmation gate Agent executes unauthorized purchases Default auto_purchase_enabled=false; require explicit opt-in
Storing Stripe keys in env files checked into git Credential leak Use a secrets manager; rotate on any exposure
No timeout on product API calls Agent hangs indefinitely Set 5-second timeout on all outbound calls
Retrying terminal payment declines User frustration, potential fraud flag Map decline codes explicitly; halt on terminal codes
Returning raw API errors to LLM LLM may hallucinate recovery steps Normalize all errors to structured error objects before returning
No rate limit handling 429 errors crash the agent loop Implement exponential backoff on all external API calls

Testing Your Agent

Unit Tests

Test each tool in isolation before wiring the agent loop:

def test_search_products_price_filter():
    results = search_products(query="headphones", max_price=50)
    assert all(r.price_cents <= 5000 for r in results)

def test_score_product_excludes_over_budget():
    product = Product(price_cents=6000, rating=4.8, availability="in_stock")
    intent = Intent(max_price=50)
    assert score_product(product, intent) == -1

def test_idempotency_key_stability():
    key1 = build_idempotency_key("sess_123", "prod_abc")
    key2 = build_idempotency_key("sess_123", "prod_abc")
    assert key1 == key2  # same key within the same minute bucket

Integration Tests with Stripe Test Mode

Use Stripe's test card numbers to exercise all decline scenarios:

Card Number Behavior
4242 4242 4242 4242 Always succeeds
4000 0000 0000 9995 Always declines (insufficient funds)
4000 0000 0000 0069 Expired card
4100 0000 0000 0019 Flagged as fraudulent

Set STRIPE_SECRET_KEY=sk_test_... and run your full agent loop against each scenario.

Adversarial Prompt Tests

Test your intent parser with malformed inputs:

Load Testing

Before production, run a simulated burst: 50 concurrent agent sessions, each triggering search + rank + checkout. Verify:


Deploying to Production

Infrastructure Checklist

Session State Management

Each agent session needs persistent state across turns:

{
  "session_id": "uuid",
  "user_id": "string",
  "auto_purchase_enabled": false,
  "confirmed_payment_method_id": "pm_...",
  "search_results_cache": [...],
  "selected_product_id": "string | null",
  "turn_count": 3,
  "expires_at": "ISO 8601"
}

Store in Redis with a TTL. Do not store session state in the LLM context — it is not durable and will not survive a process restart.

Observability

Instrument these events as structured logs:

intent_parsed        {session_id, confidence, product_type}
search_executed      {session_id, source, result_count, latency_ms}
products_ranked      {session_id, top_product_id, score}
checkout_initiated   {session_id, product_id, amount_cents}
payment_completed    {session_id, payment_intent_id, status}
notification_sent    {session_id, channel, success}

Trace every session end-to-end with a trace_id that spans all events. This is the minimum viable observability for debugging production failures.


ACP Protocol Enrollment

The Agent Commerce Protocol (ACP) is an emerging open standard for agent-to-merchant authentication and transaction signaling. Enrolling your agent allows merchants to verify the agent's identity before accepting programmatic purchases — reducing fraud and unlocking agent-specific pricing tiers.

What ACP Provides

Enrollment Steps

  1. Register your agent at the ACP directory (currently in beta). You receive an agent_id and a private signing key.
  2. Sign each checkout request with your agent identity:
import jwt
import time

def sign_acp_request(agent_id: str, private_key: str, payload: dict) -> str:
    return jwt.encode(
        {
            "iss": agent_id,
            "iat": int(time.time()),
            "exp": int(time.time()) + 300,  # 5-minute window
            "payload": payload
        },
        private_key,
        algorithm="RS256"
    )
  1. Pass the signed token in checkout requests to ACP-enrolled merchants:
POST /checkout
X-ACP-Agent-Token: {signed_jwt}
X-ACP-Agent-ID: {agent_id}
  1. Handle merchant verification failures gracefully — fall back to standard checkout if the merchant is not ACP-enrolled.

ACP enrollment is not required for a functioning agent, but it is the direction the industry is moving. See AI agent payment APIs for more detail on where authentication standards are heading.


Frequently Asked Questions

Do I need a different LLM for each step, or can one model handle the whole agent loop?

One model can handle the full loop. The intent parser, product ranker, and confirmation steps are all prompt-driven tool calls against the same model. The only reason to split models is cost optimization — you can use a smaller, cheaper model for the ranking step if you've already extracted a structured scored list from a deterministic function.

How do I handle users who want to compare products before buying?

Build a comparison_mode flag in your session state. When the user asks to compare, surface a structured table (title, price, rating, key specs) and wait for an explicit selection before proceeding to checkout. Do not auto-select even the highest-scoring product in comparison mode.

What if the product API returns zero results?

Return a structured empty result with a reason code, not an LLM-generated explanation. Your agent should detect result_count: 0 and ask the user to broaden filters before retrying. Set a maximum of two automatic retry attempts with relaxed filters before surfacing the failure to the user.

Can the agent handle subscriptions or recurring purchases?

Yes, but it requires Stripe's Subscription or recurring Payment Intent setup, not a one-time Payment Intent. You also need explicit user consent for recurring charges stored in your database — agent memory alone is not sufficient consent documentation for recurring billing.

How do I prevent the agent from being manipulated into unauthorized purchases?

The confirmation gate is your primary control. Supplement it with: (1) a spending limit per session configurable by the user, (2) a cooldown period between purchases, (3) logging all execute_checkout calls with the full session context for audit. Never let the LLM override the confirmation gate based on instructions in the user message.

What is the right timeout for the full agent loop?

End-to-end, budget 10 seconds for search + rank + confirmation display. Payment execution should complete within 5 seconds (Stripe's P99 is well under this). If any step exceeds its timeout, surface a user-facing error and log the failure with full context. Do not let the agent silently retry indefinitely.

How should I handle currency and cross-border purchases?

Normalize all prices to the user's local currency before display and before checkout. Pass the currency code explicitly in every Payment Intent (currency=eur, not inferred). If your product APIs return prices in multiple currencies, pick one source of truth per session and convert at the start.

What logging do I need to satisfy a payment dispute?

At minimum: the full session transcript, the intent object that triggered checkout, the product object selected, the idempotency key used, the Payment Intent ID, and the timestamp of every state transition. Store this in append-only storage (S3 versioned bucket or equivalent) for 18 months minimum to cover chargeback windows.