Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Sign and Verify AP2 Mandates

PaymentsIntermediate25 min

AP2 (Agent Payments Protocol) is a mandate-based payment protocol for autonomous agents. A human signs an intent mandate bounding what the agent may spend; the agent later signs a cart mandatebinding a concrete cart to that intent. Both are wrapped in W3C-aligned Verifiable Digital Credentials (VDC) and verified against Tenzro's TDIP identity layer.

What You'll Build

  • A signed intent mandate issued by a human DID
  • A signed cart mandate issued by the agent's machine DID
  • Verification via tenzro_ap2VerifyMandate and tenzro_ap2ValidateMandatePair
  • Delegation-scope wiring so the agent is actually allowed to present AP2 carts

Prerequisites

Step 1: Prepare Delegation Scope

The agent cannot present a cart mandate unless its TDIP delegation includes AP2:

# The agent's machine DID must have AP2 in its allowed_payment_protocols
# before a cart mandate will be accepted.
tenzro identity update-delegation \
  --did did:tenzro:machine:c3d4... \
  --allowed-payment-protocols mpp,x402,ap2 \
  --max-transaction-value 50.00 \
  --max-daily-spend 200.00

Step 2: Issue the Intent Mandate

The human signs a VDC with kind: "intent". Spending is bounded by max_amount, restricted to allowed_merchants/allowed_categories, and time-bounded by expires_at.

{
  "vdc": {
    "id": "mandate-01HQ...",
    "kind": "intent",
    "issuer": "did:tenzro:human:a1b2...",
    "subject": "did:tenzro:machine:c3d4...",
    "issued_at": "2026-04-20T12:00:00Z",
    "expires_at": "2026-04-20T14:00:00Z",
    "claims": {
      "max_amount": "50.00",
      "currency": "USDC",
      "purpose": "book a flight to SFO under 50 USDC",
      "allowed_merchants": ["did:tenzro:machine:air-shop"],
      "allowed_categories": ["travel.flights"]
    }
  },
  "signature": "base64(Ed25519(canonical(vdc)))",
  "signer_did": "did:tenzro:human:a1b2...",
  "alg": "EdDSA"
}

Step 3: Issue the Cart Mandate

Once the agent has picked a merchant and line items, it signs a cart mandate referencing the intent via parent_id. The issuer on the cart must match the subjectof the parent intent — this is what binds the agent to the original human authorization.

{
  "vdc": {
    "id": "mandate-01HR...",
    "kind": "cart",
    "issuer": "did:tenzro:machine:c3d4...",
    "subject": "did:tenzro:machine:air-shop",
    "parent_id": "mandate-01HQ...",
    "issued_at": "2026-04-20T12:05:00Z",
    "expires_at": "2026-04-20T12:10:00Z",
    "claims": {
      "merchant_did": "did:tenzro:machine:air-shop",
      "line_items": [
        { "sku": "SFO-Y-MON-AM", "qty": 1, "unit_price": "42.00" }
      ],
      "total_amount": "42.00",
      "currency": "USDC",
      "payment_method": "x402"
    }
  },
  "signature": "base64(Ed25519(canonical(vdc)))",
  "signer_did": "did:tenzro:machine:c3d4...",
  "alg": "EdDSA"
}

Step 4: Verify a Single Mandate

tenzro_ap2VerifyMandate({
  "vdc": { /* intent envelope */ }
})
  -> {
    valid: true,
    mandate_id: "mandate-01HQ...",
    kind: "intent",
    signer_did: "did:tenzro:human:a1b2...",
    alg: "EdDSA"
  }

Step 5: Validate the Pair

This is the call the merchant or facilitator makes before accepting payment:

tenzro_ap2ValidateMandatePair({
  "intent_vdc": { /* intent envelope */ },
  "cart_vdc":   { /* cart envelope  */ }
})
  -> {
    valid: true,
    intent_id: "mandate-01HQ...",
    cart_id:   "mandate-01HR...",
    total_within_intent: true,
    merchant_allowed:    true,
    agent_matches:       true,
    time_window_valid:   true
  }

The validator enforces five checks:

  1. Both VDC signatures verify against the resolved DID documents
  2. Intent issuer is a human DID, cart issuer is the machine DID named as the intent's subject
  3. Cart total_amount is within intent max_amount
  4. Cart merchant_did is in allowed_merchants
  5. Both mandates are within their expires_at windows

Step 6: Protocol Info

tenzro_ap2ProtocolInfo()
  -> {
    version: "ap2/1.0",
    supported_algs:  ["EdDSA"],
    supported_kinds: ["intent", "cart"],
    chain_id: 1337
  }

CLI

# Verify a single mandate (intent or cart)
tenzro ap2 verify --vdc-file intent.json

# Validate an intent/cart pair
tenzro ap2 validate-pair \
  --intent intent.json \
  --cart cart.json

# Protocol metadata
tenzro ap2 info

TypeScript SDK

import { TenzroClient } from "@tenzro/sdk";

const client = new TenzroClient();

const intent = await loadJson("intent.json");
const cart   = await loadJson("cart.json");

const pair = await client.ap2.validateMandatePair({ intent_vdc: intent, cart_vdc: cart });

if (!pair.valid) {
  throw new Error("AP2 validation failed");
}

// Safe to now settle the cart via the chosen payment rail (x402, MPP, etc.)
await client.payments.payX402({ /* ... */ });

AP2 vs MPP vs x402. MPP is for long-lived streaming sessions (e.g., per-token AI billing). x402 is for stateless one-shot payments. AP2 sits above both: the VDC chain authorizes what the agent may spend, and MPP/x402/Tempo handles how that charge is actually settled.

What You Learned

Next Steps

  • See the x402 tutorial for the stateless settlement rail often paired with AP2
  • See the Agentic Wallet tutorial to set up DIDs and delegation
  • Read the AP2 reference docs for the full RPC surface