エージェンティックコマースについて、日本で一番早く、詳しく、深く情報を発信するメディアRSS
AGENTIC COMMERCE LAB — エージェンティックコマースラボ
検証・実装2026.09.08 公開今すぐ試せる

AI に「クッキー買って」と頼むと何が起きるか — UCP 公式サンプルで購入完了まで

UCP の公式サンプルを Gemini API キーだけで起動し、チャットで注文完了まで通しました。店舗の /.well-known/ucp と Checkout の 3 状態を実測ログで分解し、同梱状態で止まった 2 か所も記録します。

鈴木 直登鈴木 直登株式会社 STRACT / ソフトウェアエンジニアXB!f
検証・実装画像: 本ラボ実測(公式サンプルのチャット画面。検索・金額確定・注文確定の 3 場面)ビデオ解説あり

「在庫のあるクッキーを見せて」と AI に話しかけ、住所を伝え、カードを選んで「Confirm Purchase」を押す。2 分ほどで、注文番号つきの確認が返ってきました。

本稿は、Google と Shopify が主導する UCP(Universal Commerce Protocol)の公式サンプルを手元で動かした記録です。画面の裏で購買側と店舗の間に何が流れていたかを、実測ログで分解します。UCP 連載の第 1 回にあたります。

公式サンプルは Gemini の利用キー(API キー。Google AI Studio で発行できる)だけで起動でき、クラウドの契約も店舗としての審査も要りません。

ただし 8月30日時点の同梱状態(公開されているコード一式を取得したままの状態)では 2 か所で止まり、16 行の最小修正を加えて完走しました。

3行まとめTL;DR
  1. UCP 公式サンプルは Gemini API キーだけで起動でき、チャットから注文完了まで手元で再現できる
  2. 店舗は /.well-known/ucp で対応機能(Capability)を公開し、購買側と店舗は名刺を突き合わせてから取引に入る
  3. 購入は「Checkout が 3 つの状態を進む」処理で、同梱状態では途中で止まる箇所が 2 つあった

「まず動かす」理由と、今回動かすもの

UCP は、AI エージェントなどの購買側(Platform)と店舗(Business。注文の責任を持つ売り手)が、商品検索・Checkout(買い物かごに相当する取引の入れ物)・決済・注文を共通の形式でやり取りする規格です。

Platform と Business は仕様書の正式な用語で、Platform は「利用者の代理として店舗を見つけ、取引を進める側」です。UCP は Google が 2026年1月に発表し、Shopify などと主導している規格です(全体像は全体マップ)。仕様は日付つきの版で更新されます。

仕様書だけでは「AI が買い物をすると何が起きるか」が掴みにくいため、本連載は公式サンプルを動かすところから始めます。

動かすのは、公式リポジトリ(コードの公開場所)Universal-Commerce-Protocol/samples にあるサンプル「Cymbal Retail Agent」です。A2A(Agent2Agent — エージェント同士の通信規約)で通信する構成です。

手元で動くのは、ブラウザのチャット画面と、店舗役の AI エージェントの 2 つです。エージェントは Gemini 3 Flash と、Google の開発キット ADK(Agent Development Kit)で作られています。

ここで登場人物を 4 つに切り分けておきます。

WHO DOES WHAT
会話が注文になるまでに通る 4 つの役割 — UCP は AI モデルではない
GEMINI判断する頭脳利用者の言葉を読み、次にどの道具を使うかを決める大規模言語モデル(gemini-3-flash-preview)
ADKエージェントの骨組みGemini に 8 つの道具(tool)を持たせ、会話の状態を保つ Google の開発キット
A2Aエージェント間の通信チャット画面とエージェントの間の通信規約。エージェントの自己紹介ページ(Agent Card)の公開と、決まった形式での往復
UCP商取引データの共通ルールCheckout・配送・決済・注文の「形」と、店舗が対応機能を宣言する仕組み。本連載の主役
※ 4 つの役割は「在庫のあるクッキーを見せて」という言葉が通る順
出典: samples リポジトリ a2a/README.md および docs/00-glossary.md(commit 00333a8)を基に本ラボ作成

図の要点は一つで、UCP は「取引データをどんな形で受け渡すか」だけを決めています。考えるのは Gemini、通信するのは A2A です。

UCP は、A2A・REST(通常の Web API)・MCP のどの経路で話すかも、エージェントの内部をどう作るかも決めません。AI と道具をつなぐ規格として知られる MCP は、このサンプルでは使われていません。

作り方が違うエージェントでも、店舗が公開する同じ「名刺」(次の次の章で見ます)を読んで同じ取引ができるのが規格の役目です。

今回のサンプルでは、購買側(Platform)がチャット画面、店舗側(Business)が Gemini を載せた店舗エージェントです。つまり AI は店舗の側にいて、UCP の通信はこの画面とエージェントの間に流れます。

公式サンプルを起動し、AI に買い物させる

検証環境は次のとおりです。

  • 実施日2026年8月30日
  • マシンmacOS 26.4(Apple Silicon)
  • ランタイムuv 0.11.8 / Python 3.13.13(uv が自動取得)/ Node.js v24.18.0
  • サンプルのコミット00333a8(8月24日時点の main)
  • モデルgemini-3-flash-preview(サンプルの既定値)

手順は README(公式の手順書)どおりで、エージェントとチャット画面を別々のターミナルで起動します。

Bash
git clone https://github.com/Universal-Commerce-Protocol/samples.git
cd samples/a2a/business_agent
uv sync
cp env.example .env          # GOOGLE_API_KEY=... を書き込む
uv run business_agent        # http://localhost:10999
Bash
cd samples/a2a/chat-client
npm install
npm run dev                  # http://localhost:3000

実際には 1 回目の uv run business_agent は起動に失敗しました。不足していた部品を 1 コマンドで足すと起動でき(理由は「同梱状態で止まった 2 か所」の章で。16 行の修正とは別枠です)、ログはこうなります。

検証メモ — エージェントの起動ログ(2026-08-30)
Text
$ uv pip install "a2a-sdk[http-server]"   # 同梱状態では不足していた依存を追加
$ uv run --no-sync business_agent --port 10997
INFO:     Started server process [64218]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://localhost:10997 (Press CTRL+C to quit)

本ラボは、チャット画面とエージェントの間に記録用の中継プログラム(プロキシ)を挟みました。上のログで窓口番号が 10997 なのはそのためで、画面からの 10999 番宛ての通信をプロキシが受けて渡しています。

往復したデータ(JSON という形式の文字列)はそのまま保存しています。プロキシは Python の標準ライブラリだけで書いた 100 行ほどのスクリプトで、公式サンプルには手を入れていません。

ここからは画面の流れです。README の手順は英語ですが、日本語で話しかけても Gemini は問題なく応じました。まず全体の流れを 48 秒の画面収録で見てください。

DEMO VIDEO — 48 秒音声なし・自動再生
「在庫のあるクッキーを見せて」から注文確定まで。文字入力・クリックは人が操作する速さの自動操作で、本文のログとは別に収録した回のため注文番号は異なります
出典: 本ラボ実測(2026-08-30、公式サンプル commit 00333a8 に最小修正を適用した状態)。画面操作はブラウザ自動化(Playwright)で行い、そのまま収録
HANDS-ON — PART 1
「在庫のあるクッキーを見せて」から「Add to Checkout」まで
01商品検索。在庫のあるクッキー 2 点がカードで返る
02カートに追加。Checkout ID が発行され、小計 $4.99 になる
出典: 本ラボ実測(2026-08-30、公式サンプル commit 00333a8 に最小修正を適用した状態)

サンプル店舗の商品は全部で 6 点です。在庫のあるクッキーとして返ってきたのは、Chocochip Cookies($4.99)と Classic Oat Cookies($5.99)の 2 点でした。

カードの「Add to Checkout」を押すと、Checkout Summary が現れます。この時点で店舗側に Checkout が作られ、ID が振られています。

次に「このまま購入したいです」に続けてメールアドレスと宛名・住所を日本語で送りました。Gemini は文章から郵便番号や市区町村を切り出し、店舗のコードに渡して配送先として登録しています。

HANDS-ON — PART 2
住所を伝えると送料と税が加わり、カードを選んで注文が確定する
03配送先が入り、送料 $5.00 と税 $0.50 が加算されて合計 $10.49
04模擬(モック)の VISA を選び Confirm Purchase。注文番号が返り、状態は completed
出典: 本ラボ実測(2026-08-30、同上)

「Complete Payment」を押すと、支払い方法として AMEX・VISA・Mastercard のカード 3 枚が出ます。これはチャット画面に組み込まれた模擬の支払い画面(モック)で、実際の課金は一切起きません。

VISA を選んで「Confirm Purchase」を押すと、注文番号 ORD-e4c1b9d9-… と、届け先・配送方法(Standard Shipping)を含む注文確認が日本語で返ってきました。

見た目は普通のチャットです。では、この裏でチャット画面と店舗は何をやり取りしていたのでしょうか。

裏側で何が起きていたか — /.well-known/ucp と Capability

最初のメッセージの裏で、画面には出ない「名刺交換」が起きています。チャット画面は毎回の要求に UCP-Agent という項目(ヘッダ)で自分のプロフィール URL を添えます。店舗側は初回にそれを取りに行き、自分のプロフィールと突き合わせます(本ラボの記録では取得は 1 回だけでした)。

逆向きは省略されています。仕様では Platform が事前に店舗の名刺を読む手順は任意(MAY)で、チャット画面は読まずに店舗の窓口を設定に決め打ちしています。名刺を取りに行くのは店舗側だけの片方向です。

店舗側のプロフィールは、決まった場所 /.well-known/ucp に公開されています。この URL を取得すると、次の JSON が返ってきました。

検証メモ — 店舗側プロフィール GET /.well-known/ucp(同梱状態・抜粋)
JSON
{
  "ucp": {
    "version": "2026-01-23",
    "services": {
      "dev.ucp.shopping": {
        "version": "2026-01-23",
        "a2a": {
          "endpoint": "http://localhost:10999/.well-known/agent-card.json"
        }
      }
    },
    "capabilities": [
      {
        "version": "2026-01-23",
        "spec": "https://ucp.dev/2026-01-23/specification/shopping/checkout",
        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/checkout.json"
      },
      {
        "version": "2026-01-23",
        "spec": "https://ucp.dev/2026-01-23/specification/shopping/fulfillment",
        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/fulfillment.json",
        "extends": "dev.ucp.shopping.checkout"
      }
    ]
  },
  "payment": {
    "handlers": [
      { "id": "example_payment_provider", "name": "example.payment.provider" }
    ]
  }
}

この JSON が「店舗の名刺」です。名刺を添えない要求は店舗がその場で断ります(本ラボの実測では UCP Extension is required for this agent が返りました)。4 つの項目が何に答えているかを図にまとめました。

BUSINESS PROFILE
店舗の名刺 — /.well-known/ucp の 4 つの項目が答えていること
GET /.well-known/ucpCYMBAL RETAIL AGENT
version
2026-01-23
どの版の UCP か。日付がそのままバージョン番号になる
services
dev.ucp.shoppinga2a: agent-card.json
どこで・どう話すか。この店舗は A2A 経由。A2A 以外の経路(REST・MCP)で話す店舗もあり得る
capabilities
checkoutfulfillment → extends checkout
何ができるか。基本の Checkout に、配送(fulfillment)を拡張として上乗せしている
payment.handlers
example.payment.provider
どの決済手段を受けるか。サンプルではダミーの決済事業者 1 社
出典: 本ラボ実測の /.well-known/ucp(2026-08-30、commit 00333a8)を基に本ラボ作成

Capability とは何か

Capability は、店舗が「この取引機能を扱えます」と宣言する単位です。名前は dev.ucp.shopping.checkout のように「責任を持つ組織の逆順ドメイン・業務領域・機能」の順で、版・仕様書(spec)・データ構造(schema)の場所を添えます。

上の JSON に名前が無いのは同梱状態の不備です(「同梱状態で止まった 2 か所」の章で扱います)。

仕様書の命名規則では、自社ドメインを逆順にした名前で仕様書と schema を自社に置けば、ポイント適用や分割払いのような独自の拡張を名乗れます。後述の交渉で共通部分だけが残るため、対応していない購買側には基本の Checkout だけが見え、互換性は壊れません。

extends は「どの機能を土台に拡張するか」です。図のとおり配送(fulfillment)が Checkout に配送先と配送方法の欄を足しています。

購買側も同じ形式の名刺を持ちます。UCP-Agent ヘッダで知らせる URL(http://localhost:3000/profile/agent_profile.json)の中身が次です。両者の共通部分がこの取引で使う機能になり、公式ドキュメントはこれを Capability Negotiation(交渉)と呼びます。

検証メモ — 購買側プロフィール GET /profile/agent_profile.json(同梱状態・抜粋)
JSON
{
  "ucp": {
    "version": "2026-01-23",
    "capabilities": [
      {
        "version": "2026-01-23",
        "spec": "https://ucp.dev/2026-01-23/specification/shopping/checkout",
        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/checkout.json"
      },
      {
        "version": "2026-01-23",
        "spec": "https://ucp.dev/2026-01-23/specification/shopping/fulfillment",
        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/fulfillment.json",
        "extends": "dev.ucp.shopping.checkout"
      },
      {
        "version": "2026-01-23",
        "spec": "https://ucp.dev/2026-01-23/specification/shopping/discount",
        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/discount.json",
        "extends": "dev.ucp.shopping.checkout"
      },
      { "version": "2026-01-23", "extends": "dev.ucp.shopping.checkout" }
    ]
  },
  "payment": {
    "handlers": [
      { "id": "example_payment_provider", "name": "example.payment.provider",
        "instrument_schemas": [ "…/types/card_payment_instrument.json" ] }
    ]
  }
}

購買側は 4 つの Capability(checkout・fulfillment・discount と、仕様書の指定が無い 1 つ)を名乗り、店舗側は 2 つを名乗っています。共通部分の checkout と fulfillment が、この取引の契約になります。

落ちた discount はこの取引で使えません。本ラボの読みでは、配送のような要の拡張も、購買側が対応して初めて店舗が使える関係にあります。

この交渉が実際にどう実装されているかを、店舗エージェントのコードで確認しました。読み飛ばしても以降の話は繋がります。

検証メモ — エンジニア向け補足 — 交渉と Checkout 型の合成(ucp_profile_resolver.py / helpers/type_generator.py)
Python
# (1) 版の検査: 購買側の版が店舗の版より新しければ拒否する(日付の大小で比較)
client_version = datetime.strptime(client_version, "%Y-%m-%d").date()
merchant_version = datetime.strptime(merchant_version, "%Y-%m-%d").date()
if client_version > merchant_version:
    raise ServerError(error=InternalError(
        message=f"Version {client_version} is not supported. ...",
        data={"code": "VERSION_UNSUPPORTED", "severity": "critical"}))

# (2) 共通 Capability: (name, version) の組が一致するものだけを残す
client_capabilities_set = {
    (capability.name, capability.version.root)
    for capability in client_capabilities
}
common_capabilites = [
    merchant_capability
    for merchant_capability in merchant_capabilities
    if (merchant_capability.name, merchant_capability.version.root)
    in client_capabilities_set
]

# (3) 交渉結果から Checkout のデータ型をセッションごとに合成する
def get_checkout_type(ucp_metadata: UcpMetadata) -> type[Checkout]:
    active_capability_names = {c.name for c in ucp_metadata.capabilities}
    selected_base_models = []
    # UCP_FULFILLMENT_EXTENSION = "dev.ucp.shopping.fulfillment"
    if UCP_FULFILLMENT_EXTENSION in active_capability_names:
        selected_base_models.append(FulfillmentCheckout)
    if UCP_BUYER_CONSENT_EXTENSION in active_capability_names:
        selected_base_models.append(BuyerConsentCheckout)
    if UCP_DISCOUNT_EXTENSION in active_capability_names:
        selected_base_models.append(DiscountCheckout)
    if not selected_base_models:
        return Checkout
    return create_model(
        "DynamicCheckout",
        __base__=tuple(selected_base_models),
        payment=(PaymentResponse, ...),
    )

コードからわかることは 2 つです。まず、共通機能は「名前と版の組」の一致で決まります(版は日付の大小で比べます)。次に、Checkout の形は会話ごとに組み立てられます。交渉で残った拡張(配送・同意・割引)の組み合わせで決まり、固定の型はありません。

この作りには落とし穴があります。名刺の名前が欠けてもエラーにはならず、拡張なしの Checkout に静かに落ちるのです。「同梱状態で止まった 2 か所」の章の詰まり 3 はこれです。

Capability と「8 つの道具」は別物

公式 README の「8 shopping tools」は Gemini に渡す 8 つの道具(tool。プログラム上は関数)で、店舗エージェントの内部実装です。店舗が外に向けて公開する宣言である Capability とは別物です。

TOOL ≠ CAPABILITY
Gemini が持つ 8 つの道具(tool)は、店舗が宣言する 2 つの Capability の上で動く
商品検索UCP Capability の外(A2A の skill として宣言)
search_shopping_catalog
Checkout の作成・更新・完了dev.ucp.shopping.checkout
add_to_checkout
remove_from_checkout
update_checkout
get_checkout
start_payment
complete_checkout
配送先の登録dev.ucp.shopping.fulfillment(extends checkout)
update_customer_details
※ 道具と Capability の対応づけは、各 tool が store.py のどのメソッドを呼ぶかを読んだ本ラボの整理(推定を含む)。tool 名は agent.py の定義どおり
出典: samples a2a/business_agent/src/business_agent/agent.py および data/ucp.json、data/agent_card.json(commit 00333a8)

Gemini がしているのは、会話を読んで次の道具を選ぶことだけです。UCP の型に沿ったデータを作るのは同じエージェント内の店舗コード(store.py)で、名刺を読んで版や機能を決める処理も Gemini を呼ぶ前にプログラムが終えています。8 つの道具は交渉で増減せず、変わるのは道具が扱う Checkout の形です。

Checkout は状態を持って進む

裏側で最も重要なのが Checkout の「状態」です。UCP の Checkout は購入ボタンではなく、状態を持って更新されていく取引データです。このサンプルは incompleteready_for_completecompleted の 3 つを通りました。

CHECKOUT LIFECYCLE
同じ Checkout ID のまま、3 つの状態を進んで注文になる(実測の金額つき)
STATE 01incompleteかご — 情報が足りないadd_to_checkout
Subtotal$4.99
Shipping / Tax
Total$4.99
STATE 02ready_for_complete確認待ち — 金額が確定update_customer_details → start_payment
Subtotal$4.99
Shipping(USPS Standard)$5.00
Tax(10%)$0.50
Total$10.49
STATE 03completed注文成立 — 以後は変更不可complete_checkout
Total$10.49
PaymentVISA ****8888
OrderORD-e4c1b9d9-…
※ 金額はサンプル固定のロジック(送料 $5.00 / 税 10%)
出典: 本ラボ実測の Checkout JSON(2026-08-30、記録用プロキシで保存した往復 002〜004)

記録した JSON を状態ごとに並べます。まず「Add to Checkout」の直後です。金額は米セント単位の整数で、499 が $4.99 を表します。

検証メモ — Checkout JSON — Add to Checkout 直後(抜粋)
JSON
{
  "id": "e4c1b9d9-1063-4fda-bfcc-627d59020a36",
  "status": "incomplete",
  "currency": "USD",
  "line_items": [
    { "item": { "id": "BISC-001", "title": "Chocochip Cookies", "price": 499 },
      "quantity": 1 }
  ],
  "buyer": null,
  "totals": [
    { "type": "subtotal", "amount": 499 },
    { "type": "total", "amount": 499 }
  ]
}

次に、メールと住所を伝えた直後です。店舗は配送方法の選択肢を付けて返し、既定の Standard と税 10% が加算されています。

検証メモ — Checkout JSON — 住所を伝えた直後(抜粋)
JSON
{
  "status": "ready_for_complete",
  "buyer": { "email": "aclab-test@example.com" },
  "fulfillment": {
    "methods": [ {
      "type": "shipping",
      "destinations": [ {
        "first_name": "直登", "last_name": "鈴木", "postal_code": "100-0001",
        "address_region": "東京都", "address_locality": "千代田区",
        "street_address": "千代田1-1", "address_country": "日本" } ],
      "groups": [ {
        "selected_option_id": "standard",
        "options": [
          { "id": "standard", "title": "Standard Shipping", "carrier": "USPS" },
          { "id": "express", "title": "Express Shipping", "carrier": "FedEx" } ] } ]
    } ]
  },
  "totals": [
    { "type": "subtotal", "amount": 499 },
    { "type": "fulfillment", "amount": 500 },
    { "type": "tax", "amount": 50 },
    { "type": "total", "amount": 1049 }
  ]
}

ready_for_complete は「必要な情報が揃い、金額が確定した」という関門の状態です。ここで初めて、チャット画面に「Complete Payment」が表示されます。

支払い方法の選択は画面の中だけで完結し、店舗への通信は発生していません(記録用プロキシに往復が残らないことで確認)。選んだカードはトークン(カード番号の代わりになる引換券のような文字列)に置き換えられ、「Confirm Purchase」でまとめて店舗に送られます。

検証メモ — Confirm Purchase で送られたデータと、返ってきた Checkout(抜粋)
JSON
// → チャット画面から店舗へ
[ { "data": { "action": "complete_checkout" } },
  { "data": { "a2a.ucp.checkout.payment_data": {
        "id": "instr_2", "type": "card",
        "brand": "visa", "last_digits": "8888",
        "handler_id": "example_payment_provider",
        "credential": { "type": "token", "token": "mock_token_87e2083e-…" }
  } } } ]

// ← 店舗からチャット画面へ
{ "status": "completed",
  "payment": { "selected_instrument_id": "instr_2" },
  "order": {
    "id": "ORD-e4c1b9d9-1063-4fda-bfcc-627d59020a36",
    "permalink_url": "https://example.com/order?id=ORD-e4c1b9d9-…" } }

店舗はトークンを模擬の決済処理に通し、Checkout を completed にして注文(order)を生成します。カード番号そのものは店舗に渡っていません。仕様側にもトークン化の手順と tokenized_card 型が用意されており、番号を直接渡さない前提の設計です。

仕様書は、店舗が注文の責任者(Merchant of Record)のまま、カード情報の安全基準(PCI DSS)の対象範囲を広げずにカード決済を受けられる、と説明しています。

店舗に届いた決済データも Gemini には読ませない作りです。要求の形とあわせてコードで確認しました。

検証メモ — エンジニア向け補足 — 要求の形と決済データの経路(agent_executor.py / agent.py)
HTTP
POST / HTTP/1.1            ← 画面の /api を Vite が :10999 へ中継
Content-Type: application/json
UCP-Agent: profile="http://localhost:3000/profile/agent_profile.json"
X-A2A-Extensions: https://ucp.dev/2026-01-23/specification/overview?v=2026-01-23

{"jsonrpc": "2.0", "id": "…", "method": "message/send",
 "params": {"message": {
   "role": "user", "kind": "message",
   "messageId": "…", "contextId": "…",
   "parts": [{"kind": "text", "text": "在庫のあるクッキーを見せて"}]}}}
Python
# 受信時: UCP 拡張の宣言と UCP-Agent ヘッダが無ければ処理しない
if A2A_UCP_EXTENSION_URL not in context.requested_extensions:
    raise ValueError("UCP Extension is required for this agent")
match = re.search(r'profile="([^"]*)"', ucp_agent_header_value)

# 入力の整形(_prepare_input):
# 決済データは Gemini に渡す文字列から抜き取り、セッション状態へ回す
# UCP_PAYMENT_DATA_KEY = "a2a.ucp.checkout.payment_data"
for data_part in data_list:
    for key in [UCP_PAYMENT_DATA_KEY, UCP_RISK_SIGNALS_KEY]:
        if key in data_part:
            value = data_part.pop(key)
            if key == UCP_PAYMENT_DATA_KEY:
                payment_payload[key] = PaymentInstrument.model_validate(value)
            else:
                payment_payload[key] = value
    if data_part:  # 残った {"action": "complete_checkout"} だけを
        query += "\n" + json.dumps(data_part)  # Gemini への入力に足す

# complete_checkout 道具: 決済データはプロンプトではなく状態から読む
payment_data = tool_context.state.get(ADK_PAYMENT_STATE)  # "__payment_data__"
task = mpp.process_payment(
    payment_data[UCP_PAYMENT_DATA_KEY], payment_data[UCP_RISK_SIGNALS_KEY]
)
if task.status.state == TaskState.completed:
    response = store.place_order(checkout_id)  # status="completed"、order を生成
    return {
        UCP_CHECKOUT_KEY: response.model_dump(mode="json"),
        "status": "success",
    }

# after_tool_callback: 道具の結果を退避する
# (after_agent_callback がこれを応答の構造化データ DataPart にする)
ucp_response_keys = [UCP_CHECKOUT_KEY, "a2a.product_results"]
if any(key in tool_response for key in ucp_response_keys):
    tool_context.state[ADK_LATEST_TOOL_RESULT] = tool_response

決済データ(a2a.ucp.checkout.payment_data)は Gemini に渡る前に抜き取られ、会話ごとの保管領域(セッション状態)に入ります。Gemini が見るのは {"action":"complete_checkout"} だけで、カードのトークンは道具の中で状態から読まれます。

「Add to Checkout」のようなボタン操作も文字列として送られ、Gemini が読んで道具を選びます。道具が返した Checkout は、応答の直前に自動で呼ばれる処理(コールバック)で退避され、応答の末尾に構造化データとして付きます。

同梱状態で止まった 2 か所と、動かして気づいたこと

ここまでの画面と JSON ログは、最小修正を加えた状態のものでした。同梱状態(commit 00333a8)では次の 5 点に行き当たり、購入の流れそのものを止めたのは 3 と 4 の 2 か所です(冒頭の「2 か所」)。

#事象原因(本ラボの確認)上流の状況(9月8日時点)
1REST 版サーバーが起動しない依存 ucp-sdk を固定しておらず、8月27日公開の 0.5.0 で読み込み先が移動修正案 PR #222 が提出済み(8月28日)。9月7日に取り込まれた
2A2A 版エージェントが起動しない部品 sse-starlette が依存に含まれていない(a2a-sdk[http-server] の追加で解消)該当 Issue なし
3伝えた住所が Checkout に反映されない両プロフィールの capabilities に name が無く、配送拡張が有効化されない該当 Issue なし
4Confirm Purchase で購入が確定しないカード情報の形がチャット画面と SDK(開発者向けの部品集)の型で食い違い、検証エラー該当 Issue なし
5プロフィールが指す仕様書 URL が 404specification/shopping/checkoutshopping/ が廃止済み修正案 PR #203 が提出済み(8月21日)。9月8日時点で未取り込み

1 と 2 は部品の不足で、それぞれ 1 コマンドで回避できます。5 は名刺に書かれた URL の問題で、購入の流れには影響しません。

AS SHIPPED — ROUND 1
同梱状態では、住所が消え、決済確定で止まった
03'住所を伝えても合計 $4.99 のまま。配送先・送料・税が JSON に無い
04'Confirm Purchase の応答は「理解できない応答を受け取りました」
出典: 本ラボ実測(2026-08-30、公式サンプル commit 00333a8 の同梱状態)

3 の住所が消える問題は、名刺交換の実装に原因がありました。店舗側の ucp.json とチャット側の agent_profile.json はどちらも Capability に name を書いていません(4月17日の更新 #66 で削除)。

「裏側で何が起きていたか」の章の補足で見たとおり、配送拡張は name で有効化されるため、店舗は住所を受け取っても捨て、住所なしのまま ready_for_complete に進んでいました。本ラボは両方のファイルに name を補って再実行し、住所・送料・税が反映されることを確認しました。

4 の決済確定の問題は、チャット画面と Python SDK の食い違いです。画面はカード情報を display の中に入れて送りますが、エージェントが使う ucp-sdk 0.1.0(1月12日公開)の型は brandlast_digits をトップレベルに要求します。

決済データを SDK の型に当てはめる検証(「Checkout は状態を持って進む」の章の補足にある PaymentInstrument.model_validate)が、ここで失敗していました。

ucp.dev で公開されている 2026-01-23 版のスキーマを確認すると、display を使う画面側の形が仕様どおりでした。つまり公開スキーマと同梱 SDK の型がずれていました。

本ラボは画面側の模擬決済に 4 行を足し、両方の形で送るようにして完走させました。修正は合計 3 ファイル・16 行で、差分は次のとおりです。公式サンプルへの報告は、記事公開後に行う予定です。

検証メモ — 購入完了まで通すために当てた最小修正(3 ファイル・16 行)
Diff
--- a/a2a/business_agent/src/business_agent/data/ucp.json      (店舗側の名刺: name を補う)
+++ b/a2a/business_agent/src/business_agent/data/ucp.json
@@ -14,13 +14,15 @@
-        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/checkout.json"
+        "schema": "https://ucp.dev/2026-01-23/schemas/shopping/checkout.json",
+        "name": "dev.ucp.shopping.checkout"
       },
       {
         "version": "2026-01-23",
         "spec": "https://ucp.dev/2026-01-23/specification/shopping/fulfillment",
         "schema": "https://ucp.dev/2026-01-23/schemas/shopping/fulfillment.json",
-        "extends": "dev.ucp.shopping.checkout"
+        "extends": "dev.ucp.shopping.checkout",
+        "name": "dev.ucp.shopping.fulfillment"

--- a/a2a/chat-client/profile/agent_profile.json               (購買側の名刺: 同じく name を補う。3 か所)
+++ b/a2a/chat-client/profile/agent_profile.json
+        "name": "dev.ucp.shopping.checkout"

+        "name": "dev.ucp.shopping.fulfillment"

+        "name": "dev.ucp.shopping.discount"

--- a/a2a/chat-client/mocks/credentialProviderProxy.ts          (模擬決済: SDK の型に合わせて複製)
+++ b/a2a/chat-client/mocks/credentialProviderProxy.ts
@@ -104,6 +104,12 @@ export class CredentialProviderProxy {
     return {
       ...payment_method,
+      // ucp-sdk 0.1.0 の CardPaymentInstrument は brand / last_digits を
+      // トップレベルに要求するため、display の内容を複製する
+      brand: payment_method.display.brand,
+      last_digits: payment_method.display.last_digits,
+      expiry_month: payment_method.display.expiry_month,
+      expiry_year: payment_method.display.expiry_year,
       handler_id: this.handler_id,

この 3 ファイル・16 行の修正で、住所の反映と決済の確定が通るようになりました。名刺に name を書き足すことと、決済データを SDK の型に合わせることの 2 つです。

最新版の仕様では、1 回の買い物はこう変わる

同じリポジトリでも版が違う — 1月版・4月版・8月版

同じ公式リポジトリには、REST 版のサンプルもあります。今回動かした A2A 版が 2026-01-23、REST 版が 2026-04-08 と、異なる版を実装していました。Python SDK の最新タグは v2026-08-25 です。

REST 版は Capability 6 つ・決済手段 3 種を宣言しており、A2A 版の 2 つ・1 種より多い構成です。2 つの名刺を並べると、3 か月足らずで形そのものが変わっていることがわかります。

検証メモ — エンジニア向け補足 — 2 つの版の名刺の形(A2A 版 2026-01-23 / REST 版 2026-04-08・抜粋)
JSON
// A2A 版(2026-01-23)
// capabilities は名前フィールドの無いリスト。決済は ucp の外の payment.handlers
{ "ucp": {
    "version": "2026-01-23",
    "services": { "dev.ucp.shopping": {
        "version": "2026-01-23",
        "a2a": {
          "endpoint": "http://localhost:10999/.well-known/agent-card.json" }
    } },
    "capabilities": [
      { "version": "2026-01-23",
        "spec": "…/checkout", "schema": "…/checkout.json" },
      { "version": "2026-01-23",
        "spec": "…/fulfillment", "schema": "…/fulfillment.json",
        "extends": "dev.ucp.shopping.checkout" } ] },
  "payment": { "handlers": [
      { "id": "example_payment_provider",
        "name": "example.payment.provider", … } ] } }

// REST 版(2026-04-08)
// capabilities は名前をキーにした辞書(版ごとの配列)。決済ハンドラと署名鍵が ucp の中に入る
{ "ucp": {
    "version": "2026-04-08",
    "services": { "dev.ucp.shopping": [ {
        "version": "2026-04-08", "transport": "rest",
        "endpoint": "http://localhost:8182",
        "schema": "…/services/shopping/openapi.json" } ] },
    "capabilities": {
      "dev.ucp.shopping.checkout": [
        { "version": "2026-04-08", "spec": "…", "schema": "…" } ],
      "dev.ucp.shopping.cart":  [ { … } ],
      "dev.ucp.shopping.order": [ { … } ],
      "dev.ucp.shopping.discount": [
        { …, "extends": ["dev.ucp.shopping.checkout",
                         "dev.ucp.shopping.cart"] } ],
      "dev.ucp.shopping.fulfillment": [
        { …, "extends": "dev.ucp.shopping.checkout" } ],
      "dev.ucp.shopping.buyer_consent": [
        { …, "extends": "dev.ucp.shopping.checkout" } ] },
    "payment_handlers": {
      "dev.shopify.shop_pay": [ … ],
      "com.google.pay": [ … ],
      "dev.mock.payment_handler": [ … ] },
    "keys": [ { "kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig",
                "kid": "…", "x": "…", "y": "…" } ] },
  "signing_keys": [ … ] }

違いは 3 つあります。REST 版では Capability の名前が JSON の見出し(キー)になり、A2A 版のリストで起きた「名前が無い」という問題は形の上で起きなくなっています。

services は経路(transport)を明示したリストになり、1 つの店舗が REST と A2A を併記できます。

keyssigning_keys は、通信の改ざんを防ぐ署名の公開鍵です。REST 版サーバーは要求の署名検証と、webhook(店舗からの通知を受け取るために Platform が登録しておく URL)への署名まで実装しています。使い方はこの章の第 4 幕で見ます。

UCP は 1月から 8月までに版を重ね、サンプルと SDK の足並みも揃いきっていません。まだ動いている最中の規格です。

最新版で描く 1 回の買い物 — ふくふく堂の 4 幕

ここから先は机上の例です。根拠は ucp.dev の最新版 2026-08-25 の仕様書だけで、実測はしていません。

追うのは、架空の焼き菓子店「ふくふく堂」(Business)と AI アシスタント(Platform)の 1 回の買い物です。仕様が一続きで定める「探す・買う・買った後」を 4 幕に分けます。今回のサンプルとは逆に、AI が購買側にいます。

UCP 2026-08-25 — ONE PURCHASE
最新版で描く 1 回の買い物 — 4 幕と、通信の向き
ACT 01発見と合意Platform → Business/.well-known/ucp を読み、版と Capability の共通部分を決める。以後の要求に自分の名刺 URL と署名を添える
ACT 02探す(Catalog)Platform → Businesscatalog.search で店内を検索。返る商品 ID をそのまま次の幕へ。価格はこの時点の提示で、確定は Checkout
ACT 03買う(Checkout)Platform → Business作成 → 足りない情報を messages で指摘 → 更新 → 支払いは決済事業者と直接 → 確定。人が要る場面は店舗の画面へ
ACT 04買った後(Order)Business → Platform出荷・返品・返金を店舗が Platform の webhook に署名つきで通知。向きがここだけ逆になる
※ 仕様書(2026-08-25 版)の記述を本ラボが 4 幕に整理した机上の例。実測ではない
出典: ucp.dev 2026-08-25 版 Overview / Catalog / Checkout / Order の各仕様(2026-09-03 参照)を基に本ラボ作成

第 1 幕: 発見と合意

利用者が「3,000 円以内で焼き菓子のギフトを、来週金曜までに実家へ」と頼みます。Platform はふくふく堂の名刺 https://fukufuku.example/.well-known/ucp を読みます。

名刺の項目は、先に見た店舗の名刺の 4 つと同じです。違いは Capability に catalog・discount・order が並び、決済手段が Google Pay で、末尾に署名用の公開鍵がある点です。

版と Capability の突き合わせは、双方が話せる最新の版を選び、親の無い拡張は外す、と手順が仕様書(Intersection Algorithm)に明記されています。古い版しか話せない Platform 向けには supported_versions で版ごとの名刺を用意します。

第 2 幕: 探す

Platform は POST /catalog/search に「焼き菓子 ギフト」と価格の上限を送ります。ふくふく堂は商品と、そのバリエーション(12 個入り 2,800 円 / 18 個入り 3,800 円)を返し、18 個入りには「2 週間待ち」の警告を messages に付けます。

AI は「12 個入りが予算内、18 個入りは予算超過で 2 週間待ち」と伝えられます。価格や在庫はこの時点の提示で、確定は次の幕の Checkout です。

第 3 幕: 買う

利用者が「12 個入りで」と言うと、Platform は検索で得たバリエーション ID をそのまま入れて Checkout を作成します。店舗の応答は次のとおりです(架空の例)。

JSON
// 架空の応答例(2026-08-25 版の形式に沿って本ラボ作成。金額は円の最小単位=円)
{ "id": "chk_001", "status": "incomplete", "currency": "JPY",
  "line_items": [ { "item": { "id": "var_gift_12", "price": 2800 }, "quantity": 1 } ],
  "messages": [ { "type": "error", "code": "field_required",
                  "path": "$.fulfillment.destination", "content": "お届け先を指定してください" } ],
  "continue_url": "https://fukufuku.example/checkout/chk_001" }

incomplete は「足りない情報がある」状態です。何が足りないかは messagespath で機械可読に示されるので、AI は「お届け先を教えてください」と聞けます。

住所と割引コードを送って更新すると、送料・税・割引が計算され、ready_for_complete になります。AI は「合計 3,622 円、9月10日までに届きます。予算を超えますが進めますか」と確認できます。

支払いは、店舗の名刺にあった Google Pay の手順に従い、Platform が決済事業者と直接やり取りしてトークンを得ます。確定の要求にトークンを添えると、店舗は completed と注文番号を返します(処理が長引く場合は complete_in_progress)。

年齢確認や店舗独自のログインが要るときは requires_escalationcontinue_url を返し、AI は「ここから先は店舗の画面で」と利用者に引き渡します。

第 4 幕: 買った後

ここだけ通信の向きが逆になります。ふくふく堂が出荷すると、Platform が事前に登録した webhook の URL へ、店舗が注文の状態を POST します。要求には RFC 9421 の署名が付き、Platform は店舗の名刺の公開鍵で検証します。

AI は「ふくふく堂から発送されました」と利用者に伝えられます。返品・返金も同じ経路で、注文に対する調整(adjustments)として届きます。

会員登録とログインはどこに入るか

会員・ログインは、UCP では dev.ucp.common.identity_linking という Capability に規定されています(1月版から存在)。

「Platform が利用者の代理として、店舗の会員として振る舞ってよい」という許可を取る仕組みです。許可の取り方は OAuth 2.0 で、「Google でログイン」などに使われている認可の標準です。

アクセスレベル認証できることの例
Publicなし公開カタログの閲覧
Agent-authenticatedPlatform 自身の資格情報ゲスト購入、かごの作成
User-authenticated上記+利用者本人の証明(identity token)保存済み住所、注文履歴、会員価格、ポイント

仕様は「ログインは体験を格上げするもので、関門ではない」と明記しており、ふくふく堂の例なら実家の住所を保存済み住所から呼び出す場面が 3 段階目です。ゲスト購入が基本で、店舗は名刺の scopes に「注文履歴の閲覧には利用者の token が要る」のように、縛る操作だけを列挙します。

ログインが要る場面では、Platform は利用者を店舗の認可画面に送り、利用者が店舗の画面でログインして「AI に注文履歴の閲覧を許可しますか」に同意します。同意画面は店舗側が出します。新規登録の画面も店舗側が用意し、UCP は continue_url でそこへ引き渡すだけです。

本ラボの読みでは、日本の EC 事業者がやることは明快です。会員データベースを AI に開放するのではなく、既存の「〇〇でログイン」連携と同じ OAuth の認可サーバーとスコープを用意することが、会員対応の仕事になります。

今回のサンプルとの差分

場面今回のサンプルの実装2026-08-25 版の仕様
版の合意日付の大小だけ(新しすぎたら拒否)supported_versions で複数版を併存し、最新を選ぶ(最新版で追加)
Capability の合意名前と版の一致のみ加えて、親の無い拡張を削る(複数親は最新版で追加)
検索UCP の外(A2A の skill として実装)catalog.search / lookup。ID がそのまま Checkout へ(最新版で追加)
Checkout の状態3 つだけ通る6 つ(requires_escalationcomplete_in_progresscanceled を含む)
足りない情報の伝え方文章で聞き返すmessages[].path で機械可読
人の介在実装なしcontinue_url で店舗の画面に引き渡す
会員・ログインなし(ゲストのみ)identity_linking(OAuth 2.0)で「格上げ」
署名なし要求と webhook に RFC 9421 署名、名刺に公開鍵(最新版で追加)
買った後注文番号を返して終わりOrder webhook で出荷・返品まで店舗から通知

右列の多くは 2026-01-23 版の仕様にも既にあり、サンプルが実装していないだけです。1月版に無く最新版で加わったものには「最新版で追加」と記しました(本ラボが両版のページを比較。9月3日参照)。

第 1 回の持ち帰りと次回

  • UCP は AI モデルではない考えるのは Gemini、通信は A2A、UCP は取引データの形と機能の宣言を決める
  • 店舗は /.well-known/ucp で名刺を出す版・経路・Capability・決済手段が書かれ、購買側と店舗はそれを突き合わせてから取引に入る
  • 道具と Capability は別物8 つの道具はエージェントの内部実装で、店舗の宣言は 2 つの Capability だった
  • Checkout は状態を持つincomplete → ready_for_complete → completed と進み、決済はトークンで渡って注文が生まれる

なお「今すぐ試せる」のはサンプルの話で、Google 検索 AI モードの実際のチェックアウトは日本ではまだ対象外です(全体マップ参照)。

次回からは店舗役を自作の EC に置き換えて /.well-known/ucp と Checkout を実装し、公式の適合テスト conformance に合格するところまでを扱います。今回見つけた「名刺の name」と「決済データの形」が、そのまま要注意点になります。

最新版の仕様の詳細と、1月から 8月までの版ごとの変化は、別記事で詳しく扱う予定です。

ビデオポッドキャストVIDEO PODCAST — EP.006 / 21:54

全記事で収録し、各チャネル向けに編集して展開しています(編集方針)

執筆後記WRITER'S NOTE
鈴木 直登

今回は公式サンプルを動かして、UCP の理解を進めることができました。プロトコルは約束事なので、実際のやり取りを見て初めてイメージがつき、既存の通信の上に乗る「新しい技術ではなくルール」なのだと分かりました。

UCP はまだ策定中で、複雑なコマースの領域で何が規約に取り込まれるかを今後も注視しつつ、規約が整って実装が進めばエージェンティックコマースは一気に進むと思っています。次回は自分で UCP の部分を実装して、さらに理解を深めていきます。

鈴木 直登 — 全記事に執筆後記を掲載しています(編集方針)
出典・参照SOURCES
鈴木 直登
鈴木 直登 Naoto Suzuki
株式会社 STRACT / ソフトウェアエンジニア

プロダクトコードの設計・レビューを担う。ACLAB では UCP 実装連載のオーナーとして、公式コンフォーマンステスト合格までを全公開する。

鈴木 直登の記事一覧 →