This is for people building AI agents that can actually move money: infra engineers, payment/platform teams, and anyone wiring LLMs into checkout flows.
LLMs are non-deterministic. That is what makes them useful — they can reason about ambiguous input, handle varied phrasing, and generate contextually appropriate responses. But non-determinism becomes dangerous the moment money is involved.
If an AI agent is completing a checkout on a user's behalf, how do you prove that the exact cart the user confirmed is the exact cart that gets charged? How do you prevent the agent from — through a hallucination, an edge case, or an adversarial prompt — completing a payment for different items, a different quantity, or a different total?
AP2 (Agent Payment Protocol) answers this. It creates a signed mandate that cryptographically chains the user's intent, the cart, and the payment together. If anything changes between confirmation and charge, the mandate verification fails and the payment does not go through.
Think of AP2 as what 3-D Secure is to card payments: an extra cryptographic step that ties the user's explicit action to the charge — except instead of verifying the cardholder's identity, AP2 verifies the cardholder's intent.
THE PROBLEM WITH AI-DRIVEN PAYMENTS
Consider this scenario: a user asks an AI shopping assistant to "buy the shoes I was looking at earlier". The agent interprets this, finds the product, creates a checkout session, and charges the user's card. What could go wrong?
Attacks:
- A prompt injection changes the cart contents between confirmation and charge
- A compromised tool substitutes product IDs between the confirmation step and the payment call
Bugs and hallucinations:
- The agent retrieves the wrong product from conversation history
- The LLM generates a confirmation message for product A but the internal state references product B
- The user confirmed $89.99 but due to a currency conversion bug, $899.99 is charged
In a traditional checkout flow, a human reviews a cart and clicks a button. The system charges exactly what was displayed. There is no ambiguity.
In an AI-driven checkout, the agent mediates between the user's intent and the payment system. Without a verifiable record of what the user agreed to, you cannot prove that what was charged matched what was confirmed.
WHAT A MANDATE IS
In AP2, a mandate is a signed JSON record that captures exactly what the user agreed to at checkout. It contains:
- The user's intent (the action they asked for)
- The session ID of the UCP checkout session
- The exact cart items (product IDs, names, prices, quantities) at the time of confirmation
- The total in the smallest currency unit (integer cents, not a float)
- The user ID
- A timestamp
This payload is serialised and signed with a cryptographic key. The signature is stored alongside the order. Before the payment processor is called, the mandate is verified — the payload is reconstructed and the signature is checked. If the payload does not match the signature, the payment is blocked.
mandate_payload = {
"intent": "purchase",
"session_id": session.session_id,
"cart_items": [item.model_dump() for item in cart],
"total_cents": int(session.total * 100),
"currency": session.currency,
"user_id": user_id,
"timestamp": datetime.utcnow().isoformat(),
}
result = mandate_signer.sign(mandate_payload)
The total_cents is an integer, not a float. Floating-point arithmetic on money produces surprises. 89.99 * 100 in Python is 8998.999999999999. Integer cents eliminate this class of bug entirely (this also keeps you compatible with most payment APIs — Stripe, for example, expects amounts in the smallest currency unit).
THE THREE-LINK CHAIN
AP2's mandate creates a verifiable chain with three links:
Intent — What the user asked for. This comes from the classified intent in the orchestrator: "purchase", with the session ID identifying which checkout session this mandate covers.
Cart — What is being purchased. The exact cart at the moment the user said "confirm" — not the cart from the previous turn, not a reconstructed version from the LLM's memory, but the actual CartItem objects from the state at that instant.
Payment — How much is being charged. The total in integer cents, derived from the UCP checkout session. Not recalculated, not estimated — the number the merchant service produced.
If any link changes, the chain breaks. A mandate signed for $89.99 cannot authorise a charge for $899.99. A mandate signed for shoe-001 cannot authorise a charge for shoe-007.
HOW IT WORKS IN SHOPAGENT
The complete_checkout node handles both the UCP finalisation and the AP2 mandate:
async def complete_checkout(state):
session = state.get("checkout_session")
cart = state.get("cart", [])
user_id = state.get("user_id", "")
result = await ucp_complete_checkout(
session_id=session.session_id,
shipping_address=session.shipping_address,
payment_method=session.payment_method or "mock_card",
user_id=user_id,
)
Inside ucp_complete_checkout, the merchant service calls the mandate signer before it calls the payment processor:
# Build and sign the mandate
mandate_payload = {
"intent": "purchase",
"session_id": session_id,
"cart_items": cart_items,
"total_cents": total_cents,
"currency": currency,
"user_id": user_id,
}
mandate_result = mandate_signer.sign(mandate_payload)
# Only call the payment processor if mandate is valid
if mandate_signer.verify(mandate_payload, mandate_result):
payment_result = await payment_processor.charge(
amount_cents=total_cents,
currency=currency,
payment_method=payment_method,
metadata={"mandate_signature": mandate_result.signature},
)
The mandate signature is stored in the payment metadata. Every successful charge has a verifiable record of exactly what the user agreed to.
SWAPPABLE SIGNERS: ED25519 TO VAULT
The MandateSigner interface in ShopAgent is abstract:
class MandateSigner(ABC):
@abstractmethod
def sign(self, payload: dict) -> SignatureResult: ...
@abstractmethod
def verify(self, payload: dict, result: SignatureResult) -> bool: ...
@abstractmethod
def public_key_pem(self) -> str: ...
The default implementation uses Ed25519 — fast, small signatures, widely supported. Set MANDATE_SIGNER=ed25519 and the service generates an ephemeral key on startup (suitable for demos and development, where cross-restart verification is not needed).
For production, you would implement VaultMandateSigner that uses HashiCorp Vault's transit secrets engine, or KMSMandateSigner for AWS KMS or Google Cloud KMS. The keys are managed, rotated, and audited by the secrets manager. The orchestrator code does not change.
# factory.py
def get_mandate_signer() -> MandateSigner:
signer_type = os.getenv("MANDATE_SIGNER", "ed25519")
if signer_type == "vault":
return VaultMandateSigner(vault_addr=os.getenv("VAULT_ADDR"))
if signer_type == "kms":
return KMSMandateSigner(key_id=os.getenv("KMS_KEY_ID"))
return Ed25519MandateSigner()
The MANDATE_PRIVATE_KEY_B64 environment variable lets you provide a persistent Ed25519 key for environments where mandates need to be verifiable across restarts. Without it, each startup generates a fresh key.
If you are running a POC that touches real cards, start with Vault or KMS — not ephemeral Ed25519 keys. Ephemeral keys are fine for development, but any environment where real money moves needs managed key rotation and audit logs from day one.
THE AUDIT TRAIL
Every mandate produces a SignatureResult:
@dataclass(frozen=True)
class SignatureResult:
signature: str # base64-encoded signature bytes
algorithm: str # "Ed25519"
key_id: str # identifier for the signing key
signed_at: datetime
signer: str # "ed25519", "vault", "kms"
This goes into the order record. If a user disputes a charge, you can reconstruct the mandate payload from the order data, re-verify the signature, and prove exactly what was authorised at the time.
This is the kind of audit trail that financial regulators expect and that payment processors look for when evaluating risk.
WHAT WE'D DO DIFFERENTLY
Mandate expiry — The current mandate has no expiry. A mandate signed for a session that was created hours ago and never completed could theoretically be replayed. A valid_until field with a short window (5–10 minutes) would close this.
Mandate revocation — If the user abandons checkout and returns to it later, the old mandate should be invalidated. A revocation list or a nonce-based one-time-use scheme would enforce this.
Mandate as the authorisation token — Currently, the mandate is a verification step that runs before the payment call. In a full AP2 implementation, the mandate itself would be the token passed to the payment provider — the provider would verify it before accepting the charge. This requires payment provider cooperation but eliminates the gap between mandate creation and payment execution.
THE TAKEAWAY
AP2 is not a feature. It is a safety guarantee. Any AI agent that can authorise a payment without a verifiable, signed mandate of what the user agreed to has a security gap — not an implementation gap, but an architectural one.
The user said "confirm". The cart contained these exact items. The total was this exact number. The mandate proves all three, cryptographically, before any money moves. That is the contract AP2 provides.
The ShopAgent demo is live at https://shop-agent.agilecreativeminds.nl. See the demo showcase or follow the demo walkthrough. Built by Agile Creative Minds.