DEV Community

tubone24
tubone24

Posted on • Originally published at tubone-project24.xyz

Getting Started with AP2

Note

The purpose of this article and demo app is to provide an introduction to AP2, so some explanations may not be technically precise (prioritizing clarity over accuracy). Additionally, since there aren't many implementation references for AP2, much of this contains my own interpretations.

I'm writing this with the hope that you'll get a general feel for how it works. My apologies in advance!

For Those in a Hurry

I've created a demo app that conforms to AP2 as much as possible.

demo

Docker Compose launches all the services needed to discuss AP2, allowing you to experience AP2 shopping in a simulated environment. (Of course, you can't actually make real purchases.)

Clone the demo app from here: https://github.com/tubone24/AP2_demo_app/tree/main

docker compose build
docker compose up
Enter fullscreen mode Exit fullscreen mode

This should start the app at http://localhost:3000. However, the local LLM used internally runs on Docker Model Runner, so you'll need to set that up first.

Why I Wanted to Learn AP2

I'm currently on parental leave. While I'm grateful to watch my child grow every day, I'll be honest—it's tough seeing my talented colleagues challenge themselves with new things on X (Twitter).

So, I decided to get off my butt and learn AP2 (Agent Payments Protocol) because I'd be crushed with anxiety when I return to work if I don't learn something new.

The generative AI world keeps churning out mysterious protocols one after another... I just finished learning about MCP, and now there's A2A, and AP2... What a predicament.

According to Google, AP2 is:

An open protocol developed jointly with major payment and technology companies to securely initiate and execute agent-driven payments across platforms

In essence, it's a system that elegantly solves various inconveniences, problems, and disputes that can arise when considering shopping and payments involving AI agents.

So what problems can actually occur when shopping with AI agents?

Let's First Imagine Human Shopping

Before thinking about AP2, let's imagine how humans shop.

a

Let's say Person A is thinking, "I want some cute dog goods." By the way, in AP2, this feeling ("I want cute dog goods") or intention is called an Intent.

While walking around, they discover a shop called Mugibo Shop featuring a cute Shiba Inu motif. Let's go inside.

Mugibo Shop has excellent staff who present optimal products tailored to customer preferences. After hearing specific requests and checking inventory, they pick out several products within budget and create a recommended set.

a

Person A is satisfied with the recommended set and proceeds to payment. They complete the transaction using an xxPay terminal installed in the shop. They receive their products and receipt, completing the shopping experience.

a

That's human shopping.

An important point is that the shopping scenario AP2 envisions is not a self-service style like supermarkets, but rather over-the-counter sales like pharmacies.

So the staff puts items in the shopping cart, the user confirms the cart and gives approval to proceed with the purchase.

In other words, it's like the AI-generated image below, where the staff picks items from the shelves saying, "For these symptoms, this combination of medicines would be best!" (Understanding this will significantly change your comprehension.)

Pharmacy

What happens when we introduce AI agents into this shopping process?

Who Made the Mistake in This Purchase...?

Now, let's imagine delegating part of this shopping process to AI agents.

For example, let's consider a scenario with a Shopping Agent (SA) that shops on behalf of the user, and a Merchant Agent (MA) that handles the shop staff's duties.

Hallucination

The first thing that comes to mind is AI agent hallucination. This is the phenomenon where LLMs confidently spout incorrect information as if they know it.

When this happens in a shopping situation, it's a big problem. Moreover, there are many patterns of potential mistakes.

The easiest to imagine is the pattern where the SA misunderstands the user's intent and conveys it incorrectly to the MA. Like a game of telephone, the purchase proceeds based on the wrong intent. It might be acceptable for small purchases, but having millions of yen in transactions processed without permission would be a problem.

ia

There could also be cases where the MA misinterprets the intent.

aa

Considering more complex cases, even if the user's intent is correctly conveyed, there could be mistakes due to insufficient context on the MA's side—for example, selling products that should only be sold to regular customers.

aaa

Even More Troublesome Patterns

Even more troublesome patterns are conceivable. For example, Person A tells the AI agent "I want cute dog goods" but then changes their mind midway.

Whether it's a change of heart, mischief, or misunderstanding, there could be various reasons, but if the user says after the transaction is complete, "Actually, I wanted cute cat goods," it becomes unclear who should take responsibility.

aaa

Additionally, similar problems could arise if someone impersonates the user and instructs the AI agent to make purchases. (Impersonation problem)

aaa

The Problem of AI Agents Knowing Too Much Information

Also, on a slightly different note, when an AI agent (SA) shops on behalf of the user, it becomes problematic if it can freely access the user's wallet.

When assuming credit card payments, the agent would know the card number, expiration date, and CVC (Card Validation Code, the 3-digit code on the back of the card). This creates risks of card information leakage in case of incidents, or the AI agent going rogue and buying luxury items indiscriminately.

Additionally, there's a system maintenance concern about the AI agent (MA) becoming tightly coupled with the shop's payment system.

aa

How Does AP2 Solve These Problems?

AP2 solves these issues by cleverly extending digital signatures combining advanced cryptographic techniques to A2A (Agent2Agent) communication.

Sequence

First, let me show you an easy-to-understand (?) picture story without difficult terminology, and then cover the specific concepts and terms that appear in it—I'm confident this approach will be easier to understand. Please bear with me through this picture story.

(For explanation purposes, I've rearranged some steps. Also, while I've referenced the currently published Illustrative Transaction Flow, some specifications are unclear and contain considerable speculation. Please understand this.)

a

First, Person A instructs the Shopping Agent (SA) with specific purchase intentions like "I want cute dog goods" and "within 5,000 yen". The SA then grasps the purchase intention and begins shopping.

Before actually shopping, the user and SA exchange an Intent Mandate—a delegation document of the purchase intention—essentially saying, "I, Person A, hereby delegate to SA the purchase of 'cute dog goods within 5,000 yen.'" I'll explain Intent Mandate in more detail later.

At the same time, the Merchant Agent (MA) of "Mugibo Shop" exchanges business cards with the SA, introducing their shop.

Those familiar with A2A will understand this—this business card exchange is the A2A Agent Card. It conveys their inventory status and that they can provide an AP2-compatible shopping experience.

c

Next, based on the business card exchange results with the MA, the SA decides to start a transaction with Mugibo Shop. (In reality, not just the Agent Card itself, but trust domains and certificate chains would also be checked to verify transaction eligibility, but I'll skip that here.)

Agent-to-agent communication between SA-MA is conducted using A2A Messages.

By sending the Intent Mandate exchanged with the user, the SA demonstrates to the MA that it's making a purchase that truly captures the user's intent, while asking them to create the target shopping cart.

The MA receives the user's Intent and begins considering what product set would be best. It examines the shop's (Merchant's) inventory status, prices, and descriptions of each product to create a shopping cart with products perfect for the user.

d

Now, the MA has finally created a shopping cart with products perfect for the user. This shopping cart creation should originally be done by the shop side (Merchant), but the MA is creating it on behalf of the shop.

Therefore, it's necessary to show that the shop's sales activities are being delegated in the form of a delegation document.

So, the MA creates a shopping cart delegation document (Cart Mandate), and the shop confirms it and adds their digital signature.

b

Once the delegation document exchange between the shop and MA is complete, that delegation document (Cart Mandate) is sent to the SA. The SA will present this shopping cart to the user while proceeding with the purchase.

h

Person A finalizes the shopping cart they're purchasing based on the Cart Mandate presented by the SA. (Cart Mandates can be presented as multiple A2A Artifacts, so a UI for selecting from multiple candidates is assumed. This demo app presents multiple carts.)

For the finalized shopping cart delegation document, ~the user adds their signature, meaning "Person A has definitely confirmed this."~ the user is asked to confirm it. (Actually, Device Attestation operations occur, but I'll explain this in the Payment Mandate section.)

(As mentioned earlier, it was structurally difficult to add a user signature to Cart Mandate. This is because adding a user signature would break the JSON structure and invalidate the Merchant's signature before it. While it's defined in the specification, the implementation handling requires some ingenuity.)

i

At the same time, the SA needs to confirm the payment method for the user's product purchase. In AP2, a mysterious concept called Credential Provider (CP) appears here.

According to the official documentation, CP is:

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.

So, CP means a specialized entity that securely manages and executes user payment and ID credentials.

You can think of it as a digital wallet combining an ID wallet and payment wallet. (To be precise, since the main purpose is for CP to handle secure management of payment credentials and user consent flows, ID wallets could be independent from CP.)

Google and Apple seem to be working toward supporting this. The point is that it handles not only payments but also proving the user's identity.

To later ask the user about their payment method, we check the payment methods associated with CP. According to the current AP2 specification (v0.1), credit cards and debit cards are selectable, and various payment services will be supported in the future.

The key point here is that the SA receives only the minimum information needed to have the user select a payment method (such as the last 4 digits of the credit card), and receives only information that does not constitute PCI data, excluding card numbers and CVC.

f

Now let's proceed to payment. The SA has the user finalize the payment method to use from the payment information received from CP. Once the user finalizes which card to use for payment, they request CP to issue a token (payment method token) for that card.

This token becomes temporary card information usable for payment. However, the token contains no payment-required information (PCI data, card numbers, etc.). Since CP has a mapping between tokens and actual card information, payments can be made through CP.

Once the payment method token is obtained, the SA creates a Payment Mandate (Payment Mandate). This is a delegation document for payment that specifies the total amount and payment method without detailed item specifics. First, the SA presents this delegation document to the user saying, "I, SA, will make payment as per this delegation document on behalf of Person A," and obtains the user's signature to confirm user intent.

At this point, Attestation operations occur in the app. While this deviates from official documentation, according to a CSA blog article, password authentication is not recommended and stronger authentication is required.

Guidelines for secure implementation include integration with Strong Customer Authentication (SCA), configuration of Intent Mandate TTLs, and dispute resolution protocols.

By executing through hardware-backed keys and in-session authentication (such as biometrics) using TPM or Secure Enclave, it becomes a blood oath that Person A has definitely approved.

e

Once the user-signed Payment Mandate is complete, it's sent to the MA, and finally, the payment process begins.

What's important here is that the MA doesn't perform the payment processing itself—the Merchant Payment Processor (MPP), a payment entity associated with the shop, handles it. This is an implementation following AP2's basic concept of clear separation of duties that separates payment-related processing from the MA.

g

Once payment is complete, MPP issues a receipt and sends it to CP and SA. The SA receives the receipt and notifies the user of the transaction completion. This is what AP2 accomplishes.

Great work!

Mandates

Let's revisit the Mandate—the delegation document with digital signature mechanism that's essential to discussing AP2.

Users and shops digitally sign delegation documents using their respective private keys, and by verifying these at each step, we guarantee that the delegation was definitely performed by each entity. The mechanism where each entity approves, signs, and verifies delegation documents using digital signatures is arguably the most interesting part of AP2.

There are 3 types of Mandates, and necessary information is sent and received at each sequence. Additionally, Mandates have chains, with Payment Mandate referencing Cart Mandate, and Cart Mandate referencing Intent Mandate. This creates a relationship where each delegation document is created based on the previous one.

Intent Mandate

First is the Intent Mandate. This is where the SA represents the user's purchase intention (Intent).

The SA creates it and confirms with the user: "Your purchase intention is this, right? Is it okay if I send this to the MA?" and then sends the confirmed delegation document to the MA.

I mentioned taking "user confirmation," but depending on how the user is involved (the transaction modality), whether a user signature is required or just confirmation varies. I'll explain transaction modalities later.

This demo app is created using the Human Present transaction modality, which has detailed sequence breakdowns, so it's not using the more innovative Human Not Present implementation. My apologies. Once I understand the specifications better, I'd like to try Human Not Present as well.

Cart Mandate

Next is the Cart Mandate. This refers to the cart containing products that the MA has created.

To proceed with the purchase, the MA obtains approval from the shop side (Merchant) in the form of a signature, confirming "whether the shop can really sell these products, whether it's an appropriate shopping cart."

The Cart Mandate includes the shop's signature as a Base64 string of the Cart Mandate JSON signed with JWT. (Details will be explained later in the demo app explanation.)

Also, for Cart Mandate in the Human Present transaction modality, there's language suggesting user signature is required:

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.

However, when actually creating the demo app, it was difficult to add a signature to Cart Mandate itself (it would invalidate the Merchant's signature), and reliable implementation is currently difficult with official documentation alone.

By the way, in the official documentation Cart Mandate sample, there's a user_signature_required field set to false. This means the sample assumes no user signature on Cart Mandate, but this sample refers to Human Not Present Cart Mandate, so it's not useful for reference here.

If you absolutely want to add a user signature to Cart Mandate, while not specified in the specification, there are two possible implementation methods:

  1. Keep Cart Mandate unchanged (add merchant_authorization), put the user signature in PaymentMandate's user_authorization, and guarantee user consent for Cart Mandate through the Mandate chain. (The official GitHub seems to follow this specification.)
  2. Add a user_authorization part as a detached signature within Cart Mandate (completely my own interpretation. A way to add user signature without breaking Merchant's signature.)

This area seems still ambiguous as a signature schema, so let's look forward to future developments. The demo app proceeds with implementation using option 1.

Payment Mandate

Finally, the Payment Mandate. This is a delegation document necessary for the SA to make payments on behalf of the user, characterized by including the total amount as well as user signature and payment-related information.

This Mandate allows us to show that the user has given final agreement to all transactions. User signature is required for Payment Mandate.

Also, as processing proceeds from Intent Mandate → Cart Mandate → Payment Mandate, the ID and hash of the previous Mandate are recorded. (Creating a chain.) This allows the shop's payment processing system and payment network to correctly convey risks and see what process the transaction went through as an agent-based transaction by looking at the Mandate.

Transaction Modalities

AP2 has mainly 2 transaction modalities: Human Present (payment with human present) and Human Not Present (payment without human present). The way users are involved differs.

Human Present

First, Human Present applies to scenarios where the user delegates tasks to the AI agent but is present (available) to approve the final payment. The image is like chatting with a chatbot to select products and completing the purchase in that flow.

In this case, Payment Mandate should prove user intent as non-repudiable. Meanwhile, user signature on Intent Mandate can be omitted. (As far as I can tell from reading the documentation. You can sign it, but since signature requests involve passkey authentication with pop-ups appearing each time, avoiding it in this demo app as it would be quite redundant from a UI/UX perspective.)

Human Not Present

Human Not Present applies to scenarios where the user delegates tasks to the agent and allows the agent to autonomously execute payments without the user present.

For example, telling the SA conditions in advance like "Buy these shoes if the price drops below $100" and having the SA proceed with the purchase when those conditions are met while the user is absent.

In this case, Intent Mandate should prove user intent as non-repudiable. Therefore, user signature is required on Intent Mandate for Human Not Present.

That said, the Human Not Present specification isn't detailed in official documentation yet, so I don't know how Payment Mandate is handled afterward, or how dispute resolution works if inappropriate purchases proceed. My apologies.

Let's Look at What I Built (Exploring the Demo App)

My knowledge of AP2 from documentation is honestly at this level, so I'd like to dig deeper into the detailed behavior, signature mechanisms, content actually exchanged in A2A, UI/UX, etc., while actually building.

Since there was no Human Not Present sequence in official documentation, I haven't supported that.

Also, as someone unfamiliar with this field, I haphazardly built many things, and it hasn't been reviewed by experts for production readiness. Please note that I take no responsibility for any services or products created using this app.

Architecture Diagram

First, look at this architecture diagram I had Claude draw.

ig

Very complex, right...

It might actually be easier to understand by looking at the Docker Compose YAML file. Let's look at an excerpt.

version: "3.8"

services:
  # Init Keys - Key pair initialization (runs once at startup)
  init-keys:
  # Init Seeds - Seed data injection (runs once at startup)
  init-seeds:
  # Shopping Agent - User-facing agent
  shopping_agent:
  # Shopping Agent MCP - MCP tools (LangGraph node)
  shopping_agent_mcp:
  # Merchant Agent - Product search, CartMandate creation
  merchant_agent:
  # Merchant Agent MCP - MCP tools (LangGraph node)
  merchant_agent_mcp:
  # Merchant - CartMandate signing
  merchant:
  # Credential Provider 1 - WebAuthn verification, token issuance
  credential_provider:
  # Credential Provider 2 - WebAuthn verification, token issuance (multiple CP support)
  credential_provider_2:
  # Payment Processor - Payment processing
  payment_processor:
  # Payment Network - Payment network (Agent Token issuance)
  payment_network:
  # Frontend - Next.js
  frontend:
  # Meilisearch - Full-text search engine (for product search)
  meilisearch:
  # Jaeger - Distributed tracing backend (OpenTelemetry)
  jaeger:
  # Redis - KV store (temporary data, session management)
  redis:
Enter fullscreen mode Exit fullscreen mode

There's SA and MA, plus a frontend connecting SA and user. In some documentation, there might be a UA (User Agent) between SA and frontend, but this demo app directly connects SA and frontend.

Also, the tools that SA and MA use as agents to create Mandates and perform product searches are implemented as independent MCP servers as Docker Compose services, available via Streamable HTTP.

Additionally, CP, MPP, and stub services for the payment network are launched.

Each entity has its own DB, implemented with SQLite. When temporary KV store is needed, it accesses Redis.
Also, since a full-text search engine is suitable for product searches, Meilisearch is used.

Furthermore, Jaeger is introduced as the OpenTelemetry backend.

Public keys, private keys, and DIDs used for signing/verification by all services are created by initialization scripts. (I'll explain DID later.)

Also, from an LLMOps perspective, Langfuse has been introduced. This made building LangGraph graphs for SA and MA much easier. (Thanks, Langfuse!)

Preparation

First, let's start by creating a user. In this demo app, users are created with email and password. This becomes authentication information primarily used as HTTP Session for communication with SA. To put it more clearly, it's authentication for the shopping chatbot.

a

Separately, there's a passkey registration screen. This is used for attestation. Passkeys are managed not by SA but by CP. However, for simplicity, registration is done through the frontend that communicates with SA. (Normally, it should be registered from a different domain's screen.)

img

Furthermore, credit card information is registered. This is also registered with CP, not SA, but for the same reason as passkeys, it's registered from the same frontend. (So credit card information doesn't flow to SA. The flow where SA accesses credit card information will come later.)

img

Frontend Overview

The frontend consists of 3 screens: /chat, /merchant, and /payment-methods.

/chat provides the chat UI between user and SA. It's arguably the central screen of this demo app.

img

/payment-methods is a screen where you can edit, delete, or add credit cards you added earlier. Since it's under CP's jurisdiction, it should normally be a different frontend.

gg

/merchant is literally the shop-side admin screen. Product management, order history management, manual shop-side signing, etc., are possible. This should normally be a shop-dedicated frontend.

aa

Let's start chatting from /chat!

Type "Hello" to get started.

aaa

SA is LangGraph

Suddenly, but SA runs on LangGraph and Docker Model Runner. Docker Model Runner is a convenient Docker Desktop feature that lets you easily use local LLMs. The reason I chose Docker Model Runner is that you get a fully working environment with Docker Compose without API key setup (since it's a local LLM), and I'm broke being on parental leave so I can't casually hit LLM APIs for testing. (Very broke, please help.)

So, this demo is free to try! Instead, since we're using a local LLM (Qwen3), overall operation is sluggish. If that bothers you, please use a different model.

The SA's LangGraph graph looks like this:

graph

It's quite long + linear graph, but AP2 scenarios have signatures interspersed throughout, and the signature order is important, so the operation is quite complex. Therefore, nodes with autonomous LLM operation are limited to just a part (the blue node in the figure, where user Intent is extracted). It's basically a picture story graph with lots of fixed text.

Unfortunately, error recovery processing isn't implemented. So if an error occurs somewhere, it moves to the error node, and you need to type "Hello" to reset. (I'd like to refine this part of the graph too, but implementation time would run out.)

Intent Extraction & Intent Mandate Draft Creation (collect_intent)

Skipping the greeting node explanation, the important one is the collect_intent node. This crucial node receives the user's purchase intention in natural language and creates the Intent Mandate draft.

It extracts purchase intention, budget, keywords, product category, brand, etc.

aaa

This corresponds to Steps 1-3 in the official documentation sequence 7.1 Illustrative Transaction Flow.

Originally, looking at Step 3 in the official documentation sequence, it shows Confirm, so it seems necessary to ask the user for confirmation via popup or similar, but AP2's shopping experience can result in constant popups requesting user signatures if not designed well. So here, I avoid dedicated popups, assuming the user will naturally correct via chat typing like "No, what I want is~". (The graph isn't refined enough for correction input to actually work though...)

Looking at the demo app's operation logs, you can see that the user's Intent is extracted using LLM.

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: I want cute goods. Within 5000 yen
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': 'I want to buy cute goods', 'max_amount': 5000, 'keywords': ['cute', 'goods', 'stylish']}

ap2_shopping_agent         | {"timestamp": "2025-11-01T00:50:12.593597Z", "level": "INFO", "logger": "agent", "message": "[_create_intent_mandate] Reconstructed intent: I want to buy cute goods. Within 5000 yen", "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: I want to buy cute goods. Within 5000 yen", "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='I want to buy cute goods...', expiry=2025-11-01T01:50:12.593363Z", "module": "agent", "function": "_build_intent_mandate_from_session", "line": 1824}
Enter fullscreen mode Exit fullscreen mode

Finalizing Shipping Address (collect_shipping)

What's important in AP2 is finalizing the amount by the time of Cart Mandate exchange.

This is because it would be troublesome to have a last-minute surprise like "Actually, there's a 500 yen shipping fee on top of the total!" after proceeding with the transaction. Therefore, shipping address finalization is done early in the sequence. This corresponds to Step 5 in 7.1 Illustrative Transaction Flow.

If the SA already has shipping address information, this processing can be done on the SA side, so it's Optional in the sequence.

aaa

CP Selection (select_cp)

This is also Optional, but users can select from multiple CPs. The image is probably close to choosing whether to use Google Pay or PayPal. (Probably.)

Normally, the list of available CPs should be registered by the user in advance, but in this demo, it's fixed to choose from 2 CPs.

Selecting CP early is necessary because Step 6 in the official documentation sequence requires querying CP for payment methods. CP selection corresponds to Step 4 in the sequence.

The demo app implementation swaps Steps 4 and 5 from 7.1 Illustrative Transaction Flow to smoothly transition to the next payment method confirmation.

aaa

Payment Method Confirmation (get_payment_method)

Since the CP to use is decided, now we retrieve payment methods. Since we're using the Human Present transaction modality, the actual user decision on payment method comes later in the sequence, but it's retrieved internally at this timing.

For the same reason as shipping costs, fees, discounts, or loyalty information related to the selected payment method may vary, so the payment method candidates need to be presented to MA before Cart Mandate creation.

Looking at the demo app logs, while it doesn't appear on screen since it's internal operation, logs showing communication with the selected CP are output.

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 Card (****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']
Enter fullscreen mode Exit fullscreen mode

The key point is that while the response from CP contains card information, PCI data (card number itself, CVC, etc.) is not included.

By the way, I debated whether communication between SA and CP should be A2A or regular REST API. Here, it's implemented as a REST API with GET /payment-methods.

If we consider CP as an agentic entity, A2A seems appropriate, but there's a lot of WebAuthn communication with CP, and it feels closer to an e-commerce backend service rather than an agentic entity. However, using A2A would have the advantage of unifying signatures and DID-based mutual authentication, so maybe A2A is better after all. (I don't know.)

Intent Mandate Transmission (fetch_cart)

Now we finally send the Intent Mandate to MA. Transmission is done via A2A Message. This corresponds to Step 8 in the official documentation sequence.

The Intent Mandate + additional content sent via A2A looks like this: (Extracted from httpx debug logs when sending A2A messages)

{
  "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": "I want to buy cute goods. Within 5000 yen",
          "requires_refundability": false,
          "intent_expiry": "2025-11-01T01:50:12.593363Z",
          "created_at": "2025-11-01T00:50:12.593363Z"
        },
        "shipping_address": {
          "recipient": "Taro Yamada",
          "postal_code": "123-4567",
          "city": "Toshima-ku",
          "region": "Tokyo",
          "address_line1": "1-1-1 Kitaotsuka",
          "country": "Japan"
        }
      },
      "kind": null,
      "artifact": null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

First, let's look at the Intent Mandate itself, which is easiest to understand.

{
  "id": "intent_a97064a3",
  "type": "IntentMandate",
  "user_id": "usr_ee3014bbc51f4156",
  "user_cart_confirmation_required": true,
  "natural_language_description": "I want to buy cute goods. Within 5000 yen",
  "requires_refundability": false,
  "intent_expiry": "2025-11-01T01:50:12.593363Z",
  "created_at": "2025-11-01T00:50:12.593363Z"
},
Enter fullscreen mode Exit fullscreen mode

~Unfortunately, the official documentation itself doesn't have an Intent Mandate example~, but looking at the type definitions in the official GitHub reveals the specification. Let's look at the important fields.

(There is a description in the official documentation A2A Extension for AP2. My apologies for the oversight.)

user_cart_confirmation_required

Looking at the type definition:

If false, the agent can make purchases on the user's behalf once all purchase conditions have been satisfied. This must be true if the intent mandate is not signed by the user.

So, for Intent Mandates without user signatures, meaning Human Present transaction modalities, this needs to be true.

natural_language_description

In this demo app, since the SA doesn't do deep intent exploration, the user's input "I want to buy cute goods. Within 5000 yen" is entered as-is.

Looking at the type definition:

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.

So, originally the user's text itself shouldn't be entered directly, but rather the intent interpreted by SA enters in natural language form. Also, it's specified that confirmation should be requested before sending that intent.

intent_expiry

Intent Mandate has an expiration. This is to guarantee that the user's intent is definitely valid by setting a time limit. Human intentions also have a shelf life, in other words.

Here it's set to 1 hour after Intent Mandate creation, but for Human Not Present, it might be longer. (Or it might confirm with the user.)

shipping_address

Not part of Intent Mandate itself, but shipping_address is also communicated so MA can create a cart with exact pricing. Considering AP2's philosophy, payment information should also be communicated, but it's omitted in this demo.

That concludes the Intent Mandate explanation.

SA's Signature

Separate from the Intent Mandate itself, there's a Header field that follows the proof structure of W3C Verifiable Credentials, equivalent to SA's Verifiable Presentation (VP).

While there's no clear documentation in official sources, according to PayPal's blog:

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.

So I've created it to conform to that specification.

The signature follows the proof specification (RFC 8032 Ed25519).

The signing algorithm uses the pre-created SA's private key with ED25519 to sign the DataPart converted to canonicalized JSON (RFC8785).

The public key for signature verification is attached in publicKeyMultibase format. Since the value starts with z, we know it uses base58btc.

Also, nonce is generated to prevent signature replay and replacement.

Like this, with various processing including signatures, we can finally send the Intent Mandate to MA.

About VP

I briefly explained above, but Verifiable Presentation (VP) might be hard to understand. I didn't understand it until now either.

First, before discussing VP, we need to touch on the concept of VC (Verifiable Credential). Remember that there are 2 concepts: VC and VP.

VC proves "This person/thing is xx!"

For example, handling a university graduation certificate with VC. The university (issuer) cryptographically issues a certificate to the graduate (holder) that they graduated. So VC is like the graduation certificate itself.

VP proves "This is definitely a certificate about me being presented by me" when presenting this graduation certificate. It's achieved by adding the holder's signature to the VC.

With this mechanism, you can prove you graduated from university without having to briefly flash it for about 19.2 seconds.

MA is Also LangGraph

Now that the Intent Mandate has been sent to MA, MA moves to creating the shopping cart.

MA is also built with LangGraph + Docker Model Runner. This graph starts from where it receives the Intent Mandate from SA.

graph

Signature Verification Before Graph Starts

While not represented in the LangGraph graph (since it's done in the A2A message handler in the demo), we need to verify that the received A2A message was really sent from SA.

The verification steps basically do the reverse of what was done for signing. Let's look in detail.

Replay Attack Countermeasures

First, after checking (Validation) that the proof structure is correct, Timestamp verification is performed. The mechanism is simple—confirming whether the proof was created within 300 seconds = 5 minutes of the current time. This is a basic countermeasure against replay attacks.

Also, Nonce verification is performed. It checks its own KV store to confirm there's no same Nonce recently, that the same Nonce hasn't been used within a unit time (300 seconds = 5 minutes here), meaning no reuse of A2A communication.

Obtaining the Public Key

Next, we proceed to verify the message signature from the proof structure.

First, to verify the signature, we need SA's public key. AP2 recommends using DID (Decentralized Identifier) to obtain this public key.

The demo app supports using publicKeyMultibase in addition to DID. Since you might not be familiar with DID, let's look in detail.

What is DID?

DID (Decentralized Identifier) is literally a decentralized (not centralized) entity identification method.

With A2A communication having proof structures, signature verification can determine that communication completed without tampering using publicKeyMultibase.

However, the downside is that signatures alone can't tell whose key signed it.

We probably know it's from SA based on the destination, but we don't know details about what kind of agent SA is. DID can create a "context of trust."

With DID, you start by making a did.json JSON file accessible from each entity's .well-known/did.json. For example, SA's did.json looks like this:

{
  "@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 communication endpoint (user shopping proxy agent)"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

By obtaining the public key from did.json, we can show that the message signer is definitely SA. By the way, I don't really understand the standard specification for DID resolvers in AP2, so here I extract the Docker Network hostname shopping_agent from the DID ID did:ap2:agent:shopping_agent and access .well-known/did.json. (Imagining use alongside DNS.)

publicKeyMultibase

While probably not recommended by AP2, the demo app also supports signature verification using publicKeyMultibase included in the proof structure. I've made it prioritize DID, and if DID is the assumed architecture, including publicKeyMultibase in the proof structure might be unnecessary.

Verification

Now we proceed to verification. We know it was signed with ED25519 on the DataPart as canonicalized JSON (RFC8785), so we verify using the public key.

Once verification completes successfully, we know it's a Mandate definitely sent from SA and can proceed to the next processing.

Intent Extraction from Intent Mandate (analyze_intent)

Processing to extract keywords, prices, etc., from the received Intent Mandate's natural_language_description into DB-searchable information is done using LLM power.

In this node, a prompt like the following runs:

=========System Prompt==========
You are the Merchant Agent's intent analysis expert.
Analyze the user's IntentMandate (purchase intention) and extract the following information:

primary_need: The user's main request (concisely in 1 sentence)
budget_strategy: Budget strategy ("low"=lowest price priority, "balanced"=balanced type, "premium"=high quality priority)
key_factors: List of important factors (e.g.: ["quality", "price", "brand", "design"])
search_keywords: Keyword list for product search (3-5 items, words likely to be in product names)
Important:

Please always respond in JSON format.

=========User Prompt===========
Please analyze the following IntentMandate:

Natural language description: I want to buy cute goods. Within 5000 yen
Constraints: {}

Please respond in JSON format:
{
"primary_need": "...",
"budget_strategy": "low/balanced/premium",
"key_factors": ["...", "..."],
"search_keywords": ["...", "...", "..."]
}
Enter fullscreen mode Exit fullscreen mode

Since I'm using a local LLM while on parental leave due to lack of funds, I don't feel it's extracting Intent correctly, but with the latest models, more advanced extraction should be possible. The LLM response becomes JSON like the following:

{
    "role": "assistant",
    "content": {
        "primary_need": "I want to buy cute goods within 5000 yen",
        "budget_strategy": "low",
        "key_factors": [
            "price",
            "design"
        ],
        "search_keywords": [
            "cute",
            "goods",
            "accessories"
        ]
    },
    "additional_kwargs": {
        "refusal": null
    }
}
Enter fullscreen mode Exit fullscreen mode

Product Search via MCP Server (search_products)

AP2 is emphasized in the official documentation as something that extends, not competes with, A2A and MCP. So I decided to use MCP server for product search tool usage. (Streamable HTTP)

Since I wanted to use a full-text search engine for product search itself, I'm using the lightweight Meilisearch.

I'll skip the sequence explanation for Initialize processing with the MCP server, but you can see that when method tools/call runs from MA, it sends a search request like the following to the MCP server used by MA (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": [
          "cute",
          "goods",
          "affordable"
        ],
        "limit": 20
      }
    },
    "id": 387151
  }
}
Enter fullscreen mode Exit fullscreen mode

Results come back as follows. The product IDs in the product search DB match the product IDs in the RDB (SQLite) that connects later. So this ID is used for inventory checks, etc.

{
  "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\": \"Mugibo Acrylic Keychain\",\"description\": \"A cute Mugibo acrylic keychain. Attach it to bags and pouches.\",\"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\": \"Mugibo Clock\",\"description\": \"A cute wall clock with Mugibo design. Brightens up your room.\",\"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\": \"Mugibo Pouch\",\"description\": \"A cute pouch with Mugibo pattern. Use as a small items holder or pen case.\",\"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\": \"Mugibo Mug\",\"description\": \"A cute mug with Mugibo print. Makes your daily tea time enjoyable.\",\"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\": \"Mugibo Socks\",\"description\": \"Cute socks with a Mugibo accent. Soft and comfortable to wear.\",\"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\": \"Mugibo Plate\",\"description\": \"A ceramic plate with Mugibo in the center. Decorates your dining table cutely.\",\"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
}
Enter fullscreen mode Exit fullscreen mode

Inventory Check (check_inventory)

Once products are found, we query the inventory status again via MCP server. This is a query to the RDB (SQLite), but communication with the MCP server is done via Streamable HTTP just like the search_products node. We got the following inventory status for each product:

{
  "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
}
Enter fullscreen mode Exit fullscreen mode

All products have no inventory issues.

Cart Candidate Creation (optimize_cart)

Now we create the cart. Processing moves to the optimize_cart node where the local LLM creates 3 cart plans.

In optimize_cart, a prompt like the following runs:

========System Prompt=========
You are the Merchant Agent's cart optimization expert.
From the user's purchase intention and product list, suggest 3 optimal cart plans.

Include the following in each plan:

name: Plan name (including price or features, e.g.: "Budget Plan (5,000 yen)")
description: Plan description (1-2 sentences)
items: Product list [{"product_id": 123, "quantity": 1}, ...]
Plan design guidelines:

Plan 1: Most cost-effective plan within budget
Plan 2: High quality plan even if slightly over budget
Plan 3: Simple plan with only 1-2 products
Please always respond in JSON array format.

=======User Prompt============
Please suggest 3 cart plans under the following conditions:

User's request: I want to buy cute goods within 5000 yen
Budget strategy: low
Important factors: price, design
Budget limit: Not specified

Product list (6 items):
[
  {
    "id": "3446cca8-fe68-4354-a518-63eb3e47d27f",
    "name": "Mugibo Acrylic Keychain",
    "price_jpy": 800.0,
    "category": null,
    "inventory": 100
  },
  {
    "id": "1530a7db-6b7a-458e-b7b9-f510f6fdaa89",
    "name": "Mugibo Clock",
    "price_jpy": 3500.0,
    "category": null,
    "inventory": 30
  },
  {
    "id": "cf08568b-8115-461c-aa7e-5a2e07bf4476",
    "name": "Mugibo Pouch",
    "price_jpy": 950.0,
    "category": null,
    "inventory": 120
  },
  {
    "id": "e534386f-c89b-4548-9734-78bd34958f88",
    "name": "Mugibo Mug",
    "price_jpy": 1200.0,
    "category": null,
    "inventory": 80
  },
  {
    "id": "eb0e5de8-679d-4445-8e64-a1d4d40097fd",
    "name": "Mugibo Socks",
    "price_jpy": 850.0,
    "category": null,
    "inventory": 100
  },
  {
    "id": "f0f41c9f-a31d-4a88-a180-96149a9057fc",
    "name": "Mugibo Plate",
    "price_jpy": 1900.0,
    "category": null,
    "inventory": 70
  }
]

Please respond in JSON array format:
[
  {
    "name": "Plan name (including price)",
    "description": "Plan description",
    "items": [{"product_id": 123, "quantity": 1}]
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

Unfortunately, with the local LLM's performance (Qwen3:8b), it sometimes creates off-target plans (like a mysterious cart stuffed with clocks) or incomplete JSON...

When successful, JSON like the following is output:

[
    {
        "name": "Best Value Plan (1,650 yen)",
        "description": "A price-focused plan combining the cheapest keychain and socks. Balances cuteness and budget.",
        "items": [
            {
                "product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
                "quantity": 1
            },
            {
                "product_id": "c688d6ef-615f-43f7-87ce-05568ae4e63c",
                "quantity": 1
            }
        ]
    },
    {
        "name": "High Quality Plan (2,150 yen)",
        "description": "A plan emphasizing cuteness with pouch and mug combination. Good balance of quality and design.",
        "items": [
            {
                "product_id": "2faf8370-ada4-45d4-812c-ef5818d526b5",
                "quantity": 1
            },
            {
                "product_id": "1d9f08d9-51a9-491e-810b-f0e225ef4f59",
                "quantity": 1
            }
        ]
    },
    {
        "name": "Simple Plan (800 yen)",
        "description": "A single keychain plan recommended for those who want to keep the budget down. Maximizes cuteness with just one item.",
        "items": [
            {
                "product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
                "quantity": 1
            }
        ]
    }
]
Enter fullscreen mode Exit fullscreen mode

At this point, we're just creating cart candidates (Cart Candidate), so it's not in the formal Cart Mandate format. There's no shop side (Merchant) signature.

Cart Mandate Creation & Adding Shop Signature (build_cart_mandates)

We format each created cart candidate into Cart Mandate format. This operation is also done via MCP server.

Also, we have the shop side (Merchant) add their signature to the created Cart Mandate.

Since this processing runs in parallel for 3 carts, pasting the logs directly would be very hard to understand.

So I made a sequence diagram. (It's convenient that just pasting logs into Claude creates a sequence diagram...)

seq

One of the created Cart Mandates looks like this:

{
  "signed_cart_mandate": {
    "contents": {
      "id": "cart_70dda49a",
      "user_cart_confirmation_required": true,
      "payment_request": {
        "method_data": [],
        "details": {
          "id": "cart_70dda49a",
          "display_items": [
            {
              "label": "Mugibo Acrylic Keychain",
              "amount": {
                "value": 800.0,
                "currency": "JPY"
              },
              "refund_period": 2592000
            },
            {
              "label": "Consumption Tax (10%)",
              "amount": {
                "value": 80.0,
                "currency": "JPY"
              },
              "refund_period": 0
            },
            {
              "label": "Shipping",
              "amount": {
                "value": 500.0,
                "currency": "JPY"
              },
              "refund_period": 0
            }
          ],
          "total": {
            "label": "Total",
            "amount": {
              "value": 1380.0,
              "currency": "JPY"
            }
          }
        },
        "shipping_address": null
      },
      "cart_expiry": "2025-11-02T01:23:03.698740Z",
      "merchant_name": "Mugibo Shop"
    },
    "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": "Simple Plan (800 yen)",
      "cart_description": "A single keychain plan recommended for those who want to keep the budget down. Maximizes cuteness with just one item.",
      "raw_items": [
        {
          "product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
          "name": "Mugibo Acrylic Keychain",
          "description": "A cute Mugibo acrylic keychain. Attach it to bags and pouches.",
          "quantity": 1,
          "unit_price": {
            "value": 800.0,
            "currency": "JPY"
          },
          "total_price": {
            "value": 800.0,
            "currency": "JPY"
          },
          "image_url": null
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You can see that in addition to product information, there's a Base64 string in a field called merchant_authorization. Let's look at the details.

Merchant Authorization

I had no idea how to add the shop's signature, but looking at the official GitHub implementation example gave me roughly the answer.

A base64url-encoded JSON Web Token (JWT) that digitally

signs the cart contents, guaranteeing its authenticity and integrity:

  1. Header includes the signing algorithm and key ID.

  2. 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.

  1. 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.

So, just Base64 encode the JWT containing cart_hash. So we create a JWT containing the hash of the cart sent from MA, add a signature, and return it from the shop side (Merchant) entity. So if you verify merchant_authorization on https://www.jwt.io/ etc., it should be a valid JWT that indeed contains the cart hash. (Verification uses the shop's public key.)

aa

In this demo app, shop signing is automatic. In many AP2 commerce patterns, mechanical checks should be performed programmatically and proceed automatically. (Having shop staff intervene would also be a cause of lowering response times.)

However, for clarity in the demo app, by accessing the /merchant dashboard from the frontend and switching to manual signature mode, you can experience the shop-side cart approval.

img

This flow corresponds to Steps 10-11 of 7.1 Illustrative Transaction Flow.

Cart Ranking (rank_and_select)

Implementation is skipped in the demo app, but we rearrange (rerank) the ranking of selected carts to improve the user's shopping experience. After all, better products appearing first increases purchase motivation.

For example, cart order could be rearranged based on:

  • User preference match score
  • Inventory certainty
  • Price competitiveness

A2A Message Transmission

ap2.responses.CartCandidates

Now that the Cart Mandate is complete, we send it to SA via A2A Message.

In this demo, since there are multiple cart candidates, each Cart Mandate is stored in the A2A Message's DataPart.

(I'm not sure if this is correct according to AP2 specifications, but it seemed good for sending multiple candidates at once in an A2A Message...)

user_cart_confirmation_required

Like Intent Mandate, user_cart_confirmation_required is set to true, indicating operation under the Human Present transaction modality where user confirmation of the cart is required.

Also, characteristically, cart products follow W3C's Payment Request as noted in the official GitHub. PaymentMethodData and PaymentOptions are included following W3C Payment Request specifications.

_metadata

A problem here is that Cart Mandate itself has limited information that can be included about products. Only 4 fields can be defined for products: label, amount, pending, refund_period.

This might be sufficient as Cart Mandate, but considering actual UI/UX, we want to show users things like product descriptions and product images while shopping.

Therefore, they're recorded in metadata, not the Mandate itself.

cart_expiry

Also, like Intent Mandate, Cart Mandate has an expiration. This prevents situations where the user's Cart Mandate confirmation is delayed, too much time passes, and items become out of stock.

Complete with Signature

We add MA's signature to the A2A Message with this DataPart in proof structure, completing the Cart Mandate. This corresponds to Step 12 of official documentation Illustrative Transaction Flow.

Looking at httpx debug logs, you can see that Cart Mandate was indeed sent.

{
  "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": "Best Value Plan (4,500 yen)",
            "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": "Mugibo Acrylic Keychain",
                              "amount": {
                                "value": 800.0,
                                "currency": "JPY"
                              },
                              "refund_period": 2592000
                            },
                            {
                              "label": "Mugibo Pouch",
                              "amount": {
                                "value": 950.0,
                                "currency": "JPY"
                              },
                              "refund_period": 2592000
                            },
                            {
                              "label": "Mugibo Socks",
                              "amount": {
                                "value": 850.0,
                                "currency": "JPY"
                              },
                              "refund_period": 2592000
                            },
                            {
                              "label": "Mugibo Mug",
                              "amount": {
                                "value": 1200.0,
                                "currency": "JPY"
                              },
                              "refund_period": 2592000
                            },
                            {
                              "label": "Consumption Tax (10%)",
                              "amount": {
                                "value": 380.0,
                                "currency": "JPY"
                              },
                              "refund_period": 0
                            },
                            {
                              "label": "Shipping",
                              "amount": {
                                "value": 500.0,
                                "currency": "JPY"
                              },
                              "refund_period": 0
                            }
                          ],
                          "total": {
                            "label": "Total",
                            "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": "Taro Yamada",
                          "city": "Toshima-ku",
                          "region": "Tokyo",
                          "address_line1": "1-1-1 Kitaotsuka",
                          "country": "Japan"
                        }
                      },
                      "cart_expiry": "2025-11-02T12:14:19.255994Z",
                      "merchant_name": "Mugibo Shop"
                    },
                    "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": "Best Value Plan (4,500 yen)",
                      "cart_description": "A within-budget plan packed with maximum cute items. Total 4,500 yen for 4 items: keychain, pouch, socks, and mug.",
                      "raw_items": [
                        {
                          "product_id": "286acdd4-d1c1-4860-b06a-f87d2f916a8d",
                          "name": "Mugibo Acrylic Keychain",
                          "description": "A cute Mugibo acrylic keychain. Attach it to bags and pouches.",
                          "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": "Mugibo Pouch",
                          "description": "A cute pouch with Mugibo pattern. Use as a small items holder or pen case.",
                          "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": "Mugibo Socks",
                          "description": "Cute socks with a Mugibo accent. Soft and comfortable to wear.",
                          "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": "Mugibo Mug",
                          "description": "A cute mug with Mugibo print. Makes your daily tea time enjoyable.",
                          "quantity": 1,
                          "unit_price": {
                            "value": 1200.0,
                            "currency": "JPY"
                          },
                          "total_price": {
                            "value": 1200.0,
                            "currency": "JPY"
                          },
                          "image_url": null
                        }
                      ]
                    }
                  }
                }
              }
            ]
          },
          {
            "artifactId": "artifact_3ec2cecd",
            "name": "High Quality Plan (5,300 yen)",
            "parts": [...]
          },
          {
            "artifactId": "artifact_4ea2a38f",
            "name": "Simple Plan (3,500 yen)",
            "parts": [...]
          }
        ],
        "merchant_id": "did:ap2:merchant:mugibo_merchant",
        "merchant_name": "Mugibo Shop"
      },
      "kind": null,
      "artifact": null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Back to SA for Continued Processing

After receiving the Cart Mandate from MA, now the cart is finalized on the SA and user side, proceeding to payment processing.

To guarantee that the A2A Message was definitely sent from MA, signature verification from the proof structure is performed. (The method is exactly the same as explained before, so I'll skip the details.)

Once the A2A Message is successfully received, processing resumes from the next LangGraph node (select_cart).

Cart Selection (select_cart)

We've entered the node where the user selects a cart.

First, we verify the shop side's (Merchant's) JWT. This confirms it's a shopping cart that the shop has definitely approved.

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...
Enter fullscreen mode Exit fullscreen mode

The select_cart node has the user select a cart from the sent Cart Mandate candidates (Cart Candidates).

Shopping carts are displayed in a carousel format on the frontend.

aa

Once the user selects a cart, we proceed to the next node.

Cart Signature (submit_signature) But Actually Not Signing

Now we proceed to Cart Mandate selection and ~signature~ confirmation.

Since Cart Mandate's user_cart_confirmation_required was true, user confirmation is required.

A characteristic of AP2 is that this confirmation work uses a Trusted Device Surface.

What is Trusted Device Surface?

Trusted Device Surface literally translates to trusted device surface, or more understandably, a device surface where a human is definitely operating.

I'll admit I'm not well-versed in this area, so I'm afraid my terminology might be imprecise, but it refers to a device surface connecting trusted key material (private key within TPM/Secure Enclave) with human operation (biometric authentication, etc.).

In this case, we implement Trusted Device Surface using passkeys registered between CP and user.

(With passkeys, biometrics are also involved... But I'm not really sure if this is right... I need to properly study this area...)

User Doesn't Sign the Cart

I didn't realize this until I implemented it, but Step 20 of 7.1 Illustrative Transaction Flow says:

Redirect to trusted device surface { PaymentMandate, CartMandate }

I thought this meant adding user signature to Cart Mandate too, but this was incorrect.

First, the official GitHub Cart Mandate definition has a merchant_authorization field but no user_authorization field.

Also, if you add a user signature afterward to the JWT that the shop side (Merchant) signed, the JSON structure changes, naturally changing the hash value and invalidating the shop's signature.

This is obvious when you think about it, but you can't notice these things just reading the sequence, so I really felt implementation is what counts.

However, confirmed confirmation using user's Trusted Device Surface is still required for Cart Mandate, so passkey authentication is required during cart selection, and completing authentication shows SA that the user has definitely confirmed.

aa

(The screenshot says "cart signature" but it should correctly be "cart confirmation." I apologize for the correction.)

Passkey (WebAuthn) Authentication is Verified by CP

Passkey authentication is performed by CP, not SA.

aa

This flow is quite complex just following logs, so let's turn the behavior of SA's /cart/submit-signature endpoint directly into a sequence diagram. (This is more about implementing AI agents with AP2 rather than AP2 implementation itself, so you can skip if not needed.)

![https://i.imgur.com/NRw8CwW.png]

After cart selection, this demo app sends a special SSE event signature_request from SA.

img

After having the user confirm the Cart Mandate via popup, pressing the Authenticate with Passkey button requests WebAuthn from the browser via frontend.

Then, the Authenticate with Passkey popup appears (here from 1Password).

g

When communication reaches CP from SA and passkey authentication completes, you receive the authentication result along with the following 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"
}
Enter fullscreen mode Exit fullscreen mode

If the authentication result is OK, we can proceed to the next step (Payment Mandate creation). Also, the received WebAuthn attestation is used when creating the Payment Mandate.

This is just performing part of Step 20 (Cart Mandate) from official documentation Illustrative Transaction Flow.

Payment Method Selection (payment_method_select)

The order differs from official documentation Illustrative Transaction Flow, but we finalize the payment method retrieved from CP. This screen is the Step 16 behavior.

aa

Now the payment method is completely finalized (using VISA card this time).

Payment Method Tokenization

Since the payment method is finalized, we request payment token issuance from CP for the finalized payment method. This corresponds to Step 17 of official documentation Illustrative Transaction Flow.

{
  "type": "HTTP_REQUEST",
  "method": "POST",
  "url": "http://credential_provider:8003/payment-methods/tokenize",
  "headers": {},
  "body": {
    "user_id": "usr_cdb4ec851bcf4f73",
    "payment_method_id": "pm_f4745ec2"
  }
}
Enter fullscreen mode Exit fullscreen mode

A payment token linked to the payment method is returned from CP. This token is written to Payment Mandate, and subsequent payment processing proceeds using this token.

{
  "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
}
Enter fullscreen mode Exit fullscreen mode

By the way, the basic-card payment type was defined in W3C PaymentRequest API but is already slated for deprecation. For AP2, https://a2a-protocol.org/payment-methods/ap2-payment seems better. (Though I don't understand the specification yet...)

The link between payment token and actual payment information is managed in Redis KV. The demo app sets a 15-minute TTL.

Payment Mandate Creation & User Signature Creation

We're almost at the finale! Great work so far...! We create the Payment Mandate.

The final Payment Mandate looks like this. Let's look at each field.

{
  "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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

proof Structure

You're probably familiar with this flow by now, but since Payment Mandate is created by SA, like Intent Mandate, SA's signature is added in proof structure to send the A2A Message to MA. Since this has been explained before, I'll skip the details.

Total Amount (payment_details_total)

A characteristic of Payment Mandate is that since it's a payment delegation document, the focus should be on the total amount to pay. The philosophy is that cart details reference Cart Mandate.

Therefore, payment_details_total contains only the total amount, not cart details.

Payment Information (payment_response)

Payment information sets the temporary payment token obtained from Payment Method Tokenization in payment_response. This allows payment without actual card information flowing to entities involved in payment processing (SA, MA, MPP).

User Signature (user_authorization)

User signature is required for Payment Mandate.

I struggled considerably to research and implement how to create this. The thing is, for shop side (Merchant), I could imagine creating a JWT using pre-prepared public/private keys, but what are the user's public/private keys...

I couldn't figure this out and struggled greatly, but after various research, I learned that user's public/private key pair seems to use keys safely managed at the browser or OS level using WebAuthn (passkeys). (Probably... Please correct me if wrong.)

So please understand that in this demo, the user's public/private keys are realized using WebAuthn implementation.

WebAuthn Assertion

Let's briefly review WebAuthn (passkeys).

WebAuthn (passkeys) key pairs are generated on the user's device, and the private key never leaves the device's secure area (TPM, Secure Enclave, etc.).

This happens during passkey registration. Also, the public key is registered with CP in AP2, allowing other entities in the AP2 network to use it when verifying user signatures.

So, if we follow these steps:

  • Sign through WebAuthn using the private key in the device
  • Obtain the public key via CP and verify

we should be able to implement user signature.

Here's a sequence created from demo app logs implementing this approach. Let's go through it step by step.

aaa

Get Passkey Public Key from CP

When SA requests the passkey public key from CP, it returns the public key in COSE (CBOR Object Signing and Encryption) format.

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}
Enter fullscreen mode Exit fullscreen mode

I might expose my ignorance explaining COSE, but I understand it as a structure containing elliptic curve cryptography (P-256) public key X, Y coordinates converted to binary.

Converting to binary allows for lightweight handling.

User Authorization VP Creation

User Authorization VP (User Authorization Verifiable Presentation) cryptographically proves, with signature, that the user authorized that transaction (Cart Mandate + Payment Mandate).

The flow is quite complex & I'm not an expert, so bear with my explanation.

Get webauthn_challenge

First, authenticate with passkey (assertion), and from the resulting clientDataJSON, get the challenge (webauthn_challenge).

A challenge is a temporary random value generated by the server during authentication request, used to prevent replay attacks.

By performing verification including this value, we can confirm that the user actually signed in response to the server's request.

ap2_shopping_agent         | [2025-11-03 07:54:36,508] INFO in common.user_authorization: [create_user_authorization_vp] WebAuthn challenge from assertion: eyJtYW5kYXRlX2lk...
Enter fullscreen mode Exit fullscreen mode
Calculate Mandate Hash

Next, normalize the Mandate with RFC 8785 (JSON Canonicalization Scheme) and hash with SHA-256. This lets us check if the Mandate was tampered with.

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...
Enter fullscreen mode Exit fullscreen mode
Restore COSE Format Public Key

As mentioned earlier, the COSE (CBOR Object Signing and Encryption) format public key is returned from CP, which is hard to use as JWK (JSON Web Key), so we restore it to a usable form for subsequent processing.

(The internal processing is too complex, so I'll skip it. See the code for details.)

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)
Enter fullscreen mode Exit fullscreen mode
Generate Issuer JWT and Include cnf claim

With user as Issuer, generate a signed (actually not signed) JWT, and embed the user's public key from passkey in JWK (JSON Web Key) format in the cnf claim (Confirmation claim).

This allows MPP (shop's payment process) etc. to verify that this user definitely signed the JWT—the key relationship (Key Binding) is made explicit when combined with the Key-binding JWT created later.

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 JWT
Enter fullscreen mode Exit fullscreen mode

The JWT payload with cnf claim looks like this:

(iss and sub are the user's DID. The DID is generated from this demo app's user ID, but there probably needs to be uniqueness measures in the AP2 world.)

{
  "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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Also, this is quite detailed, but earlier I wrote:

With user as Issuer, generate a signed (actually not signed) JWT

There's nuance here—we don't actually sign with Issuer's public key!

You might think that's a lie, but there's a reason. WebAuthn API generates signatures only for specific challenges, so signing Issuer JWT is impossible.

If you try to sign Issuer JWT, you'd need signatures using keys generated outside WebAuthn, but that wouldn't be a user experience that completes with just passkeys.

So, as a compromise, we don't add a signature to Issuer JWT, and instead solve it by including sd_hash (Issuer JWT hash) in the Key-binding JWT explained next.

Therefore, the Issuer JWT header explicitly states no signature:

issuer_jwt_header = {
  "alg": "none",  # JWT standard compliant (RFC 7519): Explicitly indicates no signature
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode
Generate Key-binding JWT

Next, generate a JWT containing Mandate hash transaction data. This allows expressing that the user has definitely confirmed and approved the transaction involving Cart Mandate and Payment Mandate.

Including Cart Mandate and Payment Mandate hashes in transaction_data expresses that the user approved all these transactions.

Also, including assertion in the webauthn field allows verification just by looking at this Key-binding JWT.

(I don't know if this format is correct according to specification. Expert help... please.)

{
  "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"
}
Enter fullscreen mode Exit fullscreen mode
Treat WebAuthn Signature as KB-JWT Signature (Signing)

Now that we have Issuer JWT and Key-binding JWT, we sign the Key-binding JWT. The key point is performing WebAuthn signature. Pass the Key-binding JWT in Base64url format to the passkey authenticator and receive Signature from the authenticator. This is treated as the Key-binding JWT's signature.

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...
Enter fullscreen mode Exit fullscreen mode

At this point, like in the demo app, it's good to show users a popup requesting Payment Mandate signature.

aa

Assemble SD-JWT+KB (issuer_jwt~kb_jwt)

This differs from the SD-JWT+KB standard specification. I don't know if this implementation is correct, but given the constraints, this is where I ended up... Implementation is proceeding with completely my own interpretation.

Now that we have 2 JWTs, we assemble them in SD-JWT (Selective Disclosure JWT) and KB format.

SD-JWT is technology for selectively disclosing only parts of JWT, but here we attach KB as Disclosure and sign.

(I don't know if this format is correct from AP2 documentation, but probably... okay... I don't know...)

Complete! The generated output is Base64 encoded, so to show the User Authorization VP with Base64 decoded for clarity:

SD-JWT Header
{
  "alg": "none",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode
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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
KB Header
{
  "alg": "ES256",
  "typ": "kb+jwt"
}
Enter fullscreen mode Exit fullscreen mode
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"
  }
}
Enter fullscreen mode Exit fullscreen mode

We've successfully created the User Authorization VP. Great work!

Risk Assessment

The demo app has a simplified implementation, but when SA creates Payment Mandate, it needs to correctly convey the transaction circumstances to entities involved in payment, so SA performs risk assessment.

For example, it determines as a risk score whether the transaction falls within the amount range specified in user's Intent, whether the purchase is proceeding with the specified brand, whether there are issues with the specified card brand, etc.

However, since this area has unclear specifications, more research is needed.

Payment Mandate Transmission

Now we send the Payment Mandate to MA. The payload is what I posted earlier.

We add proof structure etc. to guarantee that it was sent from SA, and add User Authorization VP to guarantee the user approved.

Finally Payment Processing! (execute_payment)

Finally, payment processing. It's been so long... Is anyone still reading...?

It's done in LangGraph's execute_payment, but SA is mostly waiting after sending the request.

Most actual processing is performed by MPP (Merchant Payment Processor).

MPP Handles Payment Processing

Payment-related processing is handled by MPP, not MA. This is because AP2 adopts a role-based architecture, and MPP is solely responsible for payment-related matters.

However, since SA can't see MPP, SA first sends Payment Mandate to MA, and MA passes it through to MPP as-is.

(Verifying SA's signature is done by MA. After that, MA adds its own signature to the A2A message again and sends it to MPP.)

Proof & User Authorization VP Verification

MPP first validates the Payment Mandate. This time, in 2 stages:

  • Proof structure verification proving it was sent from MA
  • User Authorization VP verification

Proof structure verification proving it was sent from MA is the same process done elsewhere, so I'll skip the explanation.

User Authorization VP verification parses SD-JWT+KB, then verifies whether Cart Mandate and Payment Mandate hashes are correct, whether KB signed with WebAuthn is valid, etc.

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)
Enter fullscreen mode Exit fullscreen mode

MPP Payment Processing

Now we proceed with payment processing using Payment Mandate.

MPP Merchant Signature Verification

merchant_authorization JWT signature attached to Cart Mandate is also verified by MPP, verifying that this transaction has the shop's definite approval. After verifying merchant_authorization JWT and comparing Cart Mandate hash, we confirmed that Cart Mandate was indeed signed by the shop.

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}
Enter fullscreen mode Exit fullscreen mode

Verify Cart Mandate → Payment Mandate Chain is Correct

Next, verify whether Payment Mandate correctly references Cart Mandate and whether that chain is valid.

Since Cart Mandate's ID is recorded in Payment Mandate, check that the IDs match.

Verify Payment Token Validity

The payment method has already been tokenized by CP in the previous step. We need to query CP to verify that token's validity and ownership.

CP accesses actual payment information from the token and confirms that payer_id matches, hasn't expired, etc.

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
Enter fullscreen mode Exit fullscreen mode

Risk Assessment

Check the risk assessment score SA performed, and confirm whether to proceed with payment or perform additional assessment. I couldn't understand specific check items from documentation, but I assume check items vary based on AI agent involvement in payment and transaction modality.

Payment Processing

In this demo, the payment network is just a stub so I'll skip details, but payment processing is performed against the payment network.

Receipt Generation and Notification to CP & MA

Once payment completes successfully, MPP issues a receipt. It also notifies CP of payment completion, transaction status, and receipt information.

Furthermore, to notify the user as well, it notifies MA → SA → user.

All Processing Complete

Present the receipt to the user and all processing is complete.

aa

The receipt looks like this as a PDF.

aa

Conclusion

AP2 cleverly uses a mechanism called Mandate to achieve a safe purchase experience with AI agents. However, actually implementing it with reference to official documentation and GitHub repository, I realized that incredibly difficult cryptographic/signature technologies are used that are tough for a layperson, and there's a lot missing beyond Mandates when considering UI/UX. So understanding AP2 felt like mixed martial arts. Very difficult.

Also, Human Not Present specifications honestly have many areas not yet finalized. Rather, Human Not Present is arguably the essence of AP2.

I hope various reference implementations come out so we can proceed with these implementations!

Bonus - What is Mugibo Shop?

Mugibo Shop that appears in this demo doesn't exist in reality, of course.

But Mugibo exists in reality. She's our Mame Shiba (miniature Shiba Inu). Mame Shiba... supposed to be, but she got big and is now a Mame Shiba with one foot in Shiba Inu territory.

Mugi's Instagram

The products that appear during cart selection were created from Mugi's photos using Nano Banana.

mugino

References

Top comments (0)