Безопасная агентская коммерция с использованием AP2 и UCP

1. Обзор

В этом практическом занятии вы запустите агент ADK , который будет бронировать билеты в кино у нескольких кинотеатральных компаний, используя два протокола электронной коммерции с открытым исходным кодом:

  • UCP (Universal Commerce Protocol) : Стандарт, позволяющий агентам находить продавцов, искать товары в каталогах и управлять процессами оформления заказа.
  • AP2 (Agent Payments Protocol) : протокол для безопасной и проверяемой авторизации платежей с использованием криптографически подписанных мандатов.

Демонстрационное приложение CineAgent подключается к двум имитированным кинотеатрам с различными возможностями (выбор мест, специализированные форматы и способы оплаты) и управляет полным процессом бронирования от поиска до оплаты.

Что вы узнаете

  • Как работает поиск продавцов UCP через профили /.well-known/ucp
  • Как агент ADK использует UCP для поиска по каталогам и создания заказов.
  • Как требования AP2 (CartMandate, PaymentMandate) обеспечивают безопасность транзакций
  • Как работают сквозные протоколы UCP и AP2 для обеспечения безопасности агентской электронной коммерции

Что вам понадобится

  • Проект Google Cloud с включенной функцией выставления счетов.
  • Веб-браузер, например Chrome.
  • Python 3.11+

Этот практический урок предназначен для разработчиков среднего уровня, знакомых с Python и Google Cloud. Выполнение урока занимает приблизительно 15 минут.

Стоимость ресурсов, созданных в рамках этого практического занятия, должна составлять менее 5 долларов.

2. Понимание протоколов UCP и AP2

Прежде чем приступить к созданию агента, давайте разберемся в двух протоколах, которые обеспечивают безопасную агентскую коммерцию.

Универсальный протокол коммерции ( UCP )

UCP стандартизирует взаимодействие агентов ИИ с продавцами. Он решает проблему необходимости изучения агентами пользовательских API для каждого отдельного магазина, вводя стандартизированную модель ресурсов.

Как это работает:

  1. Обнаружение : Каждый продавец, соответствующий стандарту UCP, предоставляет свой профиль по стандартному адресу: /.well-known/ucp . Пример: конечная точка UCP компании Everlane . Когда агент считывает этот профиль, он ищет:
    • Возможности : Автономные основные функции, которые поддерживает компания, такие как поиск по каталогу или оформление заказа.
    • Сервисы : Нижние уровни связи, используемые для обмена данными. Примеры: REST API, MCP (Model Context Protocol), A2A (Agent2Agent Protocol).
    • Расширения : Если продавцу требуется специализированное поведение, он может определить пользовательские расширения в этом профиле.
  2. Операции : После обнаружения агент использует предоставленную конечную точку сервиса для выполнения операций. В этом практическом занятии мы используем протокол контекста модели (MCP) в качестве транспортного протокола сервиса. Агент выполняет вызовы JSON-RPC 2.0 к этой конечной точке для активации обнаруженных возможностей: поиска товаров, создания заказов и завершения покупок.

Рабочий процесс UCP

Протокол агентских платежей ( AP2 )

AP2 стандартизирует порядок авторизации платежей агентами от имени пользователей. Он решает проблему безопасности, связанную с обработкой агентами конфиденциальных платежных данных.

Как это работает:

  1. Мандат корзины : Когда агент создает заказ с использованием протокола UCP, продавец возвращает объект CartMandate . Это JSON-объект, содержащий данные корзины и криптографическую подпись продавца. Он служит гарантией фиксации цены. Продавец не может изменить цену после выдачи этого мандата.
  2. Платежное поручение : После проверки содержимого корзины пользователь (или его представитель) создает PaymentMandate для авторизации платежа. Это PaymentMandate ссылается на платежное CartMandate и включает криптографическую подпись пользователя (или токен авторизации).
  3. Двойная проверка подписи : Продавец получает оба подтверждения. Он проверяет свою подпись в подтверждении CartMandate и подпись пользователя в подтверждении PaymentMandate . Если обе подписи действительны, транзакция продолжается.

Эта система «двойной блокировки» гарантирует, что продавцы не смогут завышать цены, а агенты не смогут тратить деньги без разрешения. В производственной среде для защиты конфиденциальности пользователей используется SD-JWT (Selective Disclosure JWT).

Рабочий процесс AP2

3. Настройте свою среду.

Настройка проекта Google Cloud

Создайте проект в Google Cloud.

  1. В консоли Google Cloud на странице выбора проекта выберите или создайте проект Google Cloud .
  2. Убедитесь, что для вашего облачного проекта включена функция выставления счетов. Узнайте, как проверить, включена ли функция выставления счетов для проекта .

Запустить Cloud Shell

Cloud Shell — это среда командной строки, работающая в Google Cloud и поставляемая с предустановленными необходимыми инструментами.

  1. В верхней части консоли Google Cloud нажмите кнопку «Активировать Cloud Shell» .
  2. После подключения к Cloud Shell подтвердите свою аутентификацию:
    gcloud auth list
    
  3. Убедитесь, что ваш проект настроен:
    gcloud config get project
    
  4. Если параметры вашего проекта заданы не так, как ожидалось, настройте их следующим образом:
    export PROJECT_ID=<YOUR_PROJECT_ID>
    gcloud config set project $PROJECT_ID
    

Доступ к моделям Gemini

В вашей среде Cloud Shell скопируйте и вставьте следующие команды. Это обеспечит доступ к моделям Gemini, которые будет использовать Cine Agent.

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

Структура каталогов установки

Скопируйте и вставьте следующие команды, чтобы создать новый каталог для агента:

mkdir -p agent_payments
cd agent_payments

Установите зависимости

В Google Cloud Shell предустановлен uv для управления средой и зависимостями.

  1. Создайте файл pyproject.toml в корневой папке agent_payments и добавьте в него следующее содержимое. Этот файл определяет метаданные проекта и зависимости.
[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. Выполните следующую команду, чтобы создать виртуальное окружение и установить все зависимости:
uv sync
  1. Активируйте виртуальную среду, созданную с помощью uv :
source .venv/bin/activate

4. Определите агента.

Прежде чем писать логику для работы инструмента, давайте определим самого агента в файле с именем agent.py . Этот агент будет выступать в роли координатора процесса бронирования билетов в кино.

Создайте 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),
    ],
)

Пояснение к коду

Давайте разберем, что происходит в этом определении агента:

  • model="gemini-3.1-pro-preview" : Мы используем последнюю предварительную модель Gemini Pro для сложных рассуждений и использования инструментов.
  • instruction : Это подсказка, которая направляет поведение агента. Она явно указывает агенту использовать UCP и AP2, перечисляет доступные инструменты и устанавливает важные правила, такие как «Никогда не выдумывайте данные» и «Цены указаны в центах».
  • tools : Это список функций Python (которые мы создадим позже), которые агент может вызывать по запросу пользователя.
  • require_confirmation : Вы можете обернуть любой инструмент функцией FunctionTool(my_function,require_confirmation=True) . При срабатывании агент приостанавливает работу и ожидает простого подтверждения «да» или «нет», прежде чем выполнить инструмент. В данном случае, перед выполнением инструмента complete_purchase , агент приостанавливает работу для подтверждения от человека.

Список инструментов

В определении агента указано, что нам нужно построить. Каждый инструмент соответствует определенной операции в протоколе UCP или AP2:

Инструмент

Что это делает

Действие протокола

discover_theaters

Найдите продавцов и узнайте об их возможностях.

Запросы /.well-known/ucp

search_movies

Поиск по каталогам различных продавцов.

JSON-RPC в конечную точку MCP

get_movie_detail

Узнайте расписание сеансов в конкретном заведении.

JSON-RPC в конечную точку MCP

create_checkout

Начать оформление заказа

JSON-RPC в конечную точку MCP

complete_purchase

Авторизуйте платеж и завершите оформление заказа.

Подписывает мандат AP2 и направляет его в MCP.

Модель Gemini будет определять, когда вызывать каждый инструмент, исходя из диалога. Наша следующая задача — реализовать функции каждого инструмента в tools.py .

5. Создайте инструменты агента: обнаружение и просмотр.

Теперь давайте реализуем инструменты, которые агент будет использовать для просмотра и поиска фильмов. Каждый инструмент представляет собой обертку операции UCP.

Создайте новый файл с именем tools.py и скопируйте в него следующий код:

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

Понимание настройки

Чтобы в этом практическом занятии основное внимание уделялось созданию агента, мы используем два вспомогательных класса, UCPClient и AP2Handler , которые мы рассмотрим позже.

  • Что это такое? Это написанные вручную вспомогательные классы, которые мы создали для этого практического занятия, чтобы имитировать взаимодействие с условными продавцами. Поскольку официальные SDK UCP и AP2 пока недоступны , мы используем эти вспомогательные классы, чтобы восполнить этот пробел. В производственной среде вы будете использовать официальные SDK, как только они станут доступны.
  • Пока что рассматривайте их как вспомогательные объекты :
    • _ucp.discover(url) : Получает профиль продавца.
    • _ucp.mcp_call(url, method, params) : Отправляет JSON-RPC 2.0 запрос на конечную точку MCP продавца.

Откройте для себя театры

Этот инструмент — первый шаг в процессе UCP. Он определяет, какие продавцы существуют и что они поддерживают.

Добавить в 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)

Что это делает :

  1. Программа перебирает список URL-адресов продавцов, предоставленный сервером. В этом практическом занятии мы создадим двух фиктивных продавцов в последующем разделе.
  2. Для каждого URL-адреса вызывается функция _ucp.discover(url) , которая обращается к конечной точке /.well-known/ucp .
  3. Он собирает имя, возможности и обработчики платежей в сводный список.
  4. Функция возвращает список в виде строки JSON, которую затем может прочитать агент.

Поиск фильмов

Этот инструмент выполняет поиск по всем найденным продавцам и объединяет результаты. Это крайне важно, поскольку один и тот же фильм может демонстрироваться в нескольких кинотеатрах с разными форматами (IMAX, Dolby) и разными ценами.

Добавить в 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)

Что это делает :

  1. Он проходит через все обнаруженные театры военных действий.
  2. Он отправляет JSON-RPC-запрос к каталогу поиска ( _ucp.mcp_call(url, "search_catalog", {"query": query}) ) на конечную точку MCP продавца.
  3. Затем выполняется небольшая обработка результатов для поиска фильмов и их «вариантов» (которые представляют собой конкретное время показа и форматы). Фильмы группируются по ID, чтобы пользователь не видел повторяющихся записей.

Получить подробную информацию о фильме

Этот инструмент позволяет получить полную информацию о конкретном фильме в конкретном кинотеатре.

Добавить в 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)

Что это делает :

  • Этот метод вызывает метод lookup_catalog в конечной точке MCP продавца, передавая конкретный movie_id . Он возвращает подробную информацию, такую ​​как расписание сеансов и наличие мест в конкретном кинотеатре.

6. Создайте инструменты для агентов: оформление заказа и оплата.

Эти инструменты обрабатывают процесс оформления заказа и покупки. Именно здесь вступает в игру протокол AP2, обеспечивающий безопасность транзакций.

Создать оформление заказа

Этот инструмент запускает сеанс оформления заказа в конкретном кинотеатре на конкретное время сеанса.

Добавить в 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)

Что это делает :

  1. Он вызывает метод create_checkout на конечной точке MCP продавца.
  2. Он передает идентификатор showtime_id и quantity запрошенные пользователем.
  3. Продавец возвращает JSON-объект, содержащий AP2 CartMandate .

Примечание : Если вы изучите данные ответа, то увидите, что ap2.cart_mandate содержит поле merchant_authorization . Это криптографическая подпись продавца, фиксирующая указанную цену. Он не сможет изменить её позже!

Завершить покупку

В этом инструменте происходит три вещи:

  1. Мы получаем CartMandate из формы оформления заказа и проверяем его (имитация) .
  2. Создайте и подпишите платежный мандат (макет) .
  3. Отправьте подписанное поручение продавцу для завершения покупки.

Добавить в 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)

Почему два мандата? Мандат CartMandate гарантирует, что продавец не сможет изменить цену после её указания. Мандат PaymentMandate гарантирует, что агент не сможет взимать плату с пользователя без его согласия. Процесс выглядит следующим образом:

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

Контрольная точка: Полный файл tools.py

Теперь ваш файл tools.py должен содержать 5 функций инструментов и инициализацию на уровне модуля для клиентов UCP и AP2. Убедитесь, что он выглядит следующим образом:

"""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. Сделайте его работоспособным.

При работе с реальными продавцами UCP вы просто указываете агенту их URL-адреса, и всё готово. Для этого практического занятия нам нужно протестировать локально две вещи:

  1. Имитаторы торговых точек — локальные серверы, которые моделируют конечные точки UCP, чтобы у вас была возможность проводить тестирование.
  2. Вспомогательные протоколы — тонкие HTTP-обертки для UCP и AP2 (в производственной среде их заменяют официальные SDK).

Примечание : Внимательно читать этот код не нужно. Эти файлы имитируют возможности реальной инфраструктуры и SDK. Копируйте их как есть.

Вспомогательные протоколы

Для UCP и AP2 пока нет клиентских SDK — эти два файла обеспечивают работу HTTP-интерфейса.

Создайте 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()

Создайте 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()

Имитация торговцев

Создайте файл 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

Это создаст два сервера FastAPI, каждый с двумя конечными точками:

  • GET /.well-known/ucp — Обнаружение UCP. Возвращает возможности продавца, URL-адрес конечной точки MCP и принимаемые способы оплаты.
  • POST /mcp — Операции MCP (Model Context Protocol). Обрабатывает вызовы JSON-RPC 2.0 для поиска в каталоге, оформления заказа, скидок и оплаты.

Запустите торговые точки в новом терминале — они должны продолжать работать:

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

Вам следует увидеть:

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

Вернувшись в первый терминал , проверьте обнаружение UCP:

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

Вы должны увидеть возможности продавца, URL-адрес конечной точки MCP и обработчики платежей.

8. Запустите агент через ADK Web.

Давайте воспользуемся встроенным веб-интерфейсом ADK CLI! Он предоставляет чат в браузере и автоматически обрабатывает запросы на подтверждение использования инструмента.

Теперь ваш проект должен выглядеть так:

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

Попробуйте!

В текущем терминале перейдите в родительский каталог (на одну папку выше agent_payments ) и запустите веб-интерфейс ADK:

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

Вы должны увидеть сообщение о том, что сервер запущен, и он предоставит вам URL-адрес (обычно http://localhost:8000 или аналогичный).

Поговорите с агентом

  1. Откройте в браузере URL-адрес, предоставленный adk web .
  2. Вы увидите интерфейс чата.
  3. Попробуйте спросить: "Какие фильмы идут в кинотеатрах?"
  4. Агент будет находить кинотеатры и искать информацию в каталогах, объединяя результаты от обоих продавцов через UCP.
  5. Запрос на бронирование билетов: «Забронируйте 2 билета на концерт Оппенхаймера на 19:00» .
  6. Когда агент пытается позвонить по номеру complete_purchase , обратите внимание, как веб-интерфейс ADK выводит диалоговое окно или карточку с подтверждением !
  7. Для авторизации транзакции ответьте в чате, указав следующую JSON-строку: {"confirmed": true} .
  8. Агент завершит покупку и вернет вам подтверждение заказа с кодами билетов!

Вот что происходило за кулисами:

  1. create_checkout → merchant вернул AP2 CartMandate (signed price lock)
  2. complete_purchase → создал PaymentMandate , подписал его (имитация SHA-256), отправил оба мандата продавцу.
  3. Продавец проверил обе подписи → выдал билеты (в имитационной игре)

9. Уборка

Чтобы избежать работы локальных серверов, очистите ресурсы:

  1. В терминале, где запущена adk web , нажмите Ctrl+C , чтобы остановить сервер-агент.
  2. В терминале, где запущена python merchants.py , нажмите Ctrl+C, чтобы остановить запуск фиктивных продавцов.
  3. Отключите виртуальную среду в обоих терминалах, выполнив следующую команду:
deactivate
  1. (Необязательно) Если вы создали новый проект Google Cloud для этого практического занятия и хотите его удалить, выполните следующую команду:
gcloud projects delete $GOOGLE_CLOUD_PROJECT

10. Поздравляем! 🎉

Вы создали агента ADK, который находит продавцов, просматривает каталоги и совершает покупки, используя UCP и AP2.

Что вы узнали

В этом практическом задании вы создали агент ADK, который обрабатывает безопасные коммерческие потоки. Вот краткое описание того, что вы создали, и ключевых концепций, которые вы применили:

Что вы построили:

  • 5 инструментов для агентов, объединяющих операции UCP и AP2 — поиск, поиск по каталогу, оформление заказа, оплата.
  • Подписание мандата AP2 — CartMandate (фиксация цены продавцом) + PaymentMandate (авторизация пользователя).
  • Поиск по нескольким кинотеатрам — один агент отправляет запросы в несколько кинотеатров и объединяет результаты.

Ключевые понятия:

Протокол

Что это делает

Как это работает

Открытие UCP

Агент находит продавцов и оценивает их возможности.

/.well-known/ucp → возможности, конечная точка MCP, способы оплаты

UCP MCP

Агент просматривает каталоги, оформляет заказы.

Вызовы JSON-RPC 2.0 к конечной точке MCP продавца

AP2 CartMandate

Продавец фиксирует указанную цену.

Подпись продавца, включает общую сумму и срок действия.

AP2 PaymentMandate

Пользователь дает разрешение на списание средств.

Подписано пользователем, содержит ссылку на CartMandate.

В чём заключаются отличия в производстве?

В этом практическом занятии используются моки. В производственной среде:

  • Обнаружение UCP осуществляется путем обращения к реестру, а не к жестко закодированным URL-адресам localhost.
  • Конечные точки MCP размещаются у реальных продавцов — тот же протокол JSON-RPC 2.0, реальный ассортимент товаров.
  • Мандаты AP2 подписываются с помощью sd-jwt-vc, а не хешей SHA-256.
  • Авторизация платежей осуществляется с помощью SDK AP2 Wallet с запросами согласия пользователя.
  • Фронтенд отображает результаты работы инструмента в виде расширенного пользовательского интерфейса (сетки товаров, сводки оформления заказа, карточки подтверждения).

Следующие шаги

Справочная документация