All Guides
advanced

Building AI Shopping Agents with UCP

Advanced guide to building AI agents that shop on behalf of users using UCP — covering agent architecture, discovery, authentication, transaction execution, and order tracking.

Published 2026-02-20Updated 2026-02-28

Building AI Shopping Agents with UCP

This guide covers the architecture and implementation of AI agents that can discover products, compare prices, and execute purchases on behalf of users through the Universal Commerce Protocol. This is not a beginner tutorial — it assumes familiarity with UCP's core concepts, experience building LLM-powered applications, and working knowledge of TypeScript.

Agent Architecture Overview

A UCP shopping agent has four layers:

  1. Natural Language Layer — interprets user intent ("find me a good deal on running shoes, size 11") and translates it into structured commerce operations.
  2. Commerce Orchestration Layer — manages the workflow of discovering merchants, comparing products, and coordinating checkout across multiple stores.
  3. UCP Protocol Layer — handles the actual UCP API calls: discovery, session management, payment token exchange.
  4. State & Memory Layer — tracks user preferences, past orders, linked merchant accounts, and active sessions.

The key architectural decision is how tightly to couple the LLM with the commerce logic. The recommended pattern is a tool-calling architecture where the LLM has access to a set of typed commerce tools, and the orchestration layer validates and executes the tool calls.

Defining Commerce Tools

Define a tool schema that your LLM can invoke. Here's a minimal set:

import { z } from 'zod';

export const commerceTools = {
  searchProducts: {
    description: 'Search for products across UCP-enabled merchants',
    parameters: z.object({
      query: z.string(),
      category: z.string().optional(),
      maxPrice: z.number().optional(),
      currency: z.string().default('USD'),
      merchantSlugs: z.array(z.string()).optional(),
    }),
  },
  getProductDetails: {
    description: 'Get full details for a specific product',
    parameters: z.object({
      merchantEndpoint: z.string().url(),
      productId: z.string(),
    }),
  },
  createCheckout: {
    description: 'Start a checkout session with a merchant',
    parameters: z.object({
      merchantEndpoint: z.string().url(),
      items: z.array(z.object({
        productId: z.string(),
        quantity: z.number().int().positive(),
      })),
      shippingAddressId: z.string(),
    }),
  },
  confirmCheckout: {
    description: 'Confirm a checkout session and process payment',
    parameters: z.object({
      sessionId: z.string(),
      shippingOptionId: z.string(),
      paymentMethodId: z.string(),
    }),
  },
  getOrderStatus: {
    description: 'Check the status of a previous order',
    parameters: z.object({
      orderId: z.string(),
      merchantEndpoint: z.string().url(),
    }),
  },
};

Merchant Discovery and Product Search

Your agent needs to know which merchants exist and what they sell. There are two approaches:

Static Registry

Maintain a local registry of known UCP merchants and their endpoints. You can bootstrap this from the UCPList.ai directory API or the UCP Foundation's public registry. This is fast but requires periodic updates.

type MerchantProfile = {
  ucp?: { capabilities?: Record<string, unknown> };
};

class MerchantRegistry {
  private merchants = new Map<string, MerchantProfile>();

  async refresh(merchantUrls: string[]): Promise<void> {
    const results = await Promise.allSettled(
      merchantUrls.map(async origin => {
        const response = await fetch(new URL('/.well-known/ucp', origin));
        if (!response.ok) throw new Error(`Discovery failed: ${response.status}`);
        return response.json() as Promise<MerchantProfile>;
      }),
    );

    for (const [i, result] of results.entries()) {
      if (result.status === 'fulfilled') {
        this.merchants.set(merchantUrls[i], result.value);
      }
    }
  }

  getByCapability(capability: string): MerchantProfile[] {
    return [...this.merchants.values()].filter(
      profile => capability in (profile.ucp?.capabilities ?? {}),
    );
  }
}

Dynamic Discovery

For broader coverage, implement a crawl-and-discover pipeline. UCP business profiles are served from /.well-known/ucp, so you can probe a candidate domain:

async function probeForUCP(domain: string): Promise<MerchantProfile | null> {
  try {
    const response = await fetch(`https://${domain}/.well-known/ucp`);
    if (!response.ok) return null;
    return response.json() as Promise<MerchantProfile>;
  } catch {
    return null;
  }
}

In practice, you'll combine both approaches: a curated registry for reliability, supplemented by dynamic discovery when users request specific merchants.

Handling Authentication and User Consent

This is the most critical part of agent design. Your agent is spending real money on behalf of real people. The consent model must be airtight.

Identity Token Management

When a user links their merchant account to your agent via UCP identity linking, you receive a scoped token. Store these tokens securely and associate them with the user:

interface UserCommerceProfile {
  userId: string;
  linkedAccounts: {
    merchantDomain: string;
    identityToken: string;
    scopes: string[];
    linkedAt: string;
    expiresAt: string;
  }[];
  paymentMethods: {
    id: string;
    type: 'card' | 'bank' | 'wallet';
    last4: string;
    isDefault: boolean;
  }[];
  shippingAddresses: {
    id: string;
    label: string;
    address: ShippingAddress;
    isDefault: boolean;
  }[];
}

Consent Checkpoints

Never auto-confirm a purchase without explicit user approval. Implement consent checkpoints at two stages:

  1. Pre-checkout consent — before creating a checkout session, present the items and estimated total to the user and get a "yes, proceed" signal.
  2. Payment consent — after the session is confirmed and you have the final total (including merchant-calculated tax and shipping), present the exact charge amount and get a second confirmation.
async function executeCheckoutWithConsent(
  agent: ShoppingAgent,
  user: UserCommerceProfile,
  items: CartItem[],
  merchantEndpoint: string,
): Promise<OrderConfirmation> {
  // Stage 1: Pre-checkout consent
  const estimate = await agent.estimateTotal(merchantEndpoint, items);
  const preApproval = await agent.requestUserConsent({
    type: 'pre_checkout',
    message: `Purchase ${items.length} item(s) from ${merchantEndpoint} for approximately ${formatCurrency(estimate.total)}?`,
    details: { items, estimate },
  });

  if (!preApproval.approved) {
    throw new AgentError('USER_DECLINED', 'User declined pre-checkout consent');
  }

  // Create session
  const session = await agent.ucpClient.checkout.createSession({
    merchantEndpoint,
    lineItems: items.map(i => ({
      merchantProductId: i.productId,
      quantity: i.quantity,
    })),
    shippingAddress: user.shippingAddresses.find(a => a.isDefault)!.address,
    identityToken: user.linkedAccounts.find(
      a => merchantEndpoint.includes(a.merchantDomain),
    )?.identityToken,
  });

  // Stage 2: Payment consent with exact total
  const paymentApproval = await agent.requestUserConsent({
    type: 'payment',
    message: `Confirm payment of ${formatCurrency(session.total)} (includes ${formatCurrency(session.tax)} tax, ${formatCurrency(session.shippingCost)} shipping)?`,
    details: { session },
    requireExplicitAmount: true,
  });

  if (!paymentApproval.approved) {
    // Cancel the session to free up any inventory holds
    await agent.ucpClient.checkout.cancelSession(session.sessionId);
    throw new AgentError('USER_DECLINED', 'User declined payment consent');
  }

  // Execute payment
  const confirmation = await agent.ucpClient.checkout.confirmSession({
    sessionId: session.sessionId,
    selectedShippingOption: session.shippingOptions[0].id,
  });

  const payment = await agent.paymentHandler.processToken({
    paymentToken: confirmation.paymentToken,
    paymentMethodId: user.paymentMethods.find(m => m.isDefault)!.id,
  });

  return {
    orderId: payment.transactionId,
    total: confirmation.total,
    status: 'confirmed',
  };
}

Multi-Merchant Comparison

A powerful agent capability is comparing products across multiple merchants. Implement this as a fan-out/fan-in pattern:

async function compareAcrossMerchants(
  registry: MerchantRegistry,
  query: string,
  maxResults: number = 5,
): Promise<ProductComparison[]> {
  const merchants = registry.getByCapability('checkout');

  // Fan out: search all merchants in parallel
  const searchResults = await Promise.allSettled(
    merchants.map(m =>
      client.products.search(m.endpoint, { query, limit: maxResults }),
    ),
  );

  // Fan in: normalize and rank results
  const allProducts: NormalizedProduct[] = [];
  for (const [i, result] of searchResults.entries()) {
    if (result.status === 'fulfilled') {
      for (const product of result.value.items) {
        allProducts.push({
          ...normalizeProduct(product),
          merchantName: merchants[i].name,
          merchantEndpoint: merchants[i].endpoint,
        });
      }
    }
  }

  // Deduplicate by product identifier (GTIN, UPC, or fuzzy title match)
  return deduplicateAndRank(allProducts);
}

Order Tracking and Post-Purchase

After a purchase, your agent should track the order and proactively notify the user:

class OrderTracker {
  private activeOrders: Map<string, TrackedOrder> = new Map();

  async handleWebhook(event: OrderEvent): Promise<void> {
    const order = this.activeOrders.get(event.orderId);
    if (!order) return;

    switch (event.type) {
      case 'order.shipped':
        await this.notifyUser(order.userId, {
          message: `Your order from ${order.merchantName} has shipped! Tracking: ${event.data.trackingUrl}`,
          action: 'track_package',
          data: event.data,
        });
        break;

      case 'order.delivered':
        await this.notifyUser(order.userId, {
          message: `Your order from ${order.merchantName} has been delivered.`,
          action: 'confirm_delivery',
        });
        this.activeOrders.delete(event.orderId);
        break;

      case 'order.exception':
        await this.notifyUser(order.userId, {
          message: `There's an issue with your order from ${order.merchantName}: ${event.data.reason}`,
          action: 'contact_support',
          urgent: true,
        });
        break;
    }
  }
}

Security Considerations

Building a shopping agent means handling sensitive user data and real financial transactions. Key security requirements:

  • Encrypt identity tokens at rest. Use envelope encryption with a KMS. Never log tokens.
  • Validate payment amounts server-side. Never trust the LLM's calculation of a total. Always use the merchant-confirmed amount from the checkout session.
  • Implement spending limits. Let users configure per-transaction and daily spending caps. Your agent should enforce these before reaching the payment consent stage.
  • Audit log everything. Every tool call, every consent decision, every UCP API call should be logged with timestamps and user context. You'll need this for dispute resolution.
  • Rate limit agent actions. Prevent runaway loops where the LLM repeatedly retries failed checkouts. Implement circuit breakers that halt after a configurable number of failures.

UCP provides the protocol layer, but the trust layer is your responsibility. Users are delegating purchasing power to your agent — treat that delegation with the seriousness it deserves.

Explore the UCP Ecosystem

Browse our directory of UCP tools, merchants, and platforms.

Browse Directory