Agent Commerce mit AP2 und UCP sichern

1. Übersicht

In diesem Codelab führen Sie einen ADK-Agenten aus, der Kinokarten bei mehreren Kinobetreibern über zwei Open-Source-Commerce-Protokolle bucht:

  • UCP (Universal Commerce Protocol): Ein Standard für Agenten, um Händler zu finden, Kataloge zu durchsuchen und Kaufabwicklungen zu verwalten.
  • AP2 (Agent Payments Protocol): Ein Protokoll für die sichere, überprüfbare Zahlungsautorisierung mit kryptografisch signierten Mandaten.

Die Demo-App CineAgent stellt eine Verbindung zu zwei fiktiven Kinohändlern mit unterschiedlichen Funktionen (Sitzplatzauswahl, spezielle Formate und Zahlungsmethoden) her und orchestriert den gesamten Buchungsprozess von der Suche bis zur Zahlung.

Lerninhalte

  • So funktioniert die Händlersuche über /.well-known/ucp-Profile in UCP
  • So verwendet ein ADK-Agent UCP, um Kataloge zu durchsuchen und Check-outs zu erstellen
  • Wie AP2-Mandate (CartMandate, PaymentMandate) Transaktionen sichern
  • So schützen die Ende-zu-Ende-Protokolle UCP und AP2 den agentischen E-Commerce

Voraussetzungen

  • Ein Google Cloud-Projekt mit aktivierter Abrechnung
  • Ein Webbrowser wie Chrome
  • Python 3.11 und höher

Dieses Codelab richtet sich an fortgeschrittene Entwickler, die mit Python und Google Cloud vertraut sind. Dieses Codelab dauert etwa 15 Minuten.

Die in diesem Codelab erstellten Ressourcen sollten weniger als 5 $kosten.

2. UCP- und AP2-Protokolle

Bevor wir uns mit der Entwicklung des Agents befassen, wollen wir uns die beiden Protokolle ansehen, die diesen sicheren Agentenhandel ermöglichen.

Universal Commerce Protocol (UCP)

UCP standardisiert die Interaktion von KI-Agents mit Händlern. Es löst das Problem, dass Agents für jeden einzelnen Shop benutzerdefinierte APIs lernen müssen, indem ein standardisiertes Ressourcenmodell eingeführt wird.

Funktionsweise:

  1. Auffindbarkeit: Jeder UCP-konforme Händler stellt ein Profil an einem Standardort bereit: /.well-known/ucp. Beispiel: UCP-Endpunkt von Everlane.Wenn ein Agent dieses Profil liest, sucht er nach:
    • Funktionen: Die eigenständigen Kernfunktionen, die ein Unternehmen unterstützt, z. B. die Katalogsuche oder die Kaufabwicklung.
    • Dienste: Die Kommunikationsschichten auf niedrigerer Ebene, die zum Austausch von Daten verwendet werden. Beispiele: REST API, MCP (Model Context Protocol), A2A (Agent2Agent Protocol).
    • Erweiterungen: Wenn ein Händler ein spezielles Verhalten benötigt, kann er in diesem Profil benutzerdefinierte Erweiterungen definieren.
  2. Vorgänge: Sobald der Agent den Dienstendpunkt ermittelt hat, verwendet er ihn, um Vorgänge auszuführen. In diesem Codelab verwenden wir das Model Context Protocol (MCP) als Diensttransport. Der Agent führt JSON-RPC 2.0-Aufrufe an diesen Endpunkt aus, um die erkannten Funktionen aufzurufen: Produkte suchen, Check-outs erstellen und Käufe abschließen.

UCP-Workflow

Agent Payments Protocol (AP2)

AP2 standardisiert, wie Zahlungen von Kundenservicemitarbeitern im Namen von Nutzern autorisiert werden. So wird das Sicherheitsproblem gelöst, dass Kundenservicemitarbeiter vertrauliche Zahlungsanmeldedaten verarbeiten.

Funktionsweise:

  1. Warenkorb-Mandat: Wenn ein Agent einen Checkout mit dem UCP-Protokoll erstellt, gibt der Händler eine CartMandate zurück. Dies ist ein JSON-Objekt mit den Warenkorbdetails und einer kryptografischen Signatur des Händlers. Sie fungiert als Garantie für den Preis. Der Händler kann den Preis nach der Ausstellung dieses Mandats nicht mehr ändern.
  2. Zahlungsauftrag: Nachdem der Nutzer (oder der Kundenservicemitarbeiter in seinem Namen) den Inhalt des Einkaufswagens überprüft hat, erstellt er eine PaymentMandate, um die Zahlung zu autorisieren. Diese PaymentMandate verweist auf die CartMandate und enthält die kryptografische Signatur (oder das Autorisierungstoken) des Nutzers.
  3. Bestätigung mit doppelter Unterschrift: Der Händler erhält beide Mandate. Sie überprüfen ihre eigene Signatur auf dem CartMandate und die Signatur des Nutzers auf dem PaymentMandate. Wenn beide gültig sind, wird die Transaktion fortgesetzt.

Dieses „Doppelschloss“-System sorgt dafür, dass Händler nicht zu viel berechnen und Kundenservicemitarbeiter nicht ohne Autorisierung Geld ausgeben können. In der Produktion wird für diese Mandate SD-JWT (Selective Disclosure JWT) verwendet, um den Datenschutz der Nutzer zu schützen.

AP2-Workflow

3. Umgebung einrichten

Google Cloud-Projekt einrichten

Google Cloud-Projekt erstellen

  1. Wählen Sie in der Google Cloud Console auf der Seite zur Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.
  2. Die Abrechnung für das Cloud-Projekt muss aktiviert sein. So prüfen Sie, ob die Abrechnung für ein Projekt aktiviert ist.

Cloud Shell starten

Cloud Shell ist eine Befehlszeilenumgebung, die in Google Cloud ausgeführt wird und mit den erforderlichen Tools vorinstalliert ist.

  1. Klicken Sie oben in der Google Cloud Console auf Cloud Shell aktivieren.
  2. Prüfen Sie nach der Verbindung mit Cloud Shell Ihre Authentifizierung:
    gcloud auth list
    
  3. Prüfen Sie, ob Ihr Projekt konfiguriert ist:
    gcloud config get project
    
  4. Wenn Ihr Projekt nicht wie erwartet festgelegt ist, legen Sie es fest:
    export PROJECT_ID=<YOUR_PROJECT_ID>
    gcloud config set project $PROJECT_ID
    

Auf Gemini-Modelle zugreifen

Kopieren Sie die folgenden Befehle in Ihre Cloud Shell-Umgebung und fügen Sie sie dort ein. Dadurch wird der Zugriff auf die Gemini-Modelle ermöglicht, die vom Cine-Agent verwendet werden.

export GOOGLE_CLOUD_PROJECT=$PROJECT_ID
export GOOGLE_CLOUD_LOCATION=global
export GOOGLE_GENAI_USE_VERTEXAI=True

Verzeichnisstruktur einrichten

Kopieren Sie die folgenden Befehle und fügen Sie sie ein, um ein neues Verzeichnis für den Agent zu erstellen:

mkdir -p agent_payments
cd agent_payments

Abhängigkeiten installieren

In Google Cloud Shell ist uv vorinstalliert, um die Umgebung und Abhängigkeiten zu verwalten.

  1. Erstellen Sie im Stammverzeichnis Ihres agent_payments-Ordners eine Datei mit dem Namen pyproject.toml und fügen Sie ihr folgenden Inhalt hinzu. In dieser Datei werden die Projektmetadaten und ‑abhängigkeiten definiert.
[project]
name = "agent-payments-demo"
version = "0.1.0"
description = "CineAgent booking agent using UCP and AP2"
requires-python = ">=3.11"
dependencies = [
    "google-adk>=1.29.0",
    "google-genai>=1.27.0",
    "fastapi>=0.115.0",
    "uvicorn>=0.34.0",
    "httpx>=0.28.0",
    "ap2 @ git+https://github.com/google-agentic-commerce/AP2.git@main",
    "ucp-sdk @ git+https://github.com/Universal-Commerce-Protocol/python-sdk.git@main",
]
  1. Führen Sie den folgenden Befehl aus, um die virtuelle Umgebung zu erstellen und alle Abhängigkeiten zu installieren:
uv sync
  1. Aktivieren Sie die von uv erstellte virtuelle Umgebung:
source .venv/bin/activate

4. Agent definieren

Bevor wir die Tool-Logik schreiben, definieren wir den Agenten selbst in einer Datei namens agent.py. Dieser Agent fungiert als Orchestrator für den Filmreservierungsprozess.

agent.py erstellen:

"""CineAgent — movie ticket booking agent using UCP and AP2."""

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from agent_payments.tools import (
    discover_theaters,
    search_movies,
    get_movie_detail,
    create_checkout,
    complete_purchase,
)

root_agent = Agent(
    model="gemini-3.1-pro-preview",
    name="cineagent",
    description="Movie ticket booking agent using UCP and AP2.",
    instruction="""You are CineAgent, a movie ticket booking assistant.

You help users find and book movie tickets across multiple theaters
using UCP (Universal Commerce Protocol) and AP2 (Agent Payments).

**Your tools:**
- discover_theaters: Find theaters and what they support
- search_movies: Search movies across all theaters
- get_movie_detail: Get showtimes at a specific theater
- create_checkout: Start a checkout session
- complete_purchase: Finalize with AP2 mandate signing

**Rules:**
- Always call discover_theaters first if you haven't yet
- Keep responses concise — summarize and suggest next steps
- Prices from tools are in cents (1500 = $15.00)
- Never invent data — only state what tools return
""",
    tools=[
        discover_theaters,
        search_movies,
        get_movie_detail,
        create_checkout,
        FunctionTool(complete_purchase, require_confirmation=True),
    ],
)

Erläuterung zum Code:

Sehen wir uns an, was in dieser Agent-Definition passiert:

  • model="gemini-3.1-pro-preview": Wir verwenden das neueste Gemini Pro-Vorschaumodell für komplexe Schlussfolgerungen und die Verwendung von Tools.
  • instruction: Diese Eingabeaufforderung steuert das Verhalten des Agenten. Es wird dem Agenten ausdrücklich mitgeteilt, dass er UCP und AP2 verwenden soll. Außerdem werden die verfügbaren Tools aufgeführt und wichtige Regeln wie „Niemals Daten erfinden“ und „Preise sind in Cent angegeben“ festgelegt.
  • tools: Dies ist eine Liste von Python-Funktionen (die wir als Nächstes erstellen), die der Agent basierend auf den Anfragen des Nutzers aufrufen kann.
  • require_confirmation: Sie können jedes Tool mit FunctionTool(my_function,require_confirmation=True) umschließen. Wenn der Agent ausgelöst wird, pausiert er und wartet auf eine einfache Bestätigung mit „Ja“ oder „Nein“, bevor er das Tool ausführt. Hier pausiert der KI-Agent vor der Ausführung des Tools complete_purchase, um eine Bestätigung durch den Nutzer einzuholen.

Die Tool-Liste

In der Agent-Definition wird deklariert, was wir erstellen müssen. Jedes Tool entspricht einem bestimmten Vorgang im UCP- oder AP2-Protokoll:

Tool

Funktion

Protokollaktion

discover_theaters

Händler und ihre Funktionen finden

Abfragen /.well-known/ucp

search_movies

Kataloge verschiedener Händler durchsuchen

JSON-RPC-zu-MCP-Endpunkt

get_movie_detail

Vorführzeiten bei einem bestimmten Händler abrufen

JSON-RPC-zu-MCP-Endpunkt

create_checkout

Checkout-Sitzung starten

JSON-RPC-zu-MCP-Endpunkt

complete_purchase

Zahlung autorisieren und Bestellung abschließen

Unterzeichnet das AP2-Mandat und sendet es an MCP

Das Gemini-Modell entscheidet anhand des Gesprächs, wann die einzelnen Tools aufgerufen werden sollen. Als Nächstes müssen wir implementieren, was die einzelnen Tools in tools.py tun.

5. Agent-Tools erstellen: Discovery und Browsing

Implementieren wir nun die Tools, mit denen der Agent Filme durchsuchen und finden kann. Jedes Tool umschließt einen UCP-Vorgang.

Erstellen Sie eine neue Datei mit dem Namen tools.py und kopieren Sie den folgenden Code:

"""Agent tools — each one wraps a UCP or AP2 operation."""

import asyncio
import json

from .ucp import UCPClient
from .ap2 import AP2Handler

# Initialize clients directly
_merchant_urls = ["http://localhost:8081", "http://localhost:8082"]
_ucp = UCPClient()
_ap2 = AP2Handler()

Einrichtung

Damit sich dieses Codelab auf die Erstellung des Agenten konzentriert, verwenden wir zwei Hilfsklassen, UCPClient und AP2Handler, die wir uns in einem späteren Schritt ansehen.

  • Was ist das?: Es handelt sich um handgeschriebene Hilfsklassen, die wir für dieses Codelab erstellt haben, um die Interaktion mit den Mock-Händlern zu simulieren. Da offizielle UCP- und AP2-SDKs noch nicht verfügbar sind, verwenden wir diese Hilfsprogramme, um die Lücke zu schließen. In einer Produktionsumgebung würden Sie die offiziellen SDKs verwenden, sobald sie verfügbar sind.
  • Behandeln Sie sie vorerst als Hilfsobjekte:
    • _ucp.discover(url): Ruft das Profil eines Händlers ab.
    • _ucp.mcp_call(url, method, params): Sendet eine JSON-RPC 2.0-Anfrage an den MCP-Endpunkt des Händlers.

Theater entdecken

Dieses Tool ist der erste Schritt im UCP-Ablauf. Es wird ermittelt, welche Händler es gibt und was sie unterstützen.

Zu tools.py hinzufügen:

async def discover_theaters() -> str:
    """Discover available theater merchants and their capabilities via UCP."""
    theaters = []
    for url in _merchant_urls:
        info = await _ucp.discover(url)
        theaters.append(
            {
                "url": url,
                "name": info["name"],
                "capabilities": info["capabilities"],
                "payment_handlers": info["payment_handlers"],
            }
        )
    return json.dumps(theaters, indent=2)

Funktionsweise:

  1. Es wird eine vom Server bereitgestellte Liste von Händler-URLs durchlaufen. In diesem Codelab richten wir im späteren Abschnitt zwei Test-Händler ein.
  2. Für jede URL wird _ucp.discover(url) aufgerufen, wodurch der /.well-known/ucp-Endpunkt erreicht wird.
  3. Darin werden Name, Funktionen und Zahlungshandler in einer Zusammenfassungsliste erfasst.
  4. Die Liste wird als JSON-String zurückgegeben, damit der Agent sie lesen kann.

Filme suchen

Mit diesem Tool wird bei allen erkannten Händlern gesucht und die Ergebnisse werden zusammengeführt. Das ist wichtig, da derselbe Film möglicherweise in mehreren Kinos mit unterschiedlichen Formaten (IMAX, Dolby) und unterschiedlichen Preisen läuft.

Zu tools.py hinzufügen:

async def search_movies(query: str = "") -> str:
    """Search for movies across all theaters. Use '' to browse all."""
    all_movies = {}
    for url, merchant in _ucp.merchants.items():
        result = await _ucp.mcp_call(url, "search_catalog", {"query": query})
        for product in result.get("products", []):
            mid = product["id"]
            if mid not in all_movies:
                all_movies[mid] = {
                    "id": mid,
                    "title": product["title"],
                    "categories": product.get("categories", []),
                    "theaters": {},
                }
            showtimes = []
            for v in product.get("variants", []):
                opts = {
                    o["name"]: o["value"]
                    for o in v.get("selected_options", [])
                }
                showtimes.append(
                    {
                        "id": v["id"],
                        "format": opts.get("format", "Standard"),
                        "time": opts.get("time", ""),
                        "price": v.get("price", {}),
                        "seats": v.get("availability", {}).get(
                            "seats_available", 0
                        ),
                    }
                )
            all_movies[mid]["theaters"][url] = {
                "name": merchant["name"],
                "showtimes": showtimes,
            }
    return json.dumps(list(all_movies.values()), indent=2)

Funktionsweise:

  1. Es wird eine Schleife durch alle gefundenen Kinos durchlaufen.
  2. Es wird eine JSON-RPC-Anfrage für den Suchkatalog (_ucp.mcp_call(url, "search_catalog", {"query": query})) an den MCP-Endpunkt des Händlers gesendet.
  3. Anschließend werden die Ergebnisse bereinigt, um Filme und ihre „Varianten“ (die bestimmte Vorstellungszeiten und Formate darstellen) zu finden. Gruppiert die Filme nach ID, damit der Nutzer keine doppelten Filmeinträge sieht.

Filmdetails abrufen

Mit diesem Tool werden die vollständigen Katalogdetails für einen bestimmten Film in einem bestimmten Kino abgerufen.

Zu tools.py hinzufügen:

async def get_movie_detail(movie_id: str, merchant_url: str) -> str:
    """Get detailed showtimes for a movie at a specific theater."""
    result = await _ucp.mcp_call(
        merchant_url, "lookup_catalog", {"product_id": movie_id}
    )
    return json.dumps(result, indent=2)

Funktionsweise:

  • Dabei wird die Methode lookup_catalog am MCP-Endpunkt des Händlers aufgerufen und die spezifische movie_id übergeben. Daraufhin werden detaillierte Informationen wie Spielzeiten und verfügbare Plätze für das jeweilige Kino angezeigt.

6. Agent-Tools erstellen: Checkout und Zahlung

Diese Tools übernehmen den Checkout- und Kaufvorgang. Hier kommt das AP2-Protokoll ins Spiel, um sichere Transaktionen zu gewährleisten.

Direktkauf erstellen

Mit diesem Tool wird eine Abrechnungssitzung in einem bestimmten Kino für eine bestimmte Vorstellung gestartet.

Zu tools.py hinzufügen:

async def create_checkout(
    merchant_url: str, showtime_id: str, quantity: int = 1
) -> str:
    """Create a checkout session for tickets at a theater."""
    result = await _ucp.mcp_call(merchant_url, "create_checkout", {
        "checkout": {
            "line_items": [
                {"item": {"id": showtime_id}, "quantity": quantity}
            ],
            "context": {"country": "US", "currency": "USD"},
        }
    })
    return json.dumps(result, indent=2)

Funktionsweise:

  1. Dabei wird die Methode create_checkout am MCP-Endpunkt des Händlers aufgerufen.
  2. Sie übergibt die vom Nutzer angeforderten showtime_id und quantity.
  3. Der Händler gibt ein JSON-Objekt mit einem AP2 CartMandate zurück.

Hinweis: Wenn Sie die Antwortdaten untersuchen, enthält ap2.cart_mandate das Feld merchant_authorization. Dies ist die kryptografische Signatur des Händlers, mit der der angegebene Preis gesichert wird. Sie können sie später nicht mehr ändern.

Einkauf abschließen

In diesem Tool passieren drei Dinge:

  1. Wir erhalten das CartMandate von der Kasse und bestätigen es (mock).
  2. Erstellen und unterzeichnen Sie das PaymentMandate (Mock).
  3. Senden Sie das unterschriebene Mandat an den Händler, um den Kauf abzuschließen.

Zu tools.py hinzufügen:

async def complete_purchase(
    checkout_id: str, merchant_url: str, payment_method: str = "card"
) -> str:
    """Complete purchase with AP2 payment authorization."""
    # 1. Get the CartMandate from the checkout
    checkout = await _ucp.mcp_call(
        merchant_url, "get_checkout", {"checkout": {"id": checkout_id}}
    )
    cart_mandate = _ap2.process_cart_mandate(checkout)
    if not cart_mandate:
        return {"error": "No cart mandate — checkout may have expired"}

    # 2-3. Create and sign the PaymentMandate
    # In production, this call would trigger a user prompt (biometric or device auth)
    # via the AP2 Wallet SDK. In this demo, it just computes a mock SHA-256 hash.
    payment_mandate = _ap2.create_payment_mandate(cart_mandate, payment_method)

    # 4. Send both mandates to complete the purchase
    result = await _ucp.mcp_call(merchant_url, "complete_checkout", {
        "checkout": {
            "id": checkout_id,
            "payment": {
                "instruments": [{
                    "handler_id": f"card_{merchant_url.split(':')[-1]}",
                    "type": "card",
                }],
            },
            "ap2": {"payment_mandate": payment_mandate},
        }
    })
    return json.dumps(result, indent=2)

Warum zwei Mandate? Das CartMandate sorgt dafür, dass der Händler den Preis nach der Angabe nicht mehr ändern kann. Das Zahlungsauftragsmandat sorgt dafür, dass der Kundenservicemitarbeiter den Nutzer nicht ohne Einwilligung belasten kann. Der Ablauf ist:

Merchant locks price -> User authorizes charge -> Merchant verifies both -> Order completes.

Checkpoint: Vollständige tools.py

Ihr vollständiges tools.py sollte jetzt fünf Tool-Funktionen und eine Initialisierung auf Modulebene für UCP- und AP2-Clients haben. Prüfen Sie, ob es so aussieht:

"""Agent tools — each one wraps a UCP or AP2 operation."""

import asyncio
import json

from ucp import UCPClient
from ap2 import AP2Handler

# Initialize clients directly
_merchant_urls = ["http://localhost:8081", "http://localhost:8082"]
_ucp = UCPClient()
_ap2 = AP2Handler()


async def discover_theaters() -> str:
    """Discover available theater merchants and their capabilities via UCP."""
    theaters = []
    for url in _merchant_urls:
        info = await _ucp.discover(url)
        theaters.append(
            {
                "url": url,
                "name": info["name"],
                "capabilities": info["capabilities"],
                "payment_handlers": info["payment_handlers"],
            }
        )
    return json.dumps(theaters, indent=2)


async def search_movies(query: str = "") -> str:
    """Search for movies across all theaters. Use '' to browse all."""
    all_movies = {}
    for url, merchant in _ucp.merchants.items():
        result = await _ucp.mcp_call(url, "search_catalog", {"query": query})
        for product in result.get("products", []):
            mid = product["id"]
            if mid not in all_movies:
                all_movies[mid] = {
                    "id": mid,
                    "title": product["title"],
                    "categories": product.get("categories", []),
                    "theaters": {},
                }
            showtimes = []
            for v in product.get("variants", []):
                opts = {
                    o["name"]: o["value"]
                    for o in v.get("selected_options", [])
                }
                showtimes.append(
                    {
                        "id": v["id"],
                        "format": opts.get("format", "Standard"),
                        "time": opts.get("time", ""),
                        "price": v.get("price", {}),
                        "seats": v.get("availability", {}).get(
                            "seats_available", 0
                        ),
                    }
                )
            all_movies[mid]["theaters"][url] = {
                "name": merchant["name"],
                "showtimes": showtimes,
            }
    return json.dumps(list(all_movies.values()), indent=2)


async def get_movie_detail(movie_id: str, merchant_url: str) -> str:
    """Get detailed showtimes for a movie at a specific theater."""
    result = await _ucp.mcp_call(
        merchant_url, "lookup_catalog", {"product_id": movie_id}
    )
    return json.dumps(result, indent=2)


async def create_checkout(
    merchant_url: str, showtime_id: str, quantity: int = 1
) -> str:
    """Create a checkout session for tickets at a theater."""
    result = await _ucp.mcp_call(merchant_url, "create_checkout", {
        "checkout": {
            "line_items": [
                {"item": {"id": showtime_id}, "quantity": quantity}
            ],
            "context": {"country": "US", "currency": "USD"},
        }
    })
    return json.dumps(result, indent=2)


async def complete_purchase(
    checkout_id: str, merchant_url: str, payment_method: str = "card"
) -> str:
    """Complete purchase with AP2 payment authorization."""
    # 1. Get the CartMandate from the checkout
    checkout = await _ucp.mcp_call(
        merchant_url, "get_checkout", {"checkout": {"id": checkout_id}}
    )
    cart_mandate = _ap2.process_cart_mandate(checkout)
    if not cart_mandate:
        return {"error": "No cart mandate — checkout may have expired"}

    # 2-3. Create and sign the PaymentMandate
    # In production, this call would trigger a user prompt (biometric or device auth)
    # via the AP2 Wallet SDK. In this demo, it just computes a mock SHA-256 hash.
    payment_mandate = _ap2.create_payment_mandate(cart_mandate, payment_method)

    # 4. Send both mandates to complete the purchase
    result = await _ucp.mcp_call(merchant_url, "complete_checkout", {
        "checkout": {
            "id": checkout_id,
            "payment": {
                "instruments": [{
                    "handler_id": f"card_{merchant_url.split(':')[-1]}",
                    "type": "card",
                }],
            },
            "ap2": {"payment_mandate": payment_mandate},
        }
    })
    return json.dumps(result, indent=2)

7. Ausführbar machen

Bei echten UCP-Händlern würden Sie den Agenten auf ihre URLs verweisen und das war es dann auch schon. Für dieses Codelab benötigen wir zwei Dinge, um lokal zu testen:

  1. Simulierte Händler: Lokale Server, die UCP-Endpunkte simulieren, damit Sie etwas zum Testen haben
  2. Protokoll-Helfer: schlanke HTTP-Wrapper für UCP und AP2 (in der Produktion werden diese durch offizielle SDKs ersetzt)

Hinweis: Sie müssen diesen Code nicht sorgfältig lesen. Diese Dateien simulieren die Funktionen, die echte Infrastruktur und SDKs bieten würden. Kopieren Sie sie unverändert.

Protokoll-Helper

Für UCP und AP2 gibt es noch keine Client-SDKs. Diese beiden Dateien kümmern sich um die HTTP-Verbindung.

ucp.py erstellen:

"""UCP client — discovers merchants and calls their MCP tools."""

import uuid
import httpx


class UCPClient:
    def __init__(self):
        self.client = httpx.AsyncClient(timeout=30)
        self.merchants = {}  # url -> merchant info dict

    async def discover(self, merchant_url: str) -> dict:
        """Fetch a merchant's UCP profile from /.well-known/ucp."""
        resp = await self.client.get(f"{merchant_url}/.well-known/ucp")
        resp.raise_for_status()
        profile = resp.json()
        ucp = profile["ucp"]
        info = {
            "name": merchant_url.split("//")[-1],
            "mcp_endpoint": ucp["services"]["dev.ucp.shopping"][0]["endpoint"],
            "capabilities": list(ucp.get("capabilities", {}).keys()),
            "payment_handlers": list(ucp.get("payment_handlers", {}).keys()),
        }
        self.merchants[merchant_url] = info
        return info

    async def mcp_call(
        self, merchant_url: str, tool_name: str, arguments: dict
    ) -> dict:
        """Call a merchant's MCP tool via JSON-RPC 2.0."""
        merchant = self.merchants[merchant_url]
        resp = await self.client.post(
            merchant["mcp_endpoint"],
            json={
                "jsonrpc": "2.0",
                "id": uuid.uuid4().hex,
                "method": "tools/call",
                "params": {"name": tool_name, "arguments": arguments},
            },
        )
        resp.raise_for_status()
        data = resp.json()
        if "error" in data:
            raise Exception(f"MCP error: {data['error']}")
        return data.get("result", {})

    async def close(self):
        await self.client.aclose()

ap2.py erstellen:

"""AP2 mandate handler — creates and signs payment mandates."""

import uuid
import hashlib


class AP2Handler:
    def process_cart_mandate(self, checkout_response: dict) -> dict | None:
        """Extract the merchant-signed CartMandate from a checkout response.

        The CartMandate is the merchant's cryptographic price guarantee —
        it locks the total so it can't change between checkout and payment.
        """
        return checkout_response.get("ap2", {}).get("cart_mandate")

    def create_payment_mandate(
        self, cart_mandate: dict, payment_method: str = "card"
    ) -> dict:
        """Create and sign a PaymentMandate authorizing payment.

        References the merchant's CartMandate and adds user authorization.
        Together they form a two-party agreement: merchant guarantees price,
        user authorizes charge.
        """
        contents = cart_mandate["contents"]
        mandate_id = uuid.uuid4().hex

        return {
            "mandate_id": mandate_id,
            "cart_reference": contents["id"],
            "merchant": contents["merchant_name"],
            "total": contents["total"],
            "payment_method": payment_method,
            "user_authorization": self._sign(mandate_id, contents["id"]),
        }

    def _sign(self, mandate_id: str, checkout_id: str) -> str:
        """Sign the mandate. Production uses real crypto (sd-jwt-vc)."""
        payload = f"{mandate_id}:{checkout_id}"
        return hashlib.sha256(payload.encode()).hexdigest()

Mock-Händler

merchants.py erstellen:

"""Mock UCP merchant servers — two theaters with different capabilities."""

import uuid
import time
import multiprocessing
from datetime import datetime, timezone, timedelta

import uvicorn
from fastapi import FastAPI

# ── Theater data ────────────────────────────────────────────

THEATERS = {
    8081: {
        "name": "Meridian Cinemas",
        "movies": [
            {
                "id": "opp",
                "title": "Oppenheimer",
                "categories": ["Drama", "History"],
                "showtimes": [
                    {"id": "st_opp_7pm_imax", "format": "IMAX", "time": "7:00 PM", "price": 2200, "seats": 45},
                    {"id": "st_opp_930pm", "format": "Standard", "time": "9:30 PM", "price": 1500, "seats": 80},
                ],
            },
            {
                "id": "dune3",
                "title": "Dune: Part Three",
                "categories": ["Sci-Fi", "Action"],
                "showtimes": [
                    {"id": "st_dune_8pm_imax", "format": "IMAX", "time": "8:00 PM", "price": 2200, "seats": 30},
                ],
            },
        ],
        "discounts": {},
    },
    8082: {
        "name": "StarLight Theaters",
        "movies": [
            {
                "id": "opp",
                "title": "Oppenheimer",
                "categories": ["Drama", "History"],
                "showtimes": [
                    {"id": "st_opp_6pm_atmos", "format": "Dolby Atmos", "time": "6:00 PM", "price": 1800, "seats": 60},
                ],
            },
            {
                "id": "spider",
                "title": "Spider-Verse",
                "categories": ["Animation", "Action"],
                "showtimes": [
                    {"id": "st_spider_4pm", "format": "Standard", "time": "4:00 PM", "price": 1200, "seats": 100},
                ],
            },
        ],
        "discounts": {},
    },
}


def create_app(port):
    theater = THEATERS[port]
    app = FastAPI()
    sessions = {}

    # ── UCP Discovery endpoint ──────────────────────────────
    @app.get("/.well-known/ucp")
    def discovery():
        caps = {
            "dev.ucp.shopping.catalog.search": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.catalog.lookup": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.checkout": [{"version": "2026-01-15"}],
            "dev.ucp.shopping.ap2_mandate": [{"version": "2026-01-15"}],
        }

        return {
            "ucp": {
                "version": "2026-01-15",
                "services": {
                    "dev.ucp.shopping": [
                        {"version": "2026-01-15", "transport": "mcp",
                         "endpoint": f"http://localhost:{port}/mcp"}
                    ]
                },
                "capabilities": caps,
                "payment_handlers": {
                    "com.example.card": [
                        {"id": f"card_{port}", "version": "2026-01-15",
                         "available_instruments": [{"type": "card"}], "config": {}}
                    ]
                },
            }
        }

    # ── MCP JSON-RPC endpoint ───────────────────────────────
    @app.post("/mcp")
    def mcp(body: dict):
        tool = body["params"]["name"]
        args = body["params"].get("arguments", {})
        rid = body.get("id", "1")

        if tool == "search_catalog":
            q = args.get("query", "").lower()
            hits = [m for m in theater["movies"]
                    if not q or q in m["title"].lower()
                    or any(q in c.lower() for c in m["categories"])]
            return _ok(rid, {"products": [_product(m) for m in hits]})

        if tool == "lookup_catalog":
            mid = args.get("product_id") or (args.get("ids", [None])[0])
            movie = next((m for m in theater["movies"] if m["id"] == mid), None)
            if not movie:
                return _err(rid, "Not found")
            return _ok(rid, {"products": [_product(movie)]})

        if tool == "create_checkout":
            co = args.get("checkout", {})
            sid = f"chk_{uuid.uuid4().hex[:12]}"
            items, subtotal = [], 0
            for li in co.get("line_items", []):
                st = _find_showtime(li["item"]["id"])
                if not st:
                    continue
                mv = _find_movie(li["item"]["id"])
                qty = li.get("quantity", 1)
                amt = st["price"] * qty
                subtotal += amt
                items.append({
                    "id": f"li_{uuid.uuid4().hex[:8]}",
                    "item": {
                        "id": st["id"],
                        "title": f"{mv['title']}{st['format']} {st['time']}",
                        "price": st["price"],
                    },
                    "quantity": qty,
                    "totals": [{"type": "subtotal", "amount": amt}],
                })
            tax = int(subtotal * 0.08)
            total = subtotal + tax
            session = {
                "id": sid,
                "status": "ready_for_complete",
                "currency": "USD",
                "line_items": items,
                "totals": [
                    {"type": "subtotal", "display_text": "Subtotal", "amount": subtotal},
                    {"type": "tax", "display_text": "Tax", "amount": tax},
                    {"type": "total", "display_text": "Total", "amount": total},
                ],
                "metadata": {"theater_name": theater["name"]},
                "ap2": {
                    "cart_mandate": {
                        "contents": {
                            "id": sid,
                            "merchant_name": theater["name"],
                            "total": {
                                "label": "Total",
                                "amount": {"currency": "USD", "value": total / 100},
                            },
                            "cart_expiry": (
                                datetime.now(timezone.utc) + timedelta(minutes=10)
                            ).isoformat(),
                        },
                        "merchant_authorization": f"mock_merchant_sig_{sid}",
                    }
                },
            }
            sessions[sid] = session
            return _ok(rid, session)

        if tool == "get_checkout":
            sid = args.get("checkout", {}).get("id") or args.get("id")
            return _ok(rid, sessions.get(sid, {"error": "not_found"}))



        if tool == "complete_checkout":
            co = args.get("checkout", {})
            sid = co.get("id")
            session = sessions.get(sid)
            if not session:
                return _err(rid, "Not found")
            session["status"] = "completed"
            session["order"] = {
                "id": f"ord_{uuid.uuid4().hex[:8]}",
                "created_at": datetime.now(timezone.utc).isoformat(),
                "tickets": [
                    {
                        "movie": li["item"]["title"],
                        "quantity": li["quantity"],
                        "ticket_code": uuid.uuid4().hex[:8].upper(),
                    }
                    for li in session["line_items"]
                ],
            }
            session["ap2"]["payment_mandate_verified"] = True
            return _ok(rid, session)

        return _err(rid, f"Unknown tool: {tool}")

    def _ok(rid, result):
        return {"jsonrpc": "2.0", "id": rid, "result": result}

    def _err(rid, msg):
        return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32000, "message": msg}}

    def _product(movie):
        return {
            "id": movie["id"],
            "title": movie["title"],
            "categories": movie["categories"],
            "variants": [
                {
                    "id": st["id"],
                    "selected_options": [
                        {"name": "format", "value": st["format"]},
                        {"name": "time", "value": st["time"]},
                    ],
                    "price": {"amount": st["price"], "currency": "USD"},
                    "availability": {"available": True, "seats_available": st["seats"]},
                }
                for st in movie["showtimes"]
            ],
        }

    def _find_showtime(sid):
        return next(
            (st for m in theater["movies"] for st in m["showtimes"] if st["id"] == sid),
            None,
        )

    def _find_movie(sid):
        return next(
            (m for m in theater["movies"] for st in m["showtimes"] if st["id"] == sid),
            None,
        )

    return app


def _run(port):
    uvicorn.run(create_app(port), host="0.0.0.0", port=port, log_level="warning")


if __name__ == "__main__":
    for port in THEATERS:
        multiprocessing.Process(target=_run, args=(port,), daemon=True).start()
    print("Merchants running: Meridian (:8081), StarLight (:8082)")
    print("Press Ctrl+C to stop")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass

Dadurch werden zwei FastAPI-Server mit jeweils zwei Endpunkten erstellt:

  • GET /.well-known/ucp – UCP-Erkennung Gibt die Funktionen des Händlers, die MCP-Endpunkt-URL und die akzeptierten Zahlungsmethoden zurück.
  • POST /mcp: MCP-Vorgänge (Model Context Protocol). Verarbeitet JSON-RPC 2.0-Aufrufe für die Katalogsuche, den Check-out, Rabatte und die Zahlung.

Starten Sie die Händler in einem neuen Terminal. Sie müssen weiter ausgeführt werden:

cd agent_payments
source .venv/bin/activate
python merchants.py

Hier sollten Sie dies sehen:

Merchants running: Meridian (:8081), StarLight (:8082)

Kehren Sie zu Ihrem ersten Terminal zurück und prüfen Sie die UCP-Erkennung:

curl -s http://localhost:8081/.well-known/ucp | python -m json.tool

Sie sollten die Funktionen des Händlers, die MCP-Endpunkt-URL und die Zahlungshandler sehen.

8. Agenten mit ADK Web ausführen

Verwenden wir die integrierte Web-UI der ADK CLI. Dadurch wird eine Chatoberfläche im Browser bereitgestellt und Tool-Bestätigungsaufforderungen werden automatisch verarbeitet.

Ihr Projekt sollte nun so aussehen:

agent_payments/
├── merchants.py      # Mock UCP merchants
├── ucp.py            # UCP client helper
├── ap2.py            # AP2 mandate handler
├── tools.py          # Agent tools
├── agent.py          # Agent definition
└── pyproject.toml

Jetzt ausprobieren

Wechseln Sie im aktuellen Terminal zum übergeordneten Verzeichnis (ein Ordner über agent_payments) und starten Sie die ADK-Web-UI:

cd ../
adk web --allow_origins '*'

Die Ausgabe sollte darauf hinweisen, dass der Server ausgeführt wird, und eine URL (normalerweise http://localhost:8000 oder ähnlich) enthalten.

Mit dem Kundenservicemitarbeiter sprechen

  1. Öffnen Sie die von adk web bereitgestellte URL in Ihrem Browser.
  2. Es wird eine Chat-Oberfläche angezeigt.
  3. Fragen Sie zum Beispiel: „Welche Filme laufen im Kino?“
  4. Der Agent sucht im Hintergrund nach Kinos und Katalogen und fasst die Ergebnisse beider Händler über UCP zusammen.
  5. Tickets buchen: „Buche 2 Tickets für Oppenheimer um 19:00 Uhr.“
  6. Wenn der Agent versucht, complete_purchase aufzurufen, wird in der ADK-Web-UI ein Bestätigungsdialogfeld oder eine Karte angezeigt.
  7. Um die Transaktion zu autorisieren, antworten Sie im Chat mit diesem genauen JSON-String: {"confirmed": true}.
  8. Der Kundenservicemitarbeiter schließt den Kauf ab und sendet Ihnen die Bestellbestätigung mit den Ticketcodes zurück.

So lief es hinter den Kulissen ab:

  1. create_checkout → Der Händler hat ein AP2-CartMandate (signierte Preisgarantie) zurückgegeben.
  2. complete_purchase → hat ein PaymentMandate erstellt, es signiert (Mock-SHA-256) und beide Mandate an den Händler gesendet
  3. Der Händler hat beide Signaturen bestätigt → Tickets wurden ausgestellt (im Mock)

9. Bereinigen

So vermeiden Sie, dass lokale Server weiter ausgeführt werden:

  1. Drücken Sie im Terminal, in dem adk web ausgeführt wird, Strg + C, um den Agentenserver zu beenden.
  2. Drücken Sie im Terminal, in dem python merchants.py ausgeführt wird, Strg+C, um die Test-Händler zu beenden.
  3. Deaktivieren Sie die virtuelle Umgebung in beiden Terminals mit dem folgenden Befehl:
deactivate
  1. (Optional) Wenn Sie für dieses Codelab ein neues Google Cloud-Projekt erstellt haben und es löschen möchten, führen Sie Folgendes aus:
gcloud projects delete $GOOGLE_CLOUD_PROJECT

10. Glückwunsch! 🎉

Sie haben einen ADK-Agenten erstellt, der Händler findet, Kataloge durchsucht und Käufe mit UCP und AP2 abschließt.

Das haben Sie gelernt

In diesem Codelab haben Sie einen ADK-Agenten erstellt, der sichere Commerce-Abläufe verarbeitet. Hier eine Zusammenfassung der erstellten Inhalte und der angewendeten Schlüsselkonzepte:

Was Sie erstellt haben:

  • 5 Agent-Tools, die UCP- und AP2-Vorgänge umfassen: Erkennung, Katalogsuche, Direktkauf und Zahlung.
  • AP2-Mandatsunterzeichnung: CartMandate (Händlerpreisbindung) + PaymentMandate (Nutzerautorisierung).
  • Suche bei mehreren Händlern: Ein Agent fragt mehrere Kinos ab und führt die Ergebnisse zusammen.

Wichtige Konzepte:

Protokoll

Funktion

Funktionsweise

UCP Discovery

Der Agent findet Händler und ihre Funktionen.

/.well-known/ucp → Funktionen, MCP-Endpunkt, Zahlungsmethoden

UCP MCP

Der Agent durchsucht Kataloge und erstellt Check-outs.

JSON-RPC 2.0-Aufrufe an den MCP-Endpunkt des Händlers

AP2 CartMandate

Der Händler fixiert den angegebenen Preis.

Vom Händler unterzeichnet, enthält Gesamtbetrag und Ablaufdatum

AP2 PaymentMandate

Nutzer autorisiert die Belastung

Vom Nutzer unterzeichnet, verweist auf CartMandate

Was ist bei der Produktion anders?

In diesem Codelab werden Mocks verwendet. In Produktion:

  • Die UCP-Erkennung wird anhand einer Registry aufgelöst, nicht anhand von hartcodierten localhost-URLs.
  • MCP-Endpunkte werden von echten Händlern gehostet – dasselbe JSON-RPC 2.0-Protokoll, echtes Inventar
  • AP2-Mandate werden mit sd-jwt-vc signiert, nicht mit SHA-256-Hashes.
  • Zahlungsautorisierung verwendet ein AP2 Wallet SDK mit Nutzereinwilligungsaufforderungen
  • Im Frontend werden Tool-Ergebnisse als Rich UI gerendert (Produktübersichten, Zusammenfassungen an der Kasse, Bestätigungskarten).

Nächste Schritte

Referenzdokumente