お久しぶりです。
Table of Contents
注意!!
この記事やデモアプリはAP2に入門することが目的ですので、技術的に正確でない(わかりやすさに寄せた表現)もあります。また、AP2の実装リファレンスが正直あまり存在せず、独自解釈も多いと思います。
あくまでも雰囲気を掴んでほしいという気持ちで書いてます。申し訳ございません!
すごいせっかちな人向けに
AP2にできるだけ準拠したデモアプリを作ってみました。

Docker ComposeでAP2を語るうえで必要なサービスが立ち上がり、AP2でのお買い物を擬似体験できます。(もちろん本当にお買い物はできません。)
デモアプリはこちらからCloneしてください。 https://github.com/tubone24/AP2_demo_app/tree/main
docker compose build
docker compose upとすると、http://localhost:3000で立ち上がるはずです。ただし、中で利用しているローカルLLMはDocker Model Runnerで動かしているので、そちらのセットアップは必要です。
AP2に入門してみたくなった。
突然ですが、育休を取っています。毎日子の成長を見られるのはありがたいばかりですが、同時に優秀な同僚が次々に新しいことにチャレンジしている姿をXで見ると辛くなるのも正直なところです。
なので、なにか新しい技術を学んでおかないと復帰後不安で押しつぶされそうなので、重い腰をあげてAP2(Agent Payments Protocol)に入門しようと思います。
どうにも生成AIの世界は謎のプロトコルが次々とでてくる...。この前MCPに入門したと思ったら、A2A、そしてAP2...。困りましたね。
AP2とは、Googleによると、
主要な決済企業やテクノロジー企業と共同で開発されたオープン プロトコルで、プラットフォームをまたいでエージェント主導の決済を安全に開始、実行するもの
とのことで、要はAIエージェントを絡めたお買い物、決済を考えたときに起こり得るさまざまな不都合や問題、紛争をうまい具合に解決する仕組みです。
そもそもAIエージェントを用いたお買い物で起こり得る問題ってなんでしょうか。
まず人間の買い物を想像してみましょう
AP2を考える前に、まずは人間の買い物を想像してみましょう。

Aさんは、「かわいい犬のグッズ」がほしいな〜と思っているとします。ちなみに「『かわいい犬のグッズ』がほしいな〜」という気持ち(意図)のことをAP2では、Intentといいます。
しばらく歩いていると、むぎぼーショップというかわいい柴犬をモチーフにしたお店を発見しました。入ってみましょう。
むぎぼーショップは店員さんがとても優秀で、お客さまの気持ちによりそった最適な商品を提示してくれるお店でした。細かい要望を伝えると、在庫を確認した後予算内でいくつか商品をピックアップし、おすすめセットを作ってくれました。

Aさんもおすすめセットに納得したようで支払い手続きに進みます。お店に設置されているxxPayの端末を使ってお会計を済ませます。商品と領収書をもらってお買い物完了です。

以上が人間のお買い物でした。
ポイントとしてAP2で想定されるお買い物は、スーパーマーケットのようなセルフサービス方式ではなく、薬局のような対面販売を想定しています。
なので、買い物カゴに商品を入れてくれるのは店員さんで、その買い物カゴをユーザーが確認してOKを出して購入に進む、という流れです。
つまり、生成AIで作成した下の画像のように、「この症状ならこの薬とこの薬の組み合わせですね!」と、商品棚から店員さんがピックアップしてくれるようなスタイルです。(これを意識しておくかどうかでだいぶ理解が変わるはず。)

この買い物の流れにAIエージェントを登場させたらどうなるでしょうか。
その買い物、誰が間違えた...?
では、この流れでお買い物の一部をAIエージェントに任せることを想定してみます。
例えば、ユーザーの代わりにお買い物をするお買い物エージェント(Shopping Agent、以降SA)とお店の店員さんの代わりに業務をするお店エージェント(Merchant Agent、以降MA)が追加された構図を考えていきます。
ハルシネーション
まず思い浮かぶのは、AIエージェントのハルシネーションです。LLMがあたかも知ったように誤った情報をべらべら話すあの現象です。
お買い物というシチュエーションでこれが起きると大変です。しかも、考えられるミスのパターンが多いのも特徴です。
一番想像しやすいのは、ユーザーの意図(Intent)をSAが間違って理解してMAに伝えてしまうパターンです。伝言ゲーム的に間違った意図のもと購入が進められてしまいます。安い買い物ならまだしも、数百万の決済を勝手にやられては困ります。

他にもMAが意図を間違えて解釈するケースもありそうです。

さらに複雑なケースを想定すると、ユーザーの意図は正しく伝えられても、お店側の都合、例えばお得意さんしか売ってはいけない商品を売ってしまう、というMA側のコンテキスト不足によるミスもありそうです。

もっと悩ましいパターン
もっと悩ましいパターンも考えられます。例えばAさんが、AIエージェントには「かわいい犬のグッズがほしい」とお願いしておきながら、その意思を途中で変えてしまう、というものです。
心変わりしたり、いたずらだったり、勘違いだったり色々理由はあると思いますが、ユーザーから取引完了後に「実はかわいい猫のグッズがほしかった」と言われたら誰が責任を取るべきかわからなくなってしまうのも問題です。

また、ユーザーになりすましてAIエージェントに購入の指示をされたりした場合にも同様の問題が起きてしまう可能性もあります。(なりすまし問題)

AIエージェントが情報を知りすぎる問題
また、少し話の毛色は変わりますが、AIエージェント(SA)がユーザーの代わりにお買い物をする場合、ユーザーのお財布を勝手に使えてしまったりすることも問題です。
クレジットカード決済する場合を想定すると、カード番号の他、有効期限や、CVC(Card Validation Code、カード裏面にある3桁くらいのコード)を知ってしまうことになります。これでは、万が一のときにカード情報が漏洩したり、AIエージェントが暴走して闇雲に高級品を買いまくったりする危険もありそうです。
加えて、AIエージェント(MA)がお店の決済システムに密結合になってしまうこともシステム保守観点で危うさがあります。

AP2ではどんな仕組みでこれらを解決しているのか
AP2ではこれらの課題を高度な暗号技術を組み合わせたデジタル署名をA2A(Agent2Agent)の通信にうまく拡張することで解決します。
シーケンス
まずは、難しい用語抜きでわかりやすい(?)紙芝居をお見せし、それらに出てくる具体的な概念から用語をおさえたほうがきっとわかりやすいと確信したので、しばし紙芝居にお付き合いください。
(説明の都合で各ステップの順番を入れ替えている箇所があります。また、現時点シーケンスが公開されているIllustrative Transaction Flowを参考に作成していますが仕様がはっきりしない箇所もあり推測も多分に含まれます。ご了承ください。)

まずAさんは、「かわいい犬のグッズがほしい」「5000円以内」など具体的な購入意図をお買い物エージェント(Shopping Agent、SA)に指示します。すると、SAはその購入意図を汲み取ってお買い物を始めます。
実際にお買い物に移る前に、ユーザーと購入意図を委任状にしたIntent Mandate、つまり「これから私AはSAに『かわいい犬のグッズを5000円以内』で購入することを委任します。」と言った具合で委任状を取り交わします。Intent Mandateについては後ほど詳しくお話します。
と同時に、「むぎぼーショップ」のお店エージェント(Merchant Agent、MA)がSAに対して自分のお店の紹介も兼ねて名刺交換します。
A2Aに詳しい方ならピンとくると思いますが、この名刺交換というのはA2AにおけるAgent Cardのことです。自身の取り扱い状況やAP2に対応した購入体験ができますよ、ということを伝えてます。

次に、SAはMAと名刺交換した結果をもとにむぎぼーショップと取引をスタートすることを決めました。(実際にはAgent Cardそのものだけでなく、信頼ドメインや証明書チェーンも合わせて取引可能かチェックするはずですがここでは割愛。)
A2AのMessageを用いて、SA-MAのエージェント間通信を実施します。
ここでユーザーと取り交わしたIntent Mandateを送ることで、SAがユーザーの意図を確かに汲んだ購入をしていることをMAに示しつつ、目的の商品カートを作ってもらうことをお願いするわけです。
MAはユーザーのIntentを受けて、どんな商品セットがよいか検討を始めます。お店側(Merchant)の在庫状況や各商品の金額、説明などを見極め、ユーザーにぴったりな商品が入った商品カートを作成しにいきます。

さて、MAはついにユーザーにぴったりな商品の入った商品カートを作成しました。この商品カートの作成は本来はお店側(Merchant)が作るべきものですが、MAがお店の代わりに作成しているものです。
なので、委任状の形でお店側の販売行為を委任していることを示す必要があります。
そこで、MAは商品カートの委任状(Cart Mandate)を作成し、お店側に確認と確かに確認したというデジタル署名をお店がつけます。

お店とMAの間での委任状の取り交わしが無事完了したら、その委任状(Cart Mandate)をSAに送信します。この商品カートをSAがユーザーに提示しながら購入を進めていくわけです。

AさんはSAから提示された商品カートの委任状(Cart Mandate)に基づき、自身が購入する商品カートを確定させます。(Cart MandateはA2A Artifactsの形で複数提示もできるため、複数の候補から選択するUIも想定されます。このデモアプリでは複数カートを提示するようにしております。)
確定した商品カートの委任状について、 「確かにAが確定しましたよ」という意味合いで、ユーザーが署名をつけます。(実際にはDevice Attestationの動作が入りますが、Payment Mandateのところで説明します) ユーザーに確認を求めます。
(さきほども述べましたが、Cart Mandateにはユーザー署名は構造上つけることが難しかったです。 なぜならユーザー署名を付けてしまうと、JSON構造が壊れてしまい、その手前のMerchantの署名が壊れてしまうためです。仕様として定義されているものの、実装上の取り扱いは工夫する必要がありそうです。)

同時に、SAはユーザーが商品を購入するにあたっての支払い方法についても確認する必要があります。AP2では、Credential Provider(CP)という謎の概念がここでしゃしゃり出てきます。
公式ドキュメントによるとCPは、
The User’s Credentials Provider (CP): A specialized entity responsible for the secure management and execution of payments credentials (e.g. a digital Wallet). It holds knowledge of the User's available payment methods, gets user consent (if deemed necessary) to share credentials with the SA, selects the optimal payment method based on user preferences and transaction context, and handles payment scenarios like errors, declines and transaction challenges gracefully.
とあり、CPはユーザーの支払いおよびID資格情報を安全に管理・実行する専門的なエンティティの意味です。
IDウォレットと決済ウォレットをあわせたデジタルウォレットと思っていただいて良さそうです。(正確には主に支払い資格情報の安全管理とユーザー同意フローをCPが担うことが目的なので、IDウォレットなどがCPから独立していてもOKと思う。)
GoogleとAppleが対応に向けて進めているようです。ポイントは決済だけでなく、そのユーザー自体を証明する役割も持っています。
のちのちユーザーに支払い方法を確認するため、CPに紐づく支払い方法を確認します。現状のAP2仕様(v0.1)ではクレジットカードやデビットカードが選択可能だそうですが、将来的には様々な決済サービスにも対応可能とのことです。
ここでポイントなのは、SAは支払い情報をユーザーに選択させるための最低限の情報(たとえばクレジットカードの下4桁とか)のみ受け取り、カード番号やCVCを含まない、つまりPCIデータに当たらない情報のみ受け取ります。

では支払いに移りましょう。SAはさきほどCPから受け取った支払い情報からユーザーが利用する支払い方法を確定させます。ユーザーが支払いに利用するカードを確定させたら、CPにそのカードのトークン(支払い方法トークン、payment method token)を発行依頼します。
このトークンは、一時的な支払い利用の可能なカード情報となります。ただし、トークンには一切支払いに必要な情報(PCIデータ、カード番号とか)は含まれません。CPにはトークンと実カード情報のマッピングが存在するため、CPを経由することで支払いが可能となるわけです。
支払い方法トークンが取得できたらSAは支払い委任状(Payment Mandate)を作成します。これは商品の細かな明細を含まない合計金額や支払い方法を明記した決済に関する委任状となります。まず、ユーザーに「私SAはAさんに変わってこの委任状の通り支払いしますよ」という委任状をSAは提示し、ユーザーから署名をもらうことでユーザー意思を確定させます。
このとき、アプリではAttestationの動きが入ります。公式ドキュメントから外れますが、CSAのブログ記事ではパスワード認証は推奨されずより強固な認証が求められているようです。
Guidelines for secure implementation include integration with Strong Customer Authentication (SCA), configuration of Intent Mandate TTLs, and dispute resolution protocols.
TPM、Secure Enclaveなどを用いて、ハードウェアに裏打ちされたキーとインセッション認証(生体認証など)を通じて実行することで確実にAさんが承認しましたよ。という血判状になるんですね。

ユーザー署名付き支払い委任状(Payment Mandate)が出来上がったら、MAにPayment Mandateを送信し、いよいよ決済に移ります。
ここで重要なのは、決済処理そのものはMAは実施せず、お店に紐づく決済エンティティ(Merchant Payment Processor、以降MPP)が実施します。これは決済関連の処理をMAとわけるという明確な職務分離というAP2の基本概念に従った実装となります。

支払いが完了したら領収書をMPPが発行し、CPとSAに送信します。SAは領収書を受け取ってユーザーに取引の完了を伝えます。これがAP2で実現される動きです。
お疲れ様でした!
委任状(Mandate)
改めて、AP2を語るうえで欠かせないのが委任状(Mandate)と呼ばれるデジタル署名付き委任状の仕組みを再確認しましょう。
ユーザー、お店がそれぞれ持っている秘密鍵を使って委任状にデジタル署名をし、その検証を各工程で実施することで、その委任が各エンティティによって確実に実施したことを保証してます。デジタル署名を使ってそれぞれのエンティティが委任状に承認、署名、検証する仕組みがAP2の一番面白いところと言えるでしょう。
委任状(Mandate)には3種類あり、各シーケンスで必要な情報を送受信します。また、委任状には連鎖(チェーン)があり、Payment MandateがCart Mandateを、Cart MandateがIntent Mandateを、参照しています。こうすることで、各委任状が1つ前の委任状を根拠に作成しているという関係を作ることができます。
Intent Mandate
まずはIntent Mandateです。これは、ユーザーの購入意図(Intent)をSAが代わりに示すものです。
SAが作成しユーザーへ、「あなたの購入意図はこうだよね?これでMAに依頼するけどいいかな?」といった具合で確認をとり、確認がとれた委任状をMAへ送ります。
「ユーザーの確認」をとって、と述べましたが、ユーザーの関与の仕方(トランザクションの様式)によってユーザーの署名が必要か署名は不要で確認だけすればいいか変わってきます。トランザクションの様式については後ほど説明します。
今回のデモアプリは、シーケンスの細かいところまで割れているHuman Presentというトランザクションの様式で作成しているため、より革新的な技術であるHuman Not Presentでの実装ではないです。申し訳ございません。もう少し仕様がわかってきたらHuman Not Presentも試してみたいと思います。
Cart Mandate
次にCart Mandateです。これは、MAが作成してきた商品が入ったカートを指します。
この商品カートが、「お店が本当に売っていいものなのか、適切な商品カートなのか」をMAからお店側(Merchant)へ署名の形で承認を取ることで購入へと進みます。
Cart Mandateにはお店の署名としてCart MandateのJSONをJWTで署名したBase64文字列を付加します。(詳しくは後ほどデモアプリ解説で説明)
また、Cart MandateはHuman Presentのトランザクション様式では、次のようにユーザーの署名が必要そうな記載があります。
It is generated by the Merchant based on the user's request and is cryptographically signed by the user, typically using a hardware-backed key on their device with in-session authentication. This signature binds the user's identity and authorization to their intent. The Cart Mandate is a structured object containing critical parameters that define the scope of the transaction.
しかし実際にデモアプリを作成すると、Cart Mandateそのものに署名をつけることが難しく(Merchantがつける署名が無効になってしまう)、公式ドキュメントだけだと確実な実装が難しいのが現状です。
ちなみに、公式ドキュメントのCart Mandateのサンプルでは、user_signature_requiredという項目があり、そちらがfalseとなっています。つまり、このサンプルはCart Mandateには、ユーザー署名をつけない想定で書いてありますが、このサンプルはHuman not PresentのCart Mandateを指すので今回は参考にならないです。
もし、Cart Mandateにどうしてもユーザー署名をつけたい場合、仕様に明記はされていませんが以下の2つの実装方法があると考えられます。
- Cart Mandateは不変のまま(
merchant_authorizationを付ける)、ユーザー署名はPaymentMandateのuser_authorizationに入れ、MandateのチェーンによってCart Mandateも確かなユーザーの同意があることを保証する(公式のGitHubはこっちの仕様っぽいです) - Cart Mandate内にdetached signatureとしてuser_authorization partを追加(完全な独自解釈。あくまでもMerchantの署名を壊さず、ユーザー署名を付ける方法)
このあたりは署名スキーマとしてまだ曖昧な部分だと思うので今後に期待しましょう。デモアプリでは1で実装を進めます。
Payment Mandate
最後にPayment Mandateです。これはSAがユーザーに代わって支払いをするうえで必要な委任状で合計金額のほかユーザーの署名と支払いに関する情報が含まれているのが特徴です。
このMandateがあることで、すべての取引についてユーザーが最終合意したことを示すことができます。Payment Mandateにはユーザーの署名を付ける必要があります。
また、Intent Mandate→Cart Mandate→Payment Mandateと処理が進むに連れ、その前のMandateのIDやハッシュを記載するようにしています。(チェーンを作っている)これにより、取引がエージェントによるものであり、どのようなプロセスを経たかというのがMandateを見ることでわかるようになっているのでお店の決済処理システムや決済ネットワークに正しくリスクを含めて伝えることができます。
トランザクションの様式
AP2には主に2つのトランザクションの様式があります。Human Present (人間がその場にいる決済)と、Human Not Present (人間がその場にいない決済)です。ユーザーの関与の仕方が異なります。
Human Present
まず、Human Presentは、ユーザーがAIエージェントにタスクを委任するものの、最終的な支払いを承認するためにその場にいる(利用可能である) シナリオに適用されます。イメージとしては、チャットボットと会話しながら商品を選び、その流れで買い物を完了させるような動きです。
この場合、ユーザーの意思を否認不可で証明するべきはPayment Mandateになります。一方でIntent Mandateへのユーザー署名は省略できます。(ドキュメントを読む限り省略できるはず、です。署名してもいいのですが、署名の要求はパスキーの認証が伴い、毎回ポップアップが出てしまうとUI/UX的にかなり冗長になるのでこのデモアプリでは避けてます。)
Human Not Present
Human Not Presentは、ユーザーがエージェントにタスクを委任し、ユーザーが不在の状況下でエージェントが決済を自律的に実行することを許可するシナリオに適用されます。
例えば、「価格が100ドルを下回ったらこの靴を買う」のようにあらかじめ購入の条件をSAに伝えておいて、SAがその条件になったらユーザー不在のまま購入手続きを進める、というものです。
この場合、ユーザーの意思を否認不可で証明するべきはIntent Mandateになります。なので、Human Not PresentではIntent Mandateにユーザー署名が必須となります。
とはいえ、実はHuman Not Presentの仕様は公式ドキュメントでまだ詳しくでてないので、その後のPayment Mandateはどう扱うのか。もし不都合な購入が進められてしまった場合の紛争解決はどうするのかなどは存じてません。すみません。
では作ったものを見ていこう(デモアプリを探訪する)
ドキュメントから得られる自分のAP2に関する知識は正直このレベルなので、実際に作りながら細かい挙動や署名の仕組み、A2Aで実際にやり取りされる内容、UI/UXなどを深堀りしていこうと思います。
公式ドキュメントではHuman Not Presentのシーケンスがなかったのでそちらへは対応してません。
また、門外漢の自分が適当に作ってしまったところも多く、実運用に耐えられるものか、専門家のレビューを受けていないです。このアプリを使って作ったいかなるサービス、プロダクトもその責任を負いませんのでご注意ください。
構成図
まず、Claudeに書かせた構成図を見てください。

とても複雑ですね...。
むしろDocker ComposeのYAMLファイルを見たほうがイメージしやすいかもしれません。抜粋したものを見てみましょう。
version: "3.8"
services:
# Init Keys - キーペア初期化(起動時に1回実行)
init-keys:
# Init Seeds - シードデータ投入(起動時に1回実行)
init-seeds:
# Shopping Agent - ユーザー向けエージェント
shopping_agent:
# Shopping Agent MCP - MCPツール(LangGraphノード)
shopping_agent_mcp:
# Merchant Agent - 商品検索・CartMandate作成
merchant_agent:
# Merchant Agent MCP - MCPツール(LangGraphノード)
merchant_agent_mcp:
# Merchant - CartMandate署名
merchant:
# Credential Provider 1 - WebAuthn検証・トークン発行
credential_provider:
# Credential Provider 2 - WebAuthn検証・トークン発行(複数CP対応)
credential_provider_2:
# Payment Processor - 決済処理
payment_processor:
# Payment Network - 決済ネットワーク(Agent Token発行)
payment_network:
# Frontend - Next.js
frontend:
# Meilisearch - 全文検索エンジン(商品検索用)
meilisearch:
# Jaeger - 分散トレーシングバックエンド(OpenTelemetry)
jaeger:
# Redis - KVストア(一時データ・セッション管理)
redis:SAとMAがそれぞれいるのと、SAとユーザーを繋ぐフロントエンドがいます。ドキュメントによっては、SAとフロントエンドを直接繋がず間にUA(User Agent)が入るケースもありますがこのデモアプリはSAとフロントエンドを直接接続しています。 また、SA、MAがエージェントとしてMandateを作ったり、商品検索を実施するためのツールが独立したMCPサーバーとしてDocker Composeのサービスになっており、Streamable HTTPで利用可能な形となっています。
さらにCP、MPPそして決済ネットワークのスタブサービスが立ち上がります。
各エンティティは固有のDBを持ち、SQLiteで実装されています。一時的なKVストアが必要なときはRedisにアクセスします。 また、商品検索には全文検索エンジンが適しているためMeilisearchを利用します。
さらに、OpenTelemetryのバックエンドとしてJaegerを導入しました。
すべてのサービスが利用する署名・検証で利用する公開鍵、秘密鍵および、DIDは初期化スクリプトで作成されます。(DIDについては後ほど説明します。)
また、LLMOpsの観点で、Langfuseを導入しています。これによりSAやMAのLangGraphのグラフ構築がだいぶ楽になりました。(ありがとうLangfuse)
事前準備
まずは、ユーザーを作成するところからやりましょう。このデモアプリではメールアドレス・パスワードでユーザーを作成します。主にSAとの通信で利用されるHTTP Sessionとしての認証情報となります。もうすこしわかりやすく言えば、お買い物チャットボットの認証といえるでしょう。

それとは別にパスキーの登録画面が入ります。これは、asstationのために利用するものです。パスキーはSAで管理されるのではなく、CPにて管理されます。ただし今回は簡易的にSAとの通信に使うフロントエンドにて登録する形となっています。(本来は別ドメインの画面から登録するはず。)

さらにクレジットカード情報を登録します。これもSAではなく、CPに登録しているのですがパスキーと同様の理由で同じフロントエンドから登録しています。(なのでSAにはクレジットカード情報は流れていません。SAがクレジットカード情報にアクセスする流れはこのあと出てきます。)

フロントエンドの概要
フロントエンドは3つの画面で構成されます。 /chat /merchant /payment-methodsです。
/chatはユーザーとSAのの間のチャットUIを提供します。今回のデモアプリの中心とも言える画面です。

/payment-methodsはさきほど追加したクレジットカードの編集削除や、追加ができる画面です。CPの管轄なので本来は別のフロントエンドになるはずです。

/merchant は文字通りお店側の管理画面です。商品管理や注文履歴の管理、お店側の署名を手動で行なうなどが可能です。こちらは本来はお店専用のフロントエンドとなるはずです。

早速、/chatからチャットをスタートしましょう!
「こんにちは」と挨拶を打つとスタートします。

SAはLangGraph
突然ですが、SAはLangGraphとDocker Model Runnerで動いています。Docker Model RunnerはローカルLLMを簡単に利用できるDocker Desktopの便利な機能です。Docker Model Runnerを採用した理由は、APIキーの設定なく、Docker Composeで完全に動く環境が手に入る(ローカルLLMなので)のと、育休中で金がないので検証のために気軽にLLMのAPIを叩けないためです。(とても金欠、助けてくれ。)
なので、このデモは無料で試せるのです!その代わりローカルLLM(Qwen3)を使うので全体的に動作はもっさりします。気になる方は、別のモデルを利用していただければと思います。
SAのLangGraphのグラフは次のとおりです。

だいぶ長い+直線的なグラフになっていますが、AP2のシナリオは間々に署名が入り、かつ署名の順番も重要なのでかなり複雑な動作となります。なので、LLMによる自律的な動作をするノードは一部(図の青いノード、ユーザーのIntentを抽出するところ)だけです。固定文言多めの紙芝居グラフということです。
残念ながらエラーの復帰処理などは実装してません。なのでどこかエラーになるとエラーノードに移動し、「こんにちは」と入力させてリセットさせる必要があります。(このあたりのグラフにもこだわりたいですが、そうすると実装時間も足りなくなるので。)
Intent抽出・Intent Mandate骨子作成(collect_intent)
挨拶ノードは説明をスキップしてまず、重要なのはcollet_intentノードです。ユーザーの購入意図を自然言語で受付け、Intent Mandateの骨子を作成する重要なノードです。
購入意図、予算、キーワード、商品カテゴリー、ブランドなどを抽出します。

これは公式ドキュメントのシーケンス7.1 Illustrative Transaction FlowでいうところのStep1〜3に該当します。
本来であれば公式ドキュメントのシーケンスStep3を見ると、Comfirmとなっているので、ユーザーにポップアップなどで確認を求める必要がありそうですが、AP2のお買い物体験はうまく作らないとユーザーへの署名要求で絶えずポップアップがでてしまうので、ここでは、ポップアップは出さず、自然言語で「違うよ。自分のほしいものは〜」とチャットに訂正入力してもらうことを想定して専用のポップアップは避けるようにしてます。(グラフも作り込んでないので訂正入力は実際動かないのですが...。)
デモアプリの動作ログを見ると、LLMを使って、ユーザーのIntentが取り出されていることがわかると思います。
ap2_shopping_agent | [2025-11-01 00:49:37,548] INFO in services.shopping_agent.langgraph_shopping_flow: [route_by_step] Routing decision
ap2_shopping_agent | current_step: ask_intent
ap2_shopping_agent | user_input: かわいいグッズがほしい。5000円以内
ap2_shopping_agent | is_step_up_completion: False
ap2_shopping_agent | [2025-11-01 00:50:12,592] INFO in services.shopping_agent.langgraph_shopping_flow: [collect_intent_node] LLM result: {'intent': 'かわいいグッズを購入したい', 'max_amount': 5000, 'keywords': ['かわいい', 'グッズ', 'おしゃれ']}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:12.593597Z", "level": "INFO", "logger": "agent", "message": "[_create_intent_mandate] Reconstructed intent: かわいいグッズを購入したい。5000円以内", "module": "agent", "function": "_create_intent_mandate", "line": 1733}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:12.593977Z", "level": "INFO", "logger": "agent", "message": "[_build_intent_mandate_from_session] Constructed natural_language_description: かわいいグッズを購入したい。5000円以内", "module": "agent", "function": "_build_intent_mandate_from_session", "line": 1812}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:12.594677Z", "level": "INFO", "logger": "agent", "message": "[ShoppingAgent] IntentMandate created (AP2-compliant): intent='かわいいグッズを購入したい...', expiry=2025-11-01T01:50:12.593363Z", "module": "agent", "function": "_build_intent_mandate_from_session", "line": 1824}配送先の確定(collect_shipping)
AP2で重要なのは、Cart Mandateのやり取りまでに金額を確定させることです。
これは、取引を進めていった最後で、「実は合計金額に加え、配送料が500円かかりました!」と後出しジャンケンされても困ってしまうためです。そこで、配送先の確定はシーケンスの早い段階で実施します。7.1 Illustrative Transaction FlowでいうところのStep5に該当します。
もし、SAがすでに配送先の情報を持っていたら、この処理はSA側で実施できるためシーケンス上はOptionalになっています。

CPの選択(select_cp)
こちらもOptionalにはなりますが、ユーザーはCPを複数の中から選択することができます。イメージとしては、Google Payを使うか、Paypalを使うか、みたいな選択に近いと思われます。(多分)
本来利用可能なCPのリストはあらかじめユーザーが登録しておく必要があるのですが、このデモでは固定で2つのCPを選ぶ形になっています。
CPを早い段階で選ぶのは、公式ドキュメントのシーケンスStep6でCPに対して支払い方法を問い合わせする必要があるためです。CPの選択はシーケンスでいうところのStep4に該当します。
デモアプリの実装としては、次の支払い方法の確認にスムーズに移れるため、7.1 Illustrative Transaction FlowのStep4,5を入れ替えた実装をしています。

支払い方法の確認(get_payment_method)
利用するCPが決定したため、今度は支払い方法を取得していきます。Human Presentのトランザクション様式なので、実際にユーザーに支払い方法を決めてもらうのは、シーケンスの後ろの方ですが、このタイミングで内部的に取得しておく形となります。
配送料と同じ理屈で、関連する手数料、割引、またはロイヤルティ情報が選択する支払い方法によって変わってくる可能性があるため、その支払い方法の候補をCart Mandate作成までにMAに提示する必要があるためです。
デモアプリのログを見ると、あくまでも内部的な動作なので画面には出ませんが選択したCPと通信するログが出力されます。
ap2_shopping_agent | [2025-11-01 00:50:29,045] INFO in services.shopping_agent.langgraph_shopping_flow: [select_cp_node] AP2 Step 4: User selected Credential Provider
ap2_shopping_agent | User ID: usr_ee3014bbc51f4156
ap2_shopping_agent | CP ID: did:ap2:cp:demo_cp
ap2_shopping_agent | CP Name: AP2 Demo Credential Provider
ap2_shopping_agent | [2025-11-01 00:50:29,255] INFO in services.shopping_agent.langgraph_shopping_flow: [get_payment_methods_node] AP2 Step 6-7: Requesting payment methods from CP
ap2_shopping_agent | User ID: usr_ee3014bbc51f4156
ap2_shopping_agent | CP ID: did:ap2:cp:demo_cp
ap2_shopping_agent | CP URL: http://credential_provider:8003
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.257749Z", "level": "INFO", "logger": "agent", "message": "[ShoppingAgent] Requesting payment methods from Credential Provider (http://credential_provider:8003) for user: usr_ee3014bbc51f4156", "module": "agent", "function": "_get_payment_methods_from_cp", "line": 2316}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.261011Z", "level": "INFO", "logger": "agent", "message": "HTTP Request: GET http://credential_provider:8003/payment-methods", "module": "logger", "function": "log_http_request", "line": 182}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.261495Z", "level": "DEBUG", "logger": "agent", "message": "HTTP_REQUEST_RAW: {\"type\": \"HTTP_REQUEST\", \"method\": \"GET\", \"url\": \"http://credential_provider:8003/payment-methods\", \"headers\": {}, \"body\": null}", "module": "logger", "function": "log_http_request", "line": 193}
ap2_credential_provider | {"timestamp": "2025-11-01T00:50:29.569821Z", "level": "INFO", "logger": "provider", "message": "[get_payment_methods] Retrieved 1 payment methods for user: usr_ee3014bbc51f4156", "module": "provider", "function": "get_payment_methods", "line": 631}
ap2_credential_provider | INFO: 172.18.0.9:34006 - "GET /payment-methods?user_id=usr_ee3014bbc51f4156 HTTP/1.1" 200 OK
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.576076Z", "level": "INFO", "logger": "agent", "message": "HTTP Response: 200 (317.37ms)", "module": "logger", "function": "log_http_response", "line": 215}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.576146Z", "level": "DEBUG", "logger": "agent", "message": "HTTP_RESPONSE_RAW: {\"type\": \"HTTP_RESPONSE\", \"status_code\": 200, \"headers\": {\"date\": \"Sat, 01 Nov 2025 00:50:28 GMT\", \"server\": \"uvicorn\", \"content-length\": \"252\", \"content-type\": \"application/json\"}, \"body\": {\"user_id\": \"usr_ee3014bbc51f4156\", \"payment_methods\": [{\"id\": \"pm_e6367cd7\", \"type\": \"basic-card\", \"display_name\": \"Visaカード (****1111)\", \"brand\": \"Visa\", \"last4\": \"1111\", \"requires_step_up\": false, \"billing_address\": {\"country\": \"JP\", \"postal_code\": \"111-1111\"}}]}, \"duration_ms\": 317.3692226409912}", "module": "logger", "function": "log_http_response", "line": 226}
ap2_shopping_agent | {"timestamp": "2025-11-01T00:50:29.576201Z", "level": "INFO", "logger": "agent", "message": "[ShoppingAgent] Retrieved 1 payment methods from Credential Provider", "module": "agent", "function": "_get_payment_methods_from_cp", "line": 2334}
ap2_shopping_agent | [2025-11-01 00:50:29,576] INFO in services.shopping_agent.langgraph_shopping_flow: [get_payment_methods_node] AP2 Step 7: Received 1 payment methods from CP
ap2_shopping_agent | Payment Methods: ['pm_e6367cd7']ポイントとして、CPからのレスポンスにはカードの情報が入っていますが、PCIデータ(カード番号そのものやCVCなど)は含まれていないことがわかります。
ちなみに、SAとCPとの通信はA2Aがいいのか、普通のREST APIがいいのか悩みました。ここでは GET /payment-methods というREST APIで実装してます。
CPをエージェンティックなエンティティと捉えればA2Aが適している気がしますが、CPとはWebAuthnの通信なども多く、どちらかというとエージェンティックなエンティティというよりはEコマースのバックエンドサービスに近いのかなと思ったため。ただし、A2Aにすれば署名やDIDによる相互認証が統一できるメリットも多いのでやっぱりA2Aのほうが良い気もしています。(わからん。)
Intent Mandate送信(fetch_cart)
いよいよIntent MandateをMAに送信します。送信はA2AのMessageで実施されます。公式ドキュメントのシーケンスStep8に該当します。
A2Aで送信されるIntent Mandate+αは次のようなものになります。(A2Aメッセージを送信する際のhttpxのリクエスト時のデバッグログから抽出したもの)
{
"type": "HTTP_REQUEST",
"method": "POST",
"url": "http://merchant_agent:8001/a2a/message",
"headers": {},
"body": {
"header": {
"message_id": "a2d45408-afbc-4891-af66-647e82665f25",
"sender": "did:ap2:agent:shopping_agent",
"recipient": "did:ap2:agent:merchant_agent",
"timestamp": "2025-11-01T00:50:29.785905Z",
"nonce": "cc624e26345bfe79c092580578dbba04a14f46c27d44077b2d89303a35970833",
"schema_version": "0.9",
"proof": {
"algorithm": "ed25519",
"signatureValue": "aMCMUarSscFWY8/j+NKdKflvyzdMjpZHJJqGXuKWsW/XqO0loXpUIfrFFTBzenXXPAajkw7IpQYMWtv4ytyNDg==",
"publicKeyMultibase": "z6MkwMTmaSbsecH3zoTBSAEY2vuMxguAebRnruND2a8oVvcq",
"kid": "did:ap2:agent:shopping_agent#key-2",
"created": "2025-11-01T11:42:36.177985Z",
"proofPurpose": "authentication"
},
"signature": null
},
"dataPart": {
"@type": "ap2.mandates.IntentMandate",
"id": "intent_a97064a3",
"payload": {
"intent_mandate": {
"id": "intent_a97064a3",
"type": "IntentMandate",
"user_id": "usr_ee3014bbc51f4156",
"user_cart_confirmation_required": true,
"natural_language_description": "かわいいグッズを購入したい。5000円以内",
"requires_refundability": false,
"intent_expiry": "2025-11-01T01:50:12.593363Z",
"created_at": "2025-11-01T00:50:12.593363Z"
},
"shipping_address": {
"recipient": "山田太郎",
"postal_code": "123-4567",
"city": "豊島区",
"region": "東京都",
"address_line1": "北大塚1-1-1",
"country": "日本"
}
},
"kind": null,
"artifact": null
}
}
}まずわかりやすくIntent Mandateそのものから見ていきましょう。
{
"id": "intent_a97064a3",
"type": "IntentMandate",
"user_id": "usr_ee3014bbc51f4156",
"user_cart_confirmation_required": true,
"natural_language_description": "かわいいグッズを購入したい。5000円以内",
"requires_refundability": false,
"intent_expiry": "2025-11-01T01:50:12.593363Z",
"created_at": "2025-11-01T00:50:12.593363Z"
},残念ながら、公式ドキュメントそのものにはIntent Mandateの例が載っていないのですが、公式のGitHubにある型定義を見ると仕様が見えてきます。重要な項目について見ていきましょう。
(公式ドキュメント A2A Extension for AP2に記載がありました。確認不足で申し訳ございません。)
user_cart_confirmation_required
こちらは、型定義を見ると、
If false, the agent can make purchases on the user's behalf once all purchase conditions have been satisfied. This must be true if theintent mandate is not signed by the user.
とあるので、Intent Mandateにユーザー署名がない、つまりHuman Presentのトランザクション様式ではtrueである必要があります。
natural_language_description
今回のデモアプリでは、そこまでSAが意図の深堀りをするシナリオになっていないため、「かわいいグッズを購入したい。5000円以内」というユーザーの入力そのものが入っています。
こちらも型定義を見ると、
The natural language description of the user's intent. This is generated by the shopping agent, and confirmed by the user. The goal is to have informed consent by the user.
とあるので、本来はユーザーのテキストそのものが入るのではなく、SAによって解釈された意図が自然言語の形で入ります。また、その意図は送信前に確認を求める必要がある、というのも明記されています。
intent_expiry
Intent Mandateには有効期限があります。これは、時間的な制限を設けることでユーザーの意図が確かに有効であることを保証するためです。人間の意思にも賞味期限がある、ということですね。
今回はIntent Mandate作成後1時間で設定してますが、Human Not Presentの場合はもう少し伸びる気がします。(もしくはユーザーに問い合わせて確定させるのかもしれません。)
shipping_address
Intent Mandate自体ではないですが、CartをMAが金額もドンピシャで作成するためにshipping_addressも連携しています。AP2の思想を考えると同様に支払い情報も連携する必要がありそうですがこのデモでは省略しています。
ここまででIntent Mandateの説明は終わりです。
SAの署名
Intent Mandateそのものとは別に、Headerという項目がありますが、これは、W3C Verifiable Credentialsのproof構造を踏襲したもので、SAのVerifiable Presentation (VP) に相当するものです。
公式ドキュメントで明確な文献がなかったのですが、Paypalのブログによると、
All mandates are expressed as W3C Verifiable Credentials, ensuring tamper resistance, portability, and interoperability across the ecosystem. Mandates embed cryptographically verifiable consent into authorization flows, providing merchants with dispute-grade evidence, issuers with consistent agent-presence signals and consumers with non-repudiable proof of intent.
とあるため、その仕様に準拠するように作成しております。
署名はproof仕様(RFC 8032 Ed25519)に基づき、実施します。
署名のアルゴリズムは事前に作成したSAの秘密鍵を使って、ED25519でDatapartをcanonicalized JSON(RFC8785)にして、それに対して署名をつけるようにします。
署名の検証のために使う公開鍵はpublicKeyMultibase形式で添付しています。値の先頭文字がzであるため、base58btcを使っていることがわかります。
また、署名の再送や差し替えを防止するためにnonceも生成してます。
このように、署名なども含めさまざまな処理が入ってやっとIntent MandateをMAに送ることができるのです。
VPについて
さらっと上では説明してしまいましたが、Verifiable Presentation(VP)がなにかわかりにくいと思います。私も今までわかってなかったです。
まず、VPを語る前にVC(Verifiable Credential)という概念にも触れる必要があります。VCとVPという2つの概念がある、ということを抑えておきましょう。
VCは、「その人・モノがxxですよ!」ということを証明するものになります。
例えば、VCで大学の卒業証明書を扱うとします。大学(発行者)が、卒業生(保持者)に卒業したという証明書を暗号的に発行します。なのでVCがちょうど卒業証明書そのもののイメージです。
VPはこの卒業証明書を本人が提示するときに確かに本人から提示している私に関する証明ですよという証明です。VCに本人の署名をつけることで実現できます。
この仕組みがあれば、19.2秒ほどチラ見せせずとも確かに自分が大学を卒業したことを証明できるわけです。
MAもLangGraph
さて、Intent MandateがMAに送信されましたのでMAが商品カートを作る作業に移ります。
MAもLangGraph + Docker Model Runnerで構築されています。このグラフはSAからのIntent Mandateを受け取るところからスタートします。

Graphの開始前に署名の検証
LangGraphのグラフには表現されませんが、(デモではA2Aのメッセージハンドラー上で実施しているため)受け取ったA2Aメッセージが本当にSAから送信しているか検証する必要があります。
検証のステップは先ほどの署名で実施したことの逆を基本的にやればいいです。詳しく見ていきましょう。
リプレイ攻撃の対策
まず、proof構造が正しいかのチェック(Validation)をしたあと、Timestamp検証を実施します。仕組みは単純で現在時刻から300秒=5分以内に作成されたproofかを確認します。リプレイ攻撃を避けるための基本的な対策になります。
また、Nonce検証も実施します。自身のKVストアを確認し、直近でNonceが同じものがないか、単位時間(ここでは300秒=5分)以内に同じNonceを利用していないこと、つまりA2A通信の再利用がないかを確認します。
公開鍵の取得
次に、proof構造からメッセージの署名を検証していきます。
まず、署名の検証のためにはSAの公開鍵が必要になります。こちらの公開鍵を取得する方法として、AP2ではDID(Decentralized Identifier/分散型識別子)を使う方法が推奨されています。
デモアプリではDIDに加え、publicKeyMultibaseを使う方法も合わせてサポートしています。DIDについて馴染みがあまりないと思いますので詳しく見ていきましょう。
そもそもDIDとは?
DID(Decentralized Identifier/分散型識別子)は文字通り、分散型(中央集権的な仕組みでない)のエンティティ識別方法です。
署名の検証はproof構造を持ったA2A通信であれば、publicKeyMultibaseに保存されているため、送られたメッセージが途中で改ざんされずに通信を完了したことは判断できます。
しかし欠点として、署名だけでは誰の鍵で署名したかがわからないという問題があります。
おそらく送信先からSAということはわかるのですが、SAがどんなエージェントなのか、詳しいことはわかりません。DIDを使って“信頼の文脈”を作ることができます。
DIDではdid.jsonというJSONファイルを各エンティティの.well-known/did.jsonからアクセスできるようにすることから始めます。例えばSAのdid.jsonは次のとおりです。
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1",
"https://w3id.org/security/suites/ed25519-2020/v1"
],
"id": "did:ap2:agent:shopping_agent",
"verificationMethod": [
{
"id": "did:ap2:agent:shopping_agent#key-1",
"type": "EcdsaSecp256r1VerificationKey2019",
"controller": "did:ap2:agent:shopping_agent",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPlqNMbMKh/8HoX2356uZmKM2lVuB\nY71rBhcg1lpuUBncM7LmNAEJO/9WcKboqL+KHKpwGCIEr/oWsizgd89hvA==\n-----END PUBLIC KEY-----\n",
"publicKeyMultibase": "z2oAtKWnMsubf5MPr6XqWVuLeXVipQ84i4jj2VV9Vu5EZjtQ8"
},
{
"id": "did:ap2:agent:shopping_agent#key-2",
"type": "Ed25519VerificationKey2020",
"controller": "did:ap2:agent:shopping_agent",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAkr3srUb1CmKJq6G0h0PXPnOUtJrTQKL/a8u0J3Ob1wk=\n-----END PUBLIC KEY-----\n",
"publicKeyMultibase": "z6MkpL5YFLHxAcp6LSJboXQ3nBnNGrQ4TiZRmWBZamPo7t8x"
}
],
"authentication": [
"did:ap2:agent:shopping_agent#key-1",
"did:ap2:agent:shopping_agent#key-2"
],
"assertionMethod": [
"did:ap2:agent:shopping_agent#key-1",
"did:ap2:agent:shopping_agent#key-2"
],
"created": "2025-11-02T00:16:30.663059Z",
"updated": "2025-11-02T00:16:30.663091Z",
"service": [
{
"id": "did:ap2:agent:shopping_agent#a2aendpoint",
"type": "A2AEndpoint",
"serviceEndpoint": "http://shopping_agent:8000/a2a",
"name": "Shopping Agent A2A Endpoint",
"description": "A2A通信エンドポイント(ユーザー購買代理エージェント)"
}
]
}did.jsonから公開鍵を取得することで、メッセージの署名主が確実にSAであることを示すことができるわけです。ちなみに、AP2におけるDIDのリゾルバーの標準仕様はよくわからず、今回はDIDのID did:ap2:agent:shopping_agent からDocker Network上のホスト名shopping_agentを抽出して、.well-known/did.jsonにアクセスする仕組みを取ってます。(DNSと併用するイメージ)
publicKeyMultibase
おそらくAP2的にはあまり推奨されないのですが、proof構造に含まれるpublicKeyMultibaseを使った署名の検証もデモアプリではサポートしています。一応、DIDを優先して実施するようにはしてますし、DIDが前提のアーキテクチャの場合、proof構造にpublicKeyMultibaseを入れることは不要なのかもしれません。
検証
いよいよ検証に入ります。ED25519でDataPartをcanonicalized JSON(RFC8785)で署名されたことはわかっていますので、公開鍵を使って検証を実施します。
無事に検証が完了したらSAから確かに送られてきたMandateとわかるので次の処理に移ります。
Intent MandateからIntentを抽出(analyze_intent)
受け取ったIntent Mandateのnatural_language_descriptionからキーワードや価格などを抽出してDBに検索できる情報に落とし込む処理をLLMの力を使って実施してます。
このノードでは次のようなプロンプトが動きます。
=========System Prompt==========
あなたはMerchant Agentのインテント分析エキスパートです。
ユーザーのIntentMandate(購入意図)を解析し、以下の情報を抽出してください:
primary_need: ユーザーの主な要求(1文で簡潔に、日本語)
budget_strategy: 予算戦略("low"=最安値優先、"balanced"=バランス型、"premium"=高品質優先)
key_factors: 重視する要素のリスト(例: ["品質", "価格", "ブランド", "デザイン"])
search_keywords: 商品検索用のキーワードリスト(日本語、3-5個、商品名に含まれそうな単語)
重要:
search_keywordsは必ず日本語で返してください(例: ["かわいい", "グッズ", "Tシャツ"])
商品データベースは日本語の商品名(例: "むぎぼーTシャツ", "むぎぼーマグカップ")なので、日本語キーワードが必須です
必ずJSON形式で返答してください。
=========User Prompt===========
以下のIntentMandateを分析してください:
自然言語説明: かわいいグッズを購入したい。5000円以内
制約条件: {}
JSON形式で返答してください(search_keywordsは必ず日本語):
{
"primary_need": "...",
"budget_strategy": "low/balanced/premium",
"key_factors": ["...", "..."],
"search_keywords": ["...", "...", "..."]
}今回は育休中でお金がないので、ローカルLLMを使っているためそこまでIntentを正しく抽出できていない気がしますが、きっと最新モデルだともう少し高度な抽出ができると思います。LLMからのレスポンスは次のようなJSONとなります。
{
"role": "assistant",
"content": {
"primary_need": "かわいいグッズを5000円以内で購入したい",
"budget_strategy": "low",
"key_factors": [
"価格",
"デザイン"
],
"search_keywords": [
"かわいい",
"グッズ",
"小物"
]
},
"additional_kwargs": {
"refusal": null
}
}MCPサーバー経由で商品検索DBへ検索(search_products)
AP2はA2A、MCPと対立する技術ではなく、拡張するもの、ということが公式ドキュメントでも強調されています。そこで、今回は商品検索のツール利用はMCPサーバーを利用することにしました。(Streamable HTTP)
商品検索そのものは全文検索エンジンを使いたかったので軽量なMelisearchを使っています。
MPCサーバーとのInitialize処理などのシーケンス解説は省略しますが、 MAから メソッドtools/callが走るとで次のような検索要求リクエストをMAが使うMCPサーバー(merchant_agent_mcp)へ飛ばしていることがわかります。
{
"type": "HTTP_REQUEST",
"method": "POST",
"url": "http://merchant_agent_mcp:8011/",
"headers": {
"Content-Type": "application/json",
"Mcp-Session-Id": "2ade50e5-f2ae-439c-becb-0ba97bd1a161"
},
"body": {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search_products",
"arguments": {
"keywords": [
"かわいい",
"グッズ",
"安価"
],
"limit": 20
}
},
"id": 387151
}
}結果は次のように返ってきます。商品検索DBの商品IDはこのあと接続するRDB(SQLite)にある商品IDと一致しています。なので、このIDを用いて在庫チェックなどを行ないます。
{
"type": "HTTP_RESPONSE",
"status_code": 200,
"headers": {
"date": "Sun, 02 Nov 2025 00:21:39 GMT",
"server": "uvicorn",
"content-length": "3086",
"content-type": "application/json"
},
"body": {
"jsonrpc": "2.0",
"id": 387151,
"result": {
"content": [
{
"type": "text",
"text": "{\"products\": [{\"id\": \"286acdd4-d1c1-4860-b06a-f87d2f916a8d\",\"sku\": \"MUGI-KEYCHAIN-001\",\"name\": \"むぎぼーアクリルキーホルダー\",\"description\": \"かわいいむぎぼーのアクリルキーホルダー。バッグやポーチに付けて持ち歩けます。\",\"price_cents\": 80000,\"price_jpy\": 800.0,\"inventory_count\": 100,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30},{\"id\": \"9f58d67c-5c45-4cd4-bf10-73f06647c234\",\"sku\": \"MUGI-CLOCK-001\",\"name\": \"むぎぼー時計\",\"description\": \"むぎぼーデザインのかわいい壁掛け時計。お部屋を明るく彩ります。\",\"price_cents\": 350000,\"price_jpy\": 3500.0,\"inventory_count\": 30,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30},{\"id\": \"2faf8370-ada4-45d4-812c-ef5818d526b5\",\"sku\": \"MUGI-POUCH-001\",\"name\": \"むぎぼーポーチ\",\"description\": \"むぎぼー柄のかわいいポーチ。小物入れやペンケースとして使えます。\",\"price_cents\": 95000,\"price_jpy\": 950.0,\"inventory_count\": 120,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30},{\"id\": \"1d9f08d9-51a9-491e-810b-f0e225ef4f59\",\"sku\": \"MUGI-MUG-001\",\"name\": \"むぎぼーマグカップ\",\"description\": \"むぎぼーがプリントされたかわいいマグカップ。毎日のティータイムが楽しくなります。\",\"price_cents\": 120000,\"price_jpy\": 1200.0,\"inventory_count\": 80,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30},{\"id\": \"c688d6ef-615f-43f7-87ce-05568ae4e63c\",\"sku\": \"MUGI-SOCKS-001\",\"name\": \"むぎぼー靴下\",\"description\": \"むぎぼーがワンポイントで入ったかわいい靴下。やわらかい履き心地。\",\"price_cents\": 85000,\"price_jpy\": 850.0,\"inventory_count\": 100,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30},{\"id\": \"6a169d3a-ca5a-4575-a08b-3fb659c628ed\",\"sku\": \"MUGI-PLATE-001\",\"name\": \"むぎぼープレート皿\",\"description\": \"むぎぼーが中央に描かれた陶器プレート。食卓をかわいく演出。\",\"price_cents\": 190000,\"price_jpy\": 1900.0,\"inventory_count\": 70,\"category\": null,\"brand\": null,\"image_url\": null,\"refund_period_days\": 30}]}"
}
],
"isError": false
}
},
"duration_ms": 1208.6033821105957
}在庫チェック(check_inventory)
商品が検索できたらその商品の在庫状況をまたMCPサーバー経由で問い合わせます。こちらはRDB(SQLite)への問い合わせですが、search_productsノードと同様MPCサーバーとの通信は、Steamable HTTPでやり取りします。次のような各商品の在庫状況が取得できました。
{
"3446cca8-fe68-4354-a518-63eb3e47d27f": 100,
"1530a7db-6b7a-458e-b7b9-f510f6fdaa89": 30,
"cf08568b-8115-461c-aa7e-5a2e07bf4476": 120,
"e534386f-c89b-4548-9734-78bd34958f88": 80,
"eb0e5de8-679d-4445-8e64-a1d4d40097fd": 100,
"f0f41c9f-a31d-4a88-a180-96149a9057fc": 70
}在庫はすべての商品問題なさそうですね。
カート候補作成(optimize_cart)
いよいよカートの作成を実施します。ローカルLLMに3つのカートプランを作成してもらうoptimize_cartノードへ処理が移ります。
optimize_cartでは、下記のようなプロンプトが走ります。
========System Prompt=========
あなたはMerchant Agentのカート最適化エキスパートです。
ユーザーの購入意図と商品リストから、最適なカートプラン3つを提案してください。
各プランには以下を含めてください:
name: プラン名(予算や特徴を含む、例: "予算内プラン (5,000円)")
description: プランの説明(1-2文)
items: 商品リスト [{"product_id": 123, "quantity": 1}, ...]
プラン設計のガイドライン:
プラン1: 予算内で最もコスパが良いプラン
プラン2: 予算を少し超えても高品質なプラン
プラン3: シンプルに1-2商品のみのプラン
必ずJSON配列形式で返答してください。
=======User Prompt============
以下の条件でカートプランを3つ提案してください:
ユーザーの要求: かわいいグッズを5000円以内で購入したい
予算戦略: low
重視要素: 価格, デザイン
予算上限: 指定なし
商品リスト(6件):
[
{
"id": "3446cca8-fe68-4354-a518-63eb3e47d27f",
"name": "むぎぼーアクリルキーホルダー",
"price_jpy": 800.0,
"category": null,
"inventory": 100
},
{
"id": "1530a7db-6b7a-458e-b7b9-f510f6fdaa89",
"name": "むぎぼー時計",
"price_jpy": 3500.0,
"category": null,
"inventory": 30
},
{
"id": "cf08568b-8115-461c-aa7e-5a2e07bf4476",
"name": "むぎぼーポーチ",
"price_jpy": 950.0,
"category": null,
"inventory": 120
},
{
"id": "e534386f-c89b-4548-9734-78bd34958f88",
"name": "むぎぼーマグカップ",
"price_jpy": 1200.0,
"category": null,
"inventory": 80
},
{
"id": "eb0e5de8-679d-4445-8e64-a1d4d40097fd",
"name": "むぎぼー靴下",
"price_jpy": 850.0,
"category": null,
"inventory": 100
},
{
"id": "f0f41c9f-a31d-4a88-a180-96149a9057fc",
"name": "むぎぼープレート皿",
"price_jpy": 1900.0,
"category": null,
"inventory": 70
}
]
JSON配列形式で返答してください:
[
{
"name": "プラン名(価格含む)",
"description": "プラン説明",
"items": [{"product_id": 123, "quantity": 1}]
},
...
]残念ながらローカルLLMの性能(Qwen3:8b)だと、的はずれなプランを作成したり(時計ばっか詰め込んだ謎カートとか)、JSONが不完全なこともあるのですが....。
うまくいくと次のようなJSONが出力されます。
[
{
"name": "コスパ最強プラン (1,650円)",
"description": "価格重視で最安値のキーホルダーと靴下を組み合わせたプラン。かわいさと予算を両立させます。",
"items": [
{
"product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
"quantity": 1
},
{
"product_id": "c688d6ef-615f-43f7-87ce-05568ae4e63c",
"quantity": 1
}
]
},
{
"name": "高品質プラン (2,150円)",
"description": "かわいさを重視してポーチとマグカップを組み合わせたプラン。品質とデザインのバランスが良いです。",
"items": [
{
"product_id": "2faf8370-ada4-45d4-812c-ef5818d526b5",
"quantity": 1
},
{
"product_id": "1d9f08d9-51a9-491e-810b-f0e225ef4f59",
"quantity": 1
}
]
},
{
"name": "シンプルプラン (800円)",
"description": "予算を抑えたい方におすすめのキーホルダー単品プラン。かわいらしさを最大限に活かした1点です。",
"items": [
{
"product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
"quantity": 1
}
]
}
]この時点ではカート候補(Cart Candidate)を作成しているだけなので、正式なCart Mandate形式ではないです。お店側(Merchant)の署名はついていません。
Cart Mandate作成・お店の署名をつける(build_cart_mandates)
作成されたカート候補を1つ1つCart Mandateの形式に整形します。この動きもMCPサーバーで実施しています。
また、作成されたCart Mandateにお店側(Merchant)の署名を付けてもらいます。
こちらの処理はカート3つに対し並列で処理が走るため、ログをそのまま貼り付けて解説すると、たいへんわかりにくいです。
なので、シーケンス図にしてみました。(ログをClaudeに貼り付けるだけでシーケンス図ができるのは便利ですねぇ...。)

作成されたCart Mandateの1つがこんな感じです。
{
"signed_cart_mandate": {
"contents": {
"id": "cart_70dda49a",
"user_cart_confirmation_required": true,
"payment_request": {
"method_data": [],
"details": {
"id": "cart_70dda49a",
"display_items": [
{
"label": "むぎぼーアクリルキーホルダー",
"amount": {
"value": 800.0,
"currency": "JPY"
},
"refund_period": 2592000
},
{
"label": "消費税(10%)",
"amount": {
"value": 80.0,
"currency": "JPY"
},
"refund_period": 0
},
{
"label": "送料",
"amount": {
"value": 500.0,
"currency": "JPY"
},
"refund_period": 0
}
],
"total": {
"label": "合計",
"amount": {
"value": 1380.0,
"currency": "JPY"
}
}
},
"shipping_address": null
},
"cart_expiry": "2025-11-02T01:23:03.698740Z",
"merchant_name": "むぎぼーショップ"
},
"merchant_authorization": "eyJhbGciOiJ.....",
"_metadata": {
"intent_mandate_id": null,
"merchant_id": "did:ap2:merchant:mugibo_merchant",
"created_at": "2025-11-02T00:23:03.698753Z",
"cart_name": "シンプルプラン (800円)",
"cart_description": "予算を抑えたい方におすすめのキーホルダー単品プラン。かわいらしさを最大限に活かした1点です。",
"raw_items": [
{
"product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
"name": "むぎぼーアクリルキーホルダー",
"description": "かわいいむぎぼーのアクリルキーホルダー。バッグやポーチに付けて持ち歩けます。",
"quantity": 1,
"unit_price": {
"value": 800.0,
"currency": "JPY"
},
"total_price": {
"value": 800.0,
"currency": "JPY"
},
"image_url": null
}
]
}
}
}商品情報のほかに、merchant_authorizationという項目にBase64文字列が入っていることがわかります。詳しく見ていきましょう。
Merchant Authorization
お店側の署名の付け方が全然わからなかったのですが、公式GitHubの実装例を確認するとおおよそ答えがわかりました。
A base64url-encoded JSON Web Token (JWT) that digitally
signs the cart contents, guaranteeing its authenticity and integrity:
Header includes the signing algorithm and key ID.
Payload includes:
iss, sub, aud: Identifiers for the merchant (issuer) and the intended recipient (audience), like a payment processor.
iat: iat, exp: Timestamps for the token's creation and its short-lived expiration (e.g., 5-15 minutes) to enhance security.
jti: Unique identifier for the JWT to prevent replay attacks.
cart_hash: A secure hash of the CartMandate, ensuring integrity. The hash is computed over the canonical JSON representation of the CartContents object.
- Signature: A digital signature created with the merchant's private key. It allows anyone with the public key to verify the token's authenticity and confirm that the payload has not been tampered with.
The entire JWT is base64url encoded to ensure safe transmission.
つまり、cart_hashを含めたJWTをBase64エンコードすればOKとのことです。なので、MAから送信されたされたカートのハッシュを含んだJWTを作成し、署名をつけたものをお店側(Merchant)のエンティティから返しています。なので、merchant_authorizationをhttps://www.jwt.io/などで検証すると次のように確かにカートのハッシュが入った有効なJWTとなっているはずです。(検証はお店の公開鍵を利用)

このデモアプリでは、お店の署名は自動で行ってます。多くのAP2商取引パターンでも、機械的なチェックをプログラムで実施し、自動で進めるはずです。(わざわざお店の人が介在するのもレスポンスを下げる原因なので)
ただ、デモアプリではわかりやすいように、フロントエンドから /merchant ダッシュボードにアクセスし、手動署名モードに切り替えることで、お店側のカート承認を体験できます。

この流れは7.1 Illustrative Transaction FlowのStep10-11に該当します。
カートランキング付け(rank_and_select)
デモアプリでは実装をスキップしてますが、選定されたカートのランキングを入れ替えたり(リランク)してユーザーのお買い物体験をよくします。やっぱり最初にいい商品が出てきたほうが購買意欲は高くなりますからね。
例えば、
- ユーザー嗜好マッチ度
- 在庫確実性
- 価格競争力
などでカートの順番を入れ替えることが想定されます。
A2Aメッセージ送信
ap2.responses.CartCandidates
これでCart Mandateが出来上がったのでSAにA2A Messgaeで送信します。
このデモではカート候補が複数あるため、A2A MessageのDataPartにそれぞれのCart Mandate格納するようにしています。
(これがAP2の仕様的に正しいのかわからないものの、A2A Messageとして一度に複数候補送るにはいいかなと...。)
user_cart_confirmation_required
Intent Mandate同様、user_cart_confirmation_requiredをtrueにしているので、ユーザーにカートの中身を確認してもらう、つまりHuman Presentのトランザクション様式で動くことが示されています。
また、特徴的なのは、Cartの商品については、W3CのPayment Requestに準拠する旨が公式GitHubに記載されています。W3CのPayment Request仕様に従ってPaymentMethodDataやPaymentOptionsも含めてます。
_metadata
ここで問題になるのは、Cart Mandate自体には、商品そのもので載せられる情報に限りがあるという点です。商品に対して、label、amount、pending、refund_periodの4項目しか定義できません。
Cart Mandateとしては十分なのかもしれませんが、実際のUI/UXを考えると、例えば商品の説明だったり、商品の画像などをユーザーに見せながら購買させたいものです。
よって、Mandateそのものではなく、metadataに記載するようにしています。
cart_expiry
また、Intent Mandate同様、Cart Mandateにも有効期限をつけています。これによりユーザーのCart Mandate確認が遅れ、時間が経ちすぎて在庫切れになってしまった、ということを予防できます。
署名をつけて完成
これらのDataPartを持ったA2A Messageに、MAの署名をproof構造でつけてCart Mandate完成です。公式ドキュメントIllustrative Transaction FlowのStep12に該当します。
httpxのデバッグログをみるとたしかにCart Mandateが送信されたことがわかります。
{
"type": "HTTP_RESPONSE",
"status_code": 200,
"headers": {
"date": "Sun, 02 Nov 2025 11:11:45 GMT",
"server": "uvicorn",
"content-length": "10892",
"content-type": "application/json"
},
"body": {
"header": {
"message_id": "b9db77f4-a7bb-4ca3-bebb-286a29aea4af",
"sender": "did:ap2:agent:merchant_agent",
"recipient": "did:ap2:agent:shopping_agent",
"timestamp": "2025-11-02T11:14:19.459063Z",
"nonce": "d1fb8412e81e1b61a600b72214b114e27f2939169bb6a9650e4dd83093a893f8",
"schema_version": "0.2",
"proof": {
"algorithm": "ed25519",
"signatureValue": "AHTY0v7VQVALn2H8gsnbRyEot0on4QIcRDBIpDeJeBHm13WpnVauVToqyTey+C6p0/syMBq5y0y/UC8Zotj1Ag==",
"publicKeyMultibase": "z6Mko44YAnz8G71TocDDKoBqg86BJTxnqLN86UvGcpjxP47t",
"kid": "did:ap2:agent:merchant_agent#key-2",
"created": "2025-11-02T11:14:19.459063Z",
"proofPurpose": "authentication"
}
},
"dataPart": {
"@type": "ap2.responses.CartCandidates",
"id": "63ff3cf2-b28c-4a0b-9458-9fa7110cf77c",
"payload": {
"intent_mandate_id": "intent_21dfc414",
"cart_candidates": [
{
"artifactId": "artifact_b66852e4",
"name": "コスパ最適プラン (4,500円)",
"parts": [
{
"kind": "data",
"data": {
"ap2.mandates.CartMandate": {
"contents": {
"id": "cart_2d6be3f2",
"user_cart_confirmation_required": true,
"payment_request": {
"method_data": [
{
"supported_methods": "basic-card",
"data": {
"supportedNetworks": [
"visa",
"mastercard",
"jcb",
"amex"
],
"supportedTypes": [
"credit",
"debit"
]
}
},
{
"supported_methods": "https://a2a-protocol.org/payment-methods/ap2-payment",
"data": {
"version": "0.2",
"processor": "did:ap2:agent:payment_processor",
"supportedMethods": [
"credential-based",
"attestation-based"
]
}
}
],
"details": {
"id": "cart_2d6be3f2",
"display_items": [
{
"label": "むぎぼーアクリルキーホルダー",
"amount": {
"value": 800.0,
"currency": "JPY"
},
"refund_period": 2592000
},
{
"label": "むぎぼーポーチ",
"amount": {
"value": 950.0,
"currency": "JPY"
},
"refund_period": 2592000
},
{
"label": "むぎぼー靴下",
"amount": {
"value": 850.0,
"currency": "JPY"
},
"refund_period": 2592000
},
{
"label": "むぎぼーマグカップ",
"amount": {
"value": 1200.0,
"currency": "JPY"
},
"refund_period": 2592000
},
{
"label": "消費税(10%)",
"amount": {
"value": 380.0,
"currency": "JPY"
},
"refund_period": 0
},
{
"label": "送料",
"amount": {
"value": 500.0,
"currency": "JPY"
},
"refund_period": 0
}
],
"total": {
"label": "合計",
"amount": {
"value": 4680.0,
"currency": "JPY"
}
}
},
"options": {
"request_payer_name": true,
"request_payer_email": true,
"request_payer_phone": false,
"request_shipping": true,
"shipping_type": "shipping"
},
"shipping_address": {
"postal_code": "111-2222",
"recipient": "山田太郎",
"city": "豊島区",
"region": "東京",
"address_line1": "北大塚1-1-1",
"country": "日本"
}
},
"cart_expiry": "2025-11-02T12:14:19.255994Z",
"merchant_name": "むぎぼーショップ"
},
"merchant_authorization": "eyJhbGx......",
"_metadata": {
"intent_mandate_id": "intent_21dfc414",
"merchant_id": "did:ap2:merchant:mugibo_merchant",
"created_at": "2025-11-02T11:14:19.256092Z",
"cart_name": "コスパ最適プラン (4,500円)",
"cart_description": "かわいいアイテムを最大限に詰め込んだ予算内プラン。キーホルダー、ポーチ、靴下、マグカップの4点で総額4,500円。",
"raw_items": [
{
"product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
"name": "むぎぼーアクリルキーホルダー",
"description": "かわいいむぎぼーのアクリルキーホルダー。バッグやポーチに付けて持ち歩けます。",
"quantity": 1,
"unit_price": {
"value": 800.0,
"currency": "JPY"
},
"total_price": {
"value": 800.0,
"currency": "JPY"
},
"image_url": null
},
{
"product_id": "2faf8370-ada4-45d4-812c-ef5818d526b5",
"name": "むぎぼーポーチ",
"description": "むぎぼー柄のかわいいポーチ。小物入れやペンケースとして使えます。",
"quantity": 1,
"unit_price": {
"value": 950.0,
"currency": "JPY"
},
"total_price": {
"value": 950.0,
"currency": "JPY"
},
"image_url": null
},
{
"product_id": "c688d6ef-615f-43f7-87ce-05568ae4e63c",
"name": "むぎぼー靴下",
"description": "むぎぼーがワンポイントで入ったかわいい靴下。やわらかい履き心地。",
"quantity": 1,
"unit_price": {
"value": 850.0,
"currency": "JPY"
},
"total_price": {
"value": 850.0,
"currency": "JPY"
},
"image_url": null
},
{
"product_id": "1d9f08d9-51a9-491e-810b-f0e225ef4f59",
"name": "むぎぼーマグカップ",
"description": "むぎぼーがプリントされたかわいいマグカップ。毎日のティータイムが楽しくなります。",
"quantity": 1,
"unit_price": {
"value": 1200.0,
"currency": "JPY"
},
"total_price": {
"value": 1200.0,
"currency": "JPY"
},
"image_url": null
}
]
}
}
}
}
]
},
{
"artifactId": "artifact_3ec2cecd",
"name": "高品質プラン (5,300円)",
"parts": [...]
},
{
"artifactId": "artifact_4ea2a38f",
"name": "シンプルプラン (3,500円)",
"parts": [...]
}
],
"merchant_id": "did:ap2:merchant:mugibo_merchant",
"merchant_name": "むぎぼーショップ"
},
"kind": null,
"artifact": null
}
}
}SAに戻って続きの処理を実施
MAからCart Mandateを受け取ったら今度は、SAとユーザー側でカートを確定させ支払い処理に進みます。
MAから確かに送信されたA2A Messageということを保証するために、proof構造から署名の検証を実施します。(やり方は前回説明したものと全く同じなので説明は割愛します。)
A2A Messageを無事受け取ると、LangGraphのノード続き(select_cart)から処理が再開されます。
カート選択(select_cart)
カートの選択をユーザーにさせるノードに入りました。
まず、お店側(Merchant)のJWTを検証します。これでお店側も確かに認めた商品カートと確認できました。
ap2_shopping_agent | [2025-11-02 22:41:15,996] INFO in services.shopping_agent.langgraph_shopping_flow: [select_cart_node] Merchant authorization JWT verified: merchant=did:ap2:merchant:mugibo_merchant, cart_hash=0049ff8c19257bed...select_cartノードでは、送られてきたCart Mandateの候補(Cart Candidates)からユーザーにカートの選択をさせるノードとなります。
フロントエンドには、カルーセルの形で商品カートが表示されます。

ユーザーがカートを選択したら、次のノードに進みます。
カート署名(submit_signature)でも実際には署名しない
いよいよCart Mandateの選択と署名確認を実施していきます。
Cart Mandateのuser_cart_confirmation_requiredがtrueだったのでユーザーへの確認は必要です。
この確認という作業にTrusted Device Surfaceを利用する点がAP2の特徴とも言えます。
Trusted Device Surfaceとは?
Trusted Device Surfaceとは、日本語直訳だと信頼できるデバイス面、もう少し表現をわかりやすくすると人間が確実に操作しているデバイス面という意味の言葉です。
このあたりは正直学習不足なので、私の用語の利用に乱れがありそうで怖いですが、信頼できる鍵素材(TPM/Secure Enclave内の秘密鍵)と人間の操作(生体認証など)を結びつけたデバイス面を指します。
今回はCPとユーザーの間登録されたパスキーを利用することでTrusted Device Surfaceを実現しています。
(パスキーであれば、生体認証も絡みますし...。でも本当によいのかわからん...。ちゃんとこのあたり勉強しないと....。)
カートにはユーザーは署名しない
ここは私も実装するまで気が付かなかったのですが、7.1 Illustrative Transaction FlowのStep20が、
Redirect to trusted device surface { PaymentMandate, CartMandate }
なっておりCart Mandateにもユーザー署名をつけるものと思ってましたが、これは誤りでした。
まず、公式GitHubのCart Mandate定義には、merchant_authorization項目はあるものの、user_authorization項目は存在しません。
また、せっかくお店側(Merchant)が署名したJWTにユーザー署名をあとから付けると、JSON構造が変わり、当然ハッシュの値も変わってしまうことでお店側の署名が、無効なものとなってしまいます。
よーく考えれば当たり前ですが、シーケンス読むだけではこのあたりに気が付けないので、やはり実装してなんぼなんだと思いました。
ただし、ユーザーによるTrusted Device Surfaceを用いた、Cart Mandateの確かな確認は必要なので、カート選択時にパスキーでの認証を要求し、認証完了することで確実にそのユーザーが確認したことをSAに示します。

(キャプチャではカートの署名となっていますが、正しくはカートの確認です。お詫びして訂正します。)
パスキー(WebAuthn)認証はCPで検証される
パスキーでの認証は、SAではなく、CPで実施します。

この流れもログを追うだけだとかなり複雑なので SAに定義している/cart/submit-signatureエンドポイントの動きをそのままシーケンス図にしてみましょう。(このあたりはAP2の実装というよりは、AP2を絡めたAIエージェントの作り方の実践となりますので不要な人は読み飛ばしてもらって大丈夫です。)
![https://i.imgur.com/NRw8CwW.png]
カート選択後、このデモアプリではSAから特殊なSSEイベントsignature_requestを送信します。

Cart Mandateをユーザーにポップアップで確認させた後、パスキーで認証ボタンを押すとフロントエンドからブラウザにWebAuthnを要求します。
すると、パスキーで認証のポップアップが(ここでは1Password)でてきます。

SAからCPまで通信し、パスキーでの認証が完了すると、認証結果とともに、次のようなWebAuthn attestationが受け取れます。
{
"id": "dCDQdGlqx50G-UFWp0ORsF5Y7mzWSAkYow",
"rawId": "dCDQdGlqx50G-UFWp0ORsF5Y7mzWSAkYow",
"response": {
"authenticatorData": "xxxxx",
"clientDataJSON": "xxxxxx",
"signature": "xxxxxx",
"userHandle": "usr_cdb4ec851bcf4f73"
},
"type": "public-key",
"attestation_type": "passkey",
"challenge": "xxxxxx"
}認証結果がOKであれば、次のステップ(Peyment Mandate作成)に進むことができます。また、受け取ったWebAuthn attestationはPeyment Mandateの作成時に利用します。
公式ドキュメント Illustrative Transaction FlowのStep20の一部(Cart Mandate)だけ実施した形です。
支払い方法選択(payment_method_select)
公式ドキュメント Illustrative Transaction Flowとは、順番が前後しますが、CPから取得していた支払い方法を確定させます。この画面はStep16の動きです。

これで、完全に支払い方法が確定(今回はVISAカードを使う)しました。
支払い方法のトークン化
支払い方法が確定したので確定した支払い方法に対応する支払いトークン発行をCPに依頼します。公式ドキュメント Illustrative Transaction FlowのStep17に該当します。
{
"type": "HTTP_REQUEST",
"method": "POST",
"url": "http://credential_provider:8003/payment-methods/tokenize",
"headers": {},
"body": {
"user_id": "usr_cdb4ec851bcf4f73",
"payment_method_id": "pm_f4745ec2"
}
}CPから支払い方法に紐づいた支払いトークンが返却されます。Payment Mandateにはこのトークンが書き込まれ、以降の決済処理はこのトークンを利用して進められます。
{
"type": "HTTP_RESPONSE",
"status_code": 200,
"headers": {
"date": "Sun, 02 Nov 2025 23:12:26 GMT",
"server": "uvicorn",
"content-length": "176",
"content-type": "application/json"
},
"body": {
"token": "tok_6c23798f_zfkGVOEF586Lxj9YllxtCFsH",
"payment_method_id": "pm_f4745ec2",
"brand": "Visa",
"last4": "1111",
"type": "basic-card",
"expires_at": "2025-11-02T23:27:26.645569Z"
},
"duration_ms": 36.832332611083984
}ちなみに、 basic-cardという支払いタイプはW3C PaymentRequest APIで規定されていましたが、すでに廃止が決まっているメソッドです。AP2であれば、https://a2a-protocol.org/payment-methods/ap2-paymentを使うのがよさそうです。(まだ仕様がわかりませんが...。)
支払いトークンと実際の支払い情報の紐づけはRedis KVで管理しています。デモアプリでは15分のTTLを設けてます。
Payment Mandate作成・ユーザー署名作成
そろそろフィナーレです!本当にお疲れさまでした..!Payment Mandateを作成します。
最終的に作成されるPayment Mandateは次のようなものです。1つ1つ項目を見ていきましょう。
{
"type": "HTTP_REQUEST",
"method": "POST",
"url": "http://merchant_agent:8001/a2a/message",
"headers": {},
"body": {
"header": {
"message_id": "3bde0bf1-d43a-48c0-bdaa-2381440cc126",
"sender": "did:ap2:agent:shopping_agent",
"recipient": "did:ap2:agent:merchant_agent",
"timestamp": "2025-11-03T04:33:37.122880Z",
"nonce": "6cd7d5cb64dc0dd79162d1498167400c673ab95991fda75c60253bed2a76fa26",
"schema_version": "0.2",
"proof": {
"algorithm": "ed25519",
"signatureValue": "b1AxcN7qA41tPkwbD0DJWxoXXTzb8BBy3rDdynepyw2mefNLI2yqYsYJE+/xmdEXXSIMKL1UIYhAbO5/4MFACw==",
"publicKeyMultibase": "z6MkpL5YFLHxAcp6LSJboXQ3nBnNGrQ4TiZRmWBZamPo7t8x",
"kid": "did:ap2:agent:shopping_agent#key-2",
"created": "2025-11-03T04:33:37.122880Z",
"proofPurpose": "authentication"
}
},
"dataPart": {
"@type": "ap2.mandates.PaymentMandate",
"id": "payment_8229592a",
"payload": {
"payment_mandate": {
"payment_mandate_contents": {
"payment_mandate_id": "payment_8229592a",
"payment_details_id": "order_87b47327",
"payment_details_total": {
"label": "Total",
"amount": {
"value": 2315.0,
"currency": "JPY"
}
},
"payment_response": {
"methodName": "https://a2a-protocol.org/payment-methods/ap2-payment",
"details": {
"cardBrand": "Visa",
"token": "tok_b75118d6_puyZ-5Oq9MT8m0Abhx3ux__w",
"tokenized": true
}
},
"merchant_agent": "did:ap2:merchant:mugibo_merchant",
"timestamp": "2025-11-03T04:33:33.924986Z"
},
"user_authorization": "eyJpc3N1ZXJfand0Ijxxxx.........",
"id": "payment_8229592a",
"cart_mandate_id": "cart_fddcfe5d",
"intent_mandate_id": "intent_88ed322e",
"payer_id": "usr_cdb4ec851bcf4f73",
"payee_id": "did:ap2:merchant:mugibo_merchant",
"amount": {
"value": 2315.0,
"currency": "JPY"
},
"payment_method": {
"type": "basic-card",
"token": "tok_b75118d6_puyZ-5Oq9MT8m0Abhx3ux__w",
"last4": "1111",
"brand": "Visa"
},
"risk_score": 50,
"fraud_indicators": [
"risk_assessment_failed"
]
}
},
"kind": null,
"artifact": null
}
}
}proof構造
もうおなじみの流れだと思いますが、Payment MandateはSAが作成するのでIntent Mandate同様、SAの署名をproof構造で付けてA2A MessageをMAに送ります。このあたりは説明済みなので詳細は省略します。
合計金額(payment_details_total)
Payment Mandateの特徴ですが、あくまでも支払い委任状なので、注目するべきは支払う合計金額です。カートの詳細はCart Mandateを参照する、という思想となります。
よって、 payment_details_totalにはカートの明細は入らず合計金額のみ入ります。
支払い情報(payment_response)
支払い情報は支払い方法のトークン化で取得した一時的な支払いトークンをpayment_responseにて設定します。こうすることで、決済処理に関わるエンティティ(SA、MA、MPP)に実カード情報が流れることなく決済が可能となります。
ユーザー署名(user_authorization)
Payment Mandateではユーザー署名が必要です。
これをどう作るのか、というところの仕様を調べる・実装するのに相当苦労しました。というのも、お店側(Merchant)の場合は、事前に用意した公開鍵・秘密鍵を使ってJWTを作成すればよいとイメージがつくのですが、ユーザーの公開鍵・秘密鍵とはいったい...。
これがよくわからず非常に苦戦していたのですが、色々調べるとユーザーの公開鍵・秘密鍵ペアは、WebAuthn(パスキー)を用いてブラウザやOSレベルで安全に管理された鍵を使っていそうなことがわかりました。(おそらく...。間違っていたら指摘してほしいです。)
なので、このデモではユーザーの公開鍵・秘密鍵はWebAuthnを用いて実現している実装、と理解いただければと思います。
WebAuthn Assertion
少しだけWebAuthn(パスキー)の復習です。
WebAuthn(パスキー)の鍵ペアはユーザーのデバイス上で生成され、秘密鍵はデバイスのセキュア領域(TPMやSecure Enclaveなど)から外部に出ることはありません。
この動きはパスキーの登録時に実施されます。また、公開鍵はAP2ではCPに登録されており、AP2ネットワーク内の他のエンティティがユーザー署名を検証する際に利用できます。
ということは、
- WebAuthnを通じてデバイス内の秘密鍵で署名
- CP経由で公開鍵を取得し、検証する
という流れを踏めばユーザー署名が実現できそうです。
その考え方で実装したデモアプリのログから作成したシーケンスがこちらです。これを1つ1つ追っていきましょう。

CPからパスキーの公開鍵をもらう
SAがCPにパスキーの公開鍵を要求すると、COSE(CBOR Object Signing and Encryption)形式の公開鍵を返します。
ap2_credential_provider | {"timestamp": "2025-11-03T07:54:36.506247Z", "level": "INFO", "logger": "services.credential_provider.provider", "message": "[get_passkey_public_key] Public key retrieved: credential_id=dCDQdGlqx50G-UFW..., user_id=usr_cdb4ec851bcf4f73", "module": "provider", "function": "get_passkey_public_key", "line": 1536}
ap2_shopping_agent | {"timestamp": "2025-11-03T07:54:36.507721Z", "level": "DEBUG", "logger": "services.shopping_agent.agent", "message": "HTTP_RESPONSE_RAW: {\"type\": \"HTTP_RESPONSE\", \"status_code\": 200, \"headers\": {\"date\": \"Mon, 03 Nov 2025 07:54:36 GMT\", \"server\": \"uvicorn\", \"content-length\": \"212\", \"content-type\": \"application/json\"}, \"body\": {\"credential_id\": \"dCDQdGlqx50G-UFWp0ORsF5Y7mzWSAkYow\", \"public_key_cose\": \"pQECAyYgASFYIOnePT967mopshGl7tTo53MmMkE/bY6/WZuuZLHSWZYrIlggJj7UcPfh0MaQpNxA5bgmtZPTWi8YVP4x4C8ivq8RFDQ=\", \"user_id\": \"usr_cdb4ec851bcf4f73\"}, \"duration_ms\": 3.767251968383789}", "module": "logger", "function": "log_http_response", "line": 226}COSEについては門外漢の私が説明するとボロが出そうなのですが、楕円曲線暗号(P-256)の公開鍵のX, Y座標を含む構造をバイナリにしたもの、という理解で大丈夫だと思います。
バイナリにすることで、軽量に扱うことができるわけですね。
User Authorization VP作成
User Authorization VP(User Authorization Verifiable Presentation)とは、その取引(Cart Mandate + Payment Mandate)をユーザーが認可したその意思を署名付きで証明することです。
かなり複雑な流れになります&私が門外漢なので拙い説明になりますがお付き合いください。
webauthn_challenge取得
まず、パスキーでユーザー認証(assertion)し、その結果得られる clientDataJSON から チャレンジ(webauthn_challenge)を取得します。
チャレンジは、サーバーが認証要求時に生成した一時的なランダム値で、リプレイ攻撃を防ぐために使用されます。
この値を含めて署名検証を行うことで、ユーザーが実際にサーバーからの要求に応じて署名したことを確認できるというわけです。
ap2_shopping_agent | [2025-11-03 07:54:36,508] INFO in common.user_authorization: [create_user_authorization_vp] WebAuthn challenge from assertion: eyJtYW5kYXRlX2lk...Mandateのハッシュ計算
次に、MandateをRFC 8785 (JSON Canonicalization Scheme)正規化し、SHA-256でハッシュ化します。これでMandateが改ざんされたかどうかを確認できるわけです。
ap2_shopping_agent | [2025-11-03 07:54:36,509] INFO in common.user_authorization: [create_user_authorization_vp] cart_hash: 1b6d38d8ef86cf9f...
ap2_shopping_agent | [2025-11-03 07:54:36,509] INFO in common.user_authorization: [create_user_authorization_vp] payment_hash: 806da3986f122c64...COSE形式の公開鍵を復元
先ほどお話したとおり、COSE(CBOR Object Signing and Encryption)形式の公開鍵がCPから返却され、これだとJWK(JSON Web Key)として利用しにくいので、復元し後段の処理で利用可能な形にします。
(中身の処理は複雑すぎるので割愛します。詳しくはコードを見てください。)
ap2_shopping_agent | [2025-11-03 07:54:36,511] INFO in common.user_authorization: [create_user_authorization_vp] Restored public key from COSE format (DB)Issuer JWT を生成しcnf claimを含める
ユーザーをIssuer(発行者)として、署名した(実はしてない)JWTを生成し、cnf claim(Confirmation claim)の項目に、パスキーから得られるユーザーの公開鍵をJWK(JSON Web Key)形式で埋め込みます。
これにより、このあと作成するKey-binding JWTと組み合わせることでMPP(お店の決済プロセス)などが検証をするときに確かにこのユーザーが署名したJWTという鍵の関連性(Key Binding)が明示できるわけです。
ap2_shopping_agent | [2025-11-03 07:54:36,511] INFO in common.user_authorization: [create_user_authorization_vp] cnf claim with JWK added to Issuer JWTcfn claimをつけたJWTのペイロードは次のような形になります。
(issやsubはユーザーのDIDです。DIDはこのデモアプリのユーザーIDから生成していますが、おそらくはAP2の世界でユニークになるような工夫が必要だと思います。)
{
"iss": "did:ap2:user:usr_cdb4ec851bcf4f73",
"sub": "did:ap2:user:usr_cdb4ec851bcf4f73",
"iat": 1762210741,
"exp": 1762211041,
"nbf": 1762210741,
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "xxxxxxxxxxxxxxx",
"y": "xxxxxxxxxxxxxxx"
}
}
}また、かなり細かい内容にはなるのですが先ほど、
ユーザーをIssuer(発行者)として、署名した(実はしてない)
という文書には含みがあって、実際にはIssuerの公開鍵で署名してません!
嘘じゃん!と思うかもですがこれには理由があって、WebAuthn APIは特定のチャレンジに対してのみ署名を生成するため、Issuer JWTに署名する、ということは不可能になります。
もし、Issuer JWTに署名付けをやるとすると、WebAuthn以外の方法で生成された鍵を使った署名を実施することになりますが、それだとパスキーだけで完結するユーザー体験になりません。
なので、ここは妥協でIssuer JWTには署名を付けず、次で説明するKey-binding JWTにsd_hash(Issuer JWTのハッシュ)を入れることで解決します。
よって、Issuer JWTのヘッダーは、
issuer_jwt_header = {
"alg": "none", # JWT標準準拠(RFC 7519): 署名なしを明示
"typ": "JWT"
}のように署名がないことを明示しています。
Key-binding JWT を生成
次に、Mandateハッシュのトランザクションデータを含めたJWTを生成します。これにより、Cart MandateやPayment Mandateの受け渡しつまりトランザクションについて、確実にユーザーが確認・承認しました。ということになるのです。
transaction_dataにCart MandateやPayment Mandateのハッシュを入れることで、これらのトランザクションすべてにユーザーは承認した、という意思表示ができます。
また、webauthn項目にassertionを入れることで、このKey-binding JWTを見るだけで検証が可能となります。
(仕様的にこれでいいのかわからない。詳しい人...助けて。)
{
"aud": "did:ap2:agent:payment_processor",
"nonce": "rH4AgxNBXcnxNlcPXjoP2nLGynP8dmJhdSF96Cngz9w",
"iat": 1762210741,
"sd_hash": "xxxx",
"transaction_data": [
"xxxxx",
"xxxxx"
],
"webauthn": {
"credential_id": "dCDQdGlqx50G-UFWp0ORsF5Y7mzWSAkYow",
"authenticator_data": "xxxxxxx",
"client_data_json": "xxxxxxx",
"user_handle": "usr_cdb4ec851bcf4f73"
}WebAuthn署名をKB-JWTの署名として扱う(署名化)
さて、Issue JWTとKey-binding JWTができたので、Key-bindng JWTに署名します。ここでポイントなのは、WebAuthn署名を実施するということです。Key-bindng JWTをBase64url形式にしたものをパスキーの認証器に渡し、認証器からSignatureを受け取ります。これをKey-binding JWTの署名として扱うわけです。
ap2_shopping_agent | [2025-11-03 22:32:19,412] INFO in common.user_authorization: [create_user_authorization_vp] Generated SD-JWT+KB user_authorization (IETF standard): length=1571, cart_hash=396e1f1ea5518055..., payment_hash=a07655c1e051df12...この際、デモアプリのようにユーザーにはPayment Mandateの署名依頼の形でポップアップを出すのが良いかと思います。

SD-JWT+KB(issuer_jwt~kb_jwt)組み立て
SD-JWT+KBの標準仕様とは異なります。この実装でいいのかわからないのですが、制約を考えるとこの実装に落ち着きました...。完全に独自解釈で実装を進めています。
2つのJWTができましたので、こちらをSD-JWT(Selective Disclosure JWT)とKBの形式で組み立てます。
SD-JWTはJWTの一部のみを選択的に公開する技術ですが、ここではDisclosureとしてKBをつけて署名します。
(このフォーマットでいいのかどうなのかはAP2のドキュメントをみてもわからないのですが、多分...大丈夫...。わからん....。)
これで完成です!生成されたものはBase64でエンコードされていますので、わかりやすくBase64をデコードしたUser Authorizaion VPを示すと次のようになります。
SD-JWTのHeader
{
"alg": "none",
"typ": "JWT"
}SD-JWTのBody
{
"iss": "did:ap2:user:usr_cdb4ec851bcf4f73",
"sub": "did:ap2:user:usr_cdb4ec851bcf4f73",
"iat": 1762214326,
"exp": 1762214626,
"nbf": 1762214326,
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "xxxxx",
"y": "xxxxx"
}
}
}KBのHeader
{
"alg": "ES256",
"typ": "kb+jwt"
}KBのBody
{
"aud": "did:ap2:agent:payment_processor",
"nonce": "rmPX74BFR6rTBG4Pdu2ec4r9g9BjjkEv_w2l3FAKqh4",
"iat": 1762214326,
"sd_hash": "26e0a50c27ea3ffe1af0d221c969503c212f75c1530ba81c4a6dd8fad76ed0ad",
"transaction_data": [
"92b50fc46c06554f2eb3786f71917076a8b3a4abf8fdb1c455c0c33f08731408",
"c66727e7d7bbe76654a860e1dcffb09f0c1d32d2065b2efcb54142dc9d92c3ce"
],
"webauthn": {
"credential_id": "dCDQdGlqx50G-UFWp0ORsF5Y7mzWSAkYow",
"authenticator_data": "xxxxxx",
"client_data_json": "xxxxx",
"user_handle": "usr_cdb4ec851bcf4f73"
}
}以上で無事、User Authorization VPを作成できました。お疲れ様でした!
リスク審査
デモアプリでは簡易的な実装にとどめてますが、SAがPayment Mandateを作成するときに決済に関係するエンティティに正しく取引の事情を伝える必要があるのでSAにてリスク審査を実施します。
例えばユーザーのIntentで指定された金額の範囲に取引が収まっているか、指定のブランドで購入が進められているか指定のカードブランドに問題はないか、などからリスクスコアの形で判定します。
しかしながら、このあたりは明確な仕様がわからないところなのでもう少し調査が必要です。
Payment Mandate送信
それでは、MAにPayment Mandateを送信します。ペイロードは先ほど貼り付けたものです。
proof構造などを追加で作成し、確かにSAから送信されていることと、User Authorization VPをつけて確かにユーザーが承認したことを保証します。
ついに決済処理!(execute_payment)
いよいよ決済処理です。とても長かった...。ここまで読んでいる人いますか...?
LangGraphのexecute_paymentで実施してますが、SAではほとんどリクエストを投げたあと待ちの状態です。
実際の処理の多くはMPP(Merchant Payment Processor)で実施しています。
決済処理はMPPが実施
決済に関する処理はMAではなく、MPPが実施します。これはAP2が役割ベースのアーキテクチャを採用しているためで、決済関係はMPPが一任することになります。
ただし、SAからMPPは見えないので、SAは一旦MAにPayment Mandateを送信し、MAはMPPにそのままPayment Mandateをパススルーします。
(SAからの署名を検証するのはMAです。その後、MAは再度A2Aメッセージに自身の署名を付けてMPPに送信をします。)
Proof・User Authorization VPの検証
MPPではまず、送信されたPayment Mandateの検証を行います。今回は2段階で、
- MAから送信されたことを証明するproof構造の検証
- User Authorization VPの検証
の検証を進めます。MAから送信されたことを証明するproof構造の検証は別箇所でも実施している処理なので説明を省略します。
User Authorization VPの検証は、SD-JWT+KBをパースした後、Cart MandateとPayment Mandateのハッシュが正しいか、WebAuthnで署名されたKBが確からしいかなどを検証します。
ap2_payment_processor | [2025-11-03 22:32:19,725] INFO in services.payment_processor.utils.mandate_helpers: [PaymentProcessor] PaymentMandate validation passed: payment_556c011e, user_authorization present: eyJhbGciOiJFUzI1NiIs...
ap2_payment_processor | {"timestamp": "2025-11-03T22:32:19.726152Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] Mandate chain validation: PaymentMandate(payment_556c011e) → CartMandate(cart_6f9f1032)", "module": "processor", "function": "_validate_mandate_chain", "line": 671}
ap2_payment_processor | {"timestamp": "2025-11-03T22:32:19.728043Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] Verifying SD-JWT-VC user_authorization: cart_hash=396e1f1ea5518055..., payment_hash=a07655c1e051df12...", "module": "processor", "function": "_validate_mandate_chain", "line": 688}
ap2_payment_processor | [2025-11-03 22:32:19,728] INFO in common.user_authorization: [verify_user_authorization_vp] Parsed SD-JWT+KB format successfully
ap2_payment_processor | [2025-11-03 22:32:19,728] INFO in common.user_authorization: [verify_user_authorization_vp] Hash verification passed: cart_hash=396e1f1ea5518055..., payment_hash=a07655c1e051df12...
ap2_payment_processor | [2025-11-03 22:32:19,734] INFO in common.user_authorization: [verify_user_authorization_vp] ✓ WebAuthn signature verified successfully
ap2_payment_processor | [2025-11-03 22:32:19,734] INFO in common.user_authorization: [verify_user_authorization_vp] Key-binding JWT payload verified
ap2_payment_processor | [2025-11-03 22:32:19,734] INFO in common.user_authorization: [verify_user_authorization_vp] ✓ SD-JWT+KB verification passed (IETF standard)MPPによる決済処理
さて、いよいよPayment Mandateを使った決済処理を実施していきます。
MPPによるMerchant署名の検証
merchant_authorizationのJWTがCart Mandateにつけた署名をMPPでも確認し、この取引がお店側の確かな承認があることを検証していきます。merchant_authorizationのJWTの検証と、Cart Mandateのハッシュを照合してCart Mandateがたしかにお店側が署名したものと確認できました。
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.672231Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[_verify_merchant_authorization_jwt] JWT validation passed: iss=did:ap2:merchant:mugibo_merchant, exp=1762217912, jti=b13c2752-9b28-44..., cart_hash=92b50fc46c06554f...", "module": "processor", "function": "_verify_merchant_authorization_jwt", "line": 613}
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.672387Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] merchant_authorization JWT verified: iss=did:ap2:merchant:mugibo_merchant", "module": "processor", "function": "_validate_mandate_chain", "line": 725}
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.672493Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] CartMandate hash in merchant_authorization: 92b50fc46c06554f...", "module": "processor", "function": "_validate_mandate_chain", "line": 733}
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.673139Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] ✓ CartMandate hash verified (merchant_authorization): 92b50fc46c06554f...", "module": "processor", "function": "_validate_mandate_chain", "line": 750}Cart Mandate→Payment Mandateのチェーンが正しいか確認
次に、Payment MandateがCart Mandateを正しく参照しているか、そのチェーンが有効かを確認します。
Cart MandateのIDがPayment Mandateに記載されているため、そのIDが一致しているかをチェックします。
支払いトークンの有効性確認
支払い方法は前回のステップですでにCPによってトークン化されています。そのトークンの有効性と所有権をCPに問い合わせて確認する必要があります。
CPでは、トークンから実際の支払い情報にアクセスし、payer_idが一致していること、期限が切れていないことなどを確認します。
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.679844Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "[PaymentProcessor] Verifying credential with Credential Provider: token=tok_835e197e_AZJUO7_...", "module": "processor", "function": "_verify_credential_with_cp", "line": 916}
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.682497Z", "level": "INFO", "logger": "services.payment_processor.processor", "message": "HTTP Request: POST http://credential_provider:8003/credentials/verify", "module": "logger", "function": "log_http_request", "line": 182}
ap2_payment_processor | {"timestamp": "2025-11-03T23:58:46.682622Z", "level": "DEBUG", "logger": "services.payment_processor.processor", "message": "HTTP_REQUEST_RAW: {\"type\": \"HTTP_REQUEST\", \"method\": \"POST\", \"url\": \"http://credential_provider:8003/credentials/verify\", \"headers\": {}, \"body\": {\"token\": \"tok_835e197e_AZJUO7_Qat-15UWBHUHtlVG8\", \"payer_id\": \"usr_cdb4ec851bcf4f73\", \"amount\": {\"value\": 6215.0, \"currency\": \"JPY\"}}}", "module": "logger", "function": "log_http_request", "line": 193}
ap2_credential_provider | {"timestamp": "2025-11-03T23:58:46.721823Z", "level": "INFO", "logger": "services.credential_provider.provider", "message": "[verify_credentials] Verifying token for payer: usr_cdb4ec851bcf4f73", "module": "provider", "function": "verify_credentials", "line": 1611}
ap2_credential_provider | {"timestamp": "2025-11-03T23:58:46.727054Z", "level": "INFO", "logger": "services.credential_provider.provider", "message": "[verify_credentials] Token verified: payment_method_id=pm_f4745ec2, user_id=usr_cdb4ec851bcf4f73", "module": "provider", "function": "verify_credentials", "line": 1648}
ap2_credential_provider | INFO: 172.18.0.5:40314 - "POST /credentials/verify HTTP/1.1" 200 OKリスク審査
SAが実施したリスク審査のスコアを確認し、最終的に決済を行うか、追加で審査するかを確認します。このあたりの具体的なチェック項目はドキュメントを読んでもわからなかったのですが、AIエージェントが決済に関わっていることや、トランザクションの様式によってチェック項目が変わると想定しています。
決済処理
このデモでは決済ネットワークはただのスタブなので詳細は割愛しますが、決済ネットワークに対して決済処理を実施します。
領収書の生成とCP・MAへの通知
無事決済が完了したらMPPは領収書の発行をします。また、CPに決済の完了とトランザクションの状態、領収書情報を通知します。
また、ユーザーにも完了を通知したいためMA→SA→ユーザーへ通知をします。
すべての処理が完了!!!
レシートをユーザーに提示してすべての処理が完了です。

レシートはこんな感じのPDFにしてます。

結論
AP2はとても画期的なMandateと呼ばれる仕組みを上手くつかって、AIエージェントでの安全な購入体験を実現できます。ただし、実際に公式ドキュメントやGitHubのリポジトリを参考に実装してみると、門外漢にはものすごい難しい暗号・署名技術が使われていたり、UI/UXまで考えるとMandateだけでは足りない項目も多いなと実感します。なので、AP2を理解するのは総合格闘技感がありました。難しい。
また、Human not Presentの仕様も固まっていないところが正直多いです。むしろAP2はHuman not Presentが醍醐味というところもあります。
このあたりの実装を進められるように色々リファレンス実装が出てくるといいですね!
おまけ - むぎぼーショップってなに?
このデモで出てくるむぎぼーショップはもちろん現実には存在しません。
ですが、むぎぼーは現実に存在します。我が家の豆柴です。豆柴...のつもりが大きくなって、柴犬に片足突っ込んでいる豆柴です。
カート選択時にでてくる商品は、むぎの写真をNano Bananaで作成しました。


