Zabezpieczanie handlu agentów za pomocą AP2 i UCP

1. Przegląd

W tym ćwiczeniu uruchomisz agenta ADK, który rezerwuje bilety do kina u różnych sprzedawców, korzystając z 2 protokołów handlowych typu open source:

  • UCP (Universal Commerce Protocol): standard umożliwiający agentom odkrywanie sprzedawców, przeszukiwanie katalogów i zarządzanie procesami płatności.
  • AP2 (Agent Payments Protocol): protokół bezpiecznej, weryfikowalnej autoryzacji płatności za pomocą podpisanych kryptograficznie upoważnień.

Aplikacja w wersji demonstracyjnej CineAgent łączy się z 2 fikcyjnymi sprzedawcami biletów do kina o różnych możliwościach (wybór miejsc, specjalne formaty i formy płatności) i koordynuje cały proces rezerwacji od wyszukiwania po płatność.

Czego się nauczysz

  • Jak działa odkrywanie sprzedawców UCP za pomocą profili /.well-known/ucp
  • Jak agent ADK używa UCP do wyszukiwania katalogów i tworzenia płatności
  • Jak wymagania AP2 (CartMandate, PaymentMandate) zabezpieczają transakcje
  • Jak protokoły UCP i AP2 działają w celu zabezpieczenia handlu elektronicznego opartego na agentach

Czego potrzebujesz

  • projekt Google Cloud z włączonymi płatnościami;
  • przeglądarka, np. Chrome;
  • Python 3.11 lub nowszy

To ćwiczenie jest przeznaczone dla średnio zaawansowanych programistów, którzy znają już Pythona i Google Cloud. Wykonanie tego laboratorium zajmie około 15 minut.

Zasoby utworzone w tym laboratorium powinny kosztować mniej niż 5 USD.

2. Omówienie protokołów UCP i AP2

Zanim przejdziemy do tworzenia agenta, poznajmy 2 protokoły, które umożliwiają bezpieczny handel oparty na agentach.

Protokół Universal Commerce Protocol (UCP)

UCP ujednolica sposób, w jaki agenci AI wchodzą w interakcje ze sprzedawcami. Rozwiązuje problem związany z koniecznością uczenia się przez agentów niestandardowych interfejsów API dla każdego sklepu, wprowadzając standardowy model zasobów.

Jak to działa:

  1. Wykrywanie: każdy sprzedawca zgodny z UCP udostępnia profil w standardowej lokalizacji: /.well-known/ucp. Przykład: punkt końcowy UCP Everlane.Gdy agent odczytuje ten profil, szuka:
    • Funkcje: samodzielne podstawowe funkcje obsługiwane przez firmę, takie jak wyszukiwanie w katalogu lub płatność.
    • Usługi: niższe warstwy komunikacji używane do wymiany danych. Przykłady: REST API, MCP (Model Context Protocol), A2A (Agent2Agent Protocol).
    • Rozszerzenia: jeśli sprzedawca potrzebuje specjalistycznych funkcji, może zdefiniować w tym profilu niestandardowe rozszerzenia.
  2. Operacje: po wykryciu agent używa podanego punktu końcowego usługi do wykonywania operacji. W tym ćwiczeniu używamy protokołu Model Context Protocol (MCP) jako transportu usługi. Agent wykonuje wywołania JSON-RPC 2.0 do tego punktu końcowego, aby wywołać wykryte funkcje: wyszukiwanie produktów, tworzenie płatności i dokonywanie zakupów.

Przepływ pracy UCP

Protokół płatności dla agentów (AP2)

AP2 ujednolica sposób autoryzacji płatności przez agentów w imieniu użytkowników. Rozwiązuje problem z bezpieczeństwem związany z obsługą poufnych danych logowania do płatności przez pracowników.

Jak to działa:

  1. Cart Mandate: gdy agent tworzy płatność za pomocą protokołu UCP, sprzedawca zwraca wartość CartMandate. Jest to obiekt JSON zawierający szczegóły koszyka i podpis kryptograficzny sprzedawcy. Działa jako gwarancja ceny. Po wydaniu tego polecenia zapłaty sprzedawca nie może zmienić ceny.
  2. Upoważnienie do płatności: po sprawdzeniu zawartości koszyka użytkownik (lub agent w jego imieniu) tworzy PaymentMandate, aby autoryzować płatność. Ten PaymentMandate odwołuje się do CartMandate i zawiera podpis kryptograficzny użytkownika (lub token autoryzacji).
  3. Weryfikacja podwójnego podpisu: sprzedawca otrzymuje oba mandaty. Weryfikują swój podpis na dokumencie CartMandate i podpis użytkownika na dokumencie PaymentMandate. Jeśli oba są prawidłowe, transakcja zostanie zrealizowana.

Ten system „podwójnego zabezpieczenia” gwarantuje, że sprzedawcy nie mogą naliczać zbyt wysokich opłat, a agenci nie mogą wydawać środków bez autoryzacji. W środowisku produkcyjnym te wymagania są realizowane za pomocą SD-JWT (Selective Disclosure JWT), aby chronić prywatność użytkowników.

Przepływ pracy AP2

3. Konfigurowanie środowiska

Konfiguracja projektu Google Cloud

Tworzenie projektu Google Cloud

  1. W konsoli Google Cloud na stronie selektora projektu wybierz lub utwórz projekt w chmurze Google.
  2. Sprawdź, czy w projekcie Cloud włączone są płatności. Dowiedz się, jak sprawdzić, czy w projekcie są włączone płatności.

Uruchamianie Cloud Shell

Cloud Shell to środowisko wiersza poleceń działające w Google Cloud, które zawiera niezbędne narzędzia.

  1. Kliknij Aktywuj Cloud Shell u góry konsoli Google Cloud.
  2. Po połączeniu z Cloud Shell sprawdź uwierzytelnianie:
    gcloud auth list
    
  3. Sprawdź, czy projekt jest skonfigurowany:
    gcloud config get project
    
  4. Jeśli projekt nie jest ustawiony zgodnie z oczekiwaniami, ustaw go:
    export PROJECT_ID=<YOUR_PROJECT_ID>
    gcloud config set project $PROJECT_ID
    

Dostęp do modeli Gemini

W środowisku Cloud Shell skopiuj i wklej te polecenia. Umożliwi to dostęp do modeli Gemini, z których będzie korzystać agent Cine.

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

Konfigurowanie struktury katalogów

Skopiuj i wklej te polecenia, aby utworzyć nowy katalog agenta:

mkdir -p agent_payments
cd agent_payments

Instalowanie zależności

Google Cloud Shell ma wstępnie zainstalowany uv do zarządzania środowiskiem i zależnościami.

  1. Utwórz plik pyproject.toml w katalogu głównym folderu agent_payments i dodaj do niego tę treść. Ten plik określa metadane i zależności projektu.
[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. Aby utworzyć środowisko wirtualne i zainstalować wszystkie zależności, uruchom to polecenie:
uv sync
  1. Aktywuj środowisko wirtualne utworzone przez uv:
source .venv/bin/activate

4. Określanie agenta

Zanim napiszemy logikę narzędzia, zdefiniujmy samego agenta w pliku o nazwie agent.py. Ten agent będzie pełnił rolę aranżera procesu rezerwacji biletów do kina.

Utwórz agent.py:

"""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),
    ],
)

Wyjaśnienie kodu

Przyjrzyjmy się, co się dzieje w tej definicji agenta:

  • model="gemini-3.1-pro-preview": do złożonego rozumowania i korzystania z narzędzi używamy najnowszego modelu Gemini Pro w wersji testowej.
  • instruction: to prompt, który określa zachowanie agenta. Wyraźnie informuje agenta o konieczności używania UCP i AP2, zawiera listę dostępnych narzędzi i określa kluczowe reguły, takie jak „Nigdy nie wymyślaj danych” i „Ceny są podawane w centach”.
  • tools: to lista funkcji Pythona (które utworzymy w następnym kroku), które agent może wywołać na podstawie żądań użytkownika.
  • require_confirmation: możesz opakować dowolne narzędzie za pomocą FunctionTool(my_function,require_confirmation=True). Gdy zostanie wywołane, narzędzie wstrzymuje działanie i czeka na proste potwierdzenie „tak” lub „nie”, zanim zostanie wykonane. W tym przypadku przed wykonaniem narzędzia complete_purchase agent wstrzymuje działanie, aby uzyskać potwierdzenie od człowieka.

Lista narzędzi

Definicja agenta określa, co musimy utworzyć. Każde narzędzie jest powiązane z określoną operacją w protokole UCP lub AP2:

Narzędzie

Działanie

Działanie protokołu

discover_theaters

Znajdowanie sprzedawców i ich rozwiązań

Zapytania /.well-known/ucp

search_movies

Przeszukiwanie katalogów różnych sprzedawców

Punkt końcowy JSON-RPC do MCP

get_movie_detail

Sprawdzanie godzin seansów u konkretnego sprzedawcy

Punkt końcowy JSON-RPC do MCP

create_checkout

Rozpoczęcie sesji płatności

Punkt końcowy JSON-RPC do MCP

complete_purchase

Autoryzuj płatność i dokończ zamówienie

Podpisuje mandat AP2 i wysyła go do MCP

Model Gemini zdecyduje, kiedy wywołać poszczególne narzędzia na podstawie rozmowy. Następnie musimy zaimplementować w tools.py działanie każdego narzędzia.

5. Tworzenie narzędzi agenta: odkrywanie i przeglądanie

Teraz wdróżmy narzędzia, których agent będzie używać do przeglądania i odkrywania filmów. Każde narzędzie obejmuje operację UCP.

Utwórz nowy plik o nazwie tools.py i wklej do niego ten kod:

"""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()

Opis konfiguracji

Aby skupić się na tworzeniu agenta, używamy 2 klas pomocniczych: UCPClientAP2Handler. Przyjrzymy się im w dalszej części tego ćwiczenia.

  • Co to jest: Są to napisane ręcznie klasy pomocnicze, które stworzyliśmy na potrzeby tego ćwiczenia, aby symulować interakcję z przykładowymi sprzedawcami. Oficjalne pakiety SDK UCP i AP2 nie są jeszcze dostępne, dlatego używamy tych narzędzi, aby wypełnić tę lukę. W środowisku produkcyjnym należy używać oficjalnych pakietów SDK, gdy staną się dostępne.
  • Na razie traktuj je jako obiekty pomocnicze:
    • _ucp.discover(url): pobiera profil sprzedawcy.
    • _ucp.mcp_call(url, method, params): wysyła żądanie JSON-RPC 2.0 do punktu końcowego MCP sprzedawcy.

Odkrywanie kin

To narzędzie jest pierwszym krokiem w procesie UCP. Określa, którzy sprzedawcy są dostępni i co obsługują.

Dodaj do: tools.py

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)

Co to robi:

  1. Iteruje po liście adresów URL sprzedawców podanych przez serwer. W dalszej części tego laboratorium skonfigurujemy 2 przykładowych sprzedawców.
  2. W przypadku każdego adresu URL wywołuje funkcję _ucp.discover(url), która wysyła żądanie do punktu końcowego /.well-known/ucp.
  3. Zbiera nazwę, możliwości i programy obsługi płatności na liście podsumowującej.
  4. Zwraca listę w postaci ciągu JSON, który może odczytać agent.

Szukaj filmów

To narzędzie przeszukuje wszystkich wykrytych sprzedawców i łączy wyniki. Jest to kluczowe, ponieważ ten sam film może być wyświetlany w wielu kinach w różnych formatach (IMAX, Dolby) i w różnych cenach.

Dodaj do: tools.py

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)

Co to robi:

  1. Przechodzi przez wszystkie wykryte kina.
  2. Wysyła żądanie JSON-RPC wyszukiwania katalogu (_ucp.mcp_call(url, "search_catalog", {"query": query})) do punktu końcowego MCP sprzedawcy.
  3. Następnie porządkuje wyniki, aby znaleźć filmy i ich „wersje” (czyli konkretne godziny seansów i formaty). Grupuje filmy według identyfikatora, aby użytkownik nie widział zduplikowanych pozycji.

Pobieranie szczegółów filmu

To narzędzie pobiera pełne szczegóły katalogu dotyczące konkretnego filmu w konkretnym kinie.

Dodaj do: tools.py

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)

Co to robi:

  • Wywołuje metodę lookup_catalog w punkcie końcowym MCP sprzedawcy, przekazując konkretny movie_id. Wyświetlą się szczegółowe informacje, takie jak godziny seansów i dostępność miejsc w danym kinie.

6. Tworzenie narzędzi agenta: płatność

Te narzędzia obsługują proces płatności i zakupu. W tym miejscu protokół AP2 zapewnia bezpieczeństwo transakcji.

Tworzenie procesu płatności

To narzędzie rozpoczyna sesję płatności w konkretnym kinie o określonej godzinie seansu.

Dodaj do: tools.py

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)

Co to robi:

  1. Wywołuje metodę create_checkout w punkcie końcowym MCP sprzedawcy.
  2. Przekazuje wartości showtime_idquantity, o które prosi użytkownik.
  3. Sprzedawca zwraca obiekt JSON zawierający AP2 CartMandate.

Uwaga: jeśli sprawdzisz dane odpowiedzi, zobaczysz, że ap2.cart_mandate zawiera pole merchant_authorization. Jest to podpis kryptograficzny sprzedawcy blokujący podaną cenę. Nie mogą go później zmienić.

Dokończ zakup

W tym narzędziu dzieją się 3 rzeczy:

  1. Pobieramy CartMandate z płatności i weryfikujemy je (mock).
  2. Utwórz i podpisz PaymentMandate (mock).
  3. Wyślij podpisane upoważnienie do sprzedawcy, aby dokończyć zakup.

Dodaj do: tools.py

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)

Dlaczego potrzebne są 2 mandaty? Zasada CartMandate zapewnia, że sprzedawca nie może zmienić ceny po jej podaniu. Dokument PaymentMandate zapewnia, że agent nie może obciążyć użytkownika bez jego zgody. Proces wygląda tak:

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

Punkt kontrolny: pełny tools.py

Twój kompletny tools.py powinien teraz zawierać 5 funkcji narzędziowych i inicjowanie na poziomie modułu dla klientów UCP i AP2. Sprawdź, czy wygląda tak:

"""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. Uruchamianie kodu

W przypadku prawdziwych sprzedawców korzystających z UCP wystarczy wskazać agentowi ich adresy URL. Aby przeprowadzić test lokalny, potrzebujemy 2 rzeczy:

  1. Sprzedawcy testowi – serwery lokalne, które symulują punkty końcowe UCP, dzięki czemu możesz przeprowadzać testy.
  2. Pomocnicze protokoły – cienkie otoczki HTTP dla UCP i AP2 (w wersji produkcyjnej zastępują je oficjalne pakiety SDK).

Uwaga: nie musisz uważnie czytać tego kodu. Te pliki symulują to, co zapewniają prawdziwa infrastruktura i zestawy SDK. Skopiuj je w niezmienionej formie.

Pomocnicy protokołu

UCP i AP2 nie mają jeszcze pakietów SDK klienta – te 2 pliki obsługują infrastrukturę HTTP.

Utwórz ucp.py:

"""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()

Utwórz ap2.py:

"""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()

Sprzedawcy testowi

Utwórz merchants.py:

"""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

Spowoduje to utworzenie 2 serwerów FastAPI, z których każdy będzie miał 2 punkty końcowe:

  • GET /.well-known/ucp – UCP discovery. Zwraca możliwości sprzedawcy, adres URL punktu końcowego MCP i akceptowane formy płatności.
  • POST /mcp – operacje MCP (Model Context Protocol). Obsługuje wywołania JSON-RPC 2.0 dotyczące wyszukiwania katalogu, realizacji transakcji, rabatów i płatności.

Uruchom sprzedawców w nowym terminalu – muszą oni pozostać aktywni:

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

Zobaczysz, że:

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

Wróć do pierwszego terminala i sprawdź wykrywanie UCP:

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

Powinny być widoczne możliwości sprzedawcy, adres URL punktu końcowego MCP i programy obsługi płatności.

8. Uruchamianie agenta za pomocą ADK Web

Skorzystajmy z wbudowanego interfejsu internetowego interfejsu wiersza poleceń ADK. Zapewnia interfejs czatu w przeglądarce i automatycznie obsługuje prośby o potwierdzenie narzędzia.

Projekt powinien teraz wyglądać tak:

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

Wypróbuj

W bieżącym terminalu przejdź do katalogu nadrzędnego (folder wyżej niż agent_payments) i uruchom interfejs ADK Web UI:

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

Powinny pojawić się dane wyjściowe wskazujące, że serwer działa, oraz adres URL (zwykle http://localhost:8000 lub podobny).

Porozmawiaj z pracownikiem obsługi klienta

  1. Otwórz w przeglądarce adres URL podany przez adk web.
  2. Zobaczysz interfejs czatu.
  3. Spróbuj zapytać: „Jakie filmy są teraz wyświetlane?”
  4. Agent odkryje kina i wyszuka katalogi za kulisami, agregując wyniki od obu sprzedawców za pomocą UCP.
  5. Poproś o zarezerwowanie biletów: „Zarezerwuj 2 bilety na film Oppenheimer na godzinę 19:00”.
  6. Gdy agent spróbuje zadzwonić pod numer complete_purchase, zobaczysz, jak w interfejsie internetowym ADK pojawi się okno dialogowe z potwierdzeniem lub karta.
  7. Aby autoryzować transakcję, odpowiedz na czacie, podając ten dokładny ciąg znaków JSON: {"confirmed": true}.
  8. Przedstawiciel sfinalizuje zakup i prześle Ci potwierdzenie zamówienia z kodami biletów.

Oto, co się działo za kulisami:

  1. create_checkout → sprzedawca zwrócił AP2 CartMandate (podpisana blokada ceny)
  2. complete_purchase → utworzył PaymentMandate, podpisał go (mock SHA-256) i wysłał oba mandaty do sprzedawcy.
  3. Sprzedawca zweryfikował oba podpisy → wystawiono bilety (w wersji demonstracyjnej)

9. Czyszczenie danych

Aby uniknąć pozostawienia uruchomionych serwerów lokalnych, zwolnij miejsce na zasoby:

  1. W terminalu, w którym działa adk web, naciśnij Ctrl+C, aby zatrzymać serwer agenta.
  2. W terminalu, w którym działa python merchants.py, naciśnij Ctrl+C, aby zatrzymać fikcyjnych sprzedawców.
  3. Dezaktywuj środowisko wirtualne w obu terminalach, uruchamiając polecenie:
deactivate
  1. (Opcjonalnie) Jeśli na potrzeby tego ćwiczenia w Codelabs został przez Ciebie utworzony nowy projekt Google Cloud, który chcesz usunąć, uruchom to polecenie:
gcloud projects delete $GOOGLE_CLOUD_PROJECT

10. Gratulacje! 🎉

Utworzono agenta ADK, który wykrywa sprzedawców, przegląda katalogi i dokonuje zakupów za pomocą UCP i AP2.

Czego się dowiedziałeś

W tym ćwiczeniu utworzyliśmy agenta ADK, który obsługuje bezpieczne przepływy handlowe. Oto podsumowanie tego, co udało Ci się stworzyć, i najważniejszych zastosowanych przez Ciebie pojęć:

Co udało Ci się stworzyć:

  • 5 narzędzi agentowych obejmujących operacje UCP i AP2: odkrywanie, przeszukiwanie katalogu, płatność.
  • Podpisanie upoważnienia AP2 – CartMandate (blokada ceny sprzedawcy) + PaymentMandate (autoryzacja użytkownika).
  • Wyszukiwanie u wielu sprzedawców – jeden agent wysyła zapytania do wielu kin i łączy wyniki.

Kluczowe pojęcia:

Protokół

Działanie

Jak to działa

UCP Discovery

Agent znajduje sprzedawców i ich możliwości

/.well-known/ucp → możliwości, punkt końcowy MCP, formy płatności

UCP MCP

Agent przegląda katalogi i tworzy płatności

Wywołania JSON-RPC 2.0 do punktu końcowego MCP sprzedawcy

AP2 CartMandate

Sprzedawca blokuje podaną cenę

Podpisany przez sprzedawcę, zawiera łączną kwotę i datę ważności

AP2 PaymentMandate

Użytkownik autoryzuje obciążenie

Podpisany przez użytkownika, odwołuje się do CartMandate

Co się zmieniło w produkcji?

W tym laboratorium używane są makiety. W wersji produkcyjnej:

  • Wykrywanie UCP odbywa się na podstawie rejestru, a nie adresów URL localhost zakodowanych na stałe.
  • Punkty końcowe MCP są hostowane przez prawdziwych sprzedawców – ten sam protokół JSON-RPC 2.0, prawdziwe zasoby reklamowe.
  • Dokumenty AP2 są podpisywane za pomocą sd-jwt-vc, a nie funkcji skrótu SHA-256.
  • Autoryzacja płatności korzysta z pakietu SDK portfela AP2 z prośbami o zgodę użytkownika.
  • Frontend renderuje wyniki narzędzia jako bogaty interfejs użytkownika (siatki produktów, podsumowania płatności, karty potwierdzenia).

Dalsze kroki

Dokumentacja