Docs menu
Guides › Examples

A2A + AP2 example: callee-seals capsule

How an A2A callee seals a neutral verifiable record on every payment action. The AP2 CartMandate is the “may” — the capsule is the “did.” Two protocols, one seam.

The compose pattern

A2A (Agent-to-Agent protocol) and AP2 (A2A Payment Profile) handle authorization — who permitted this payment, for how much, to whom. They do not handle the neutral witnessed record of what the agent actually did. That’s where the capsule fits: it binds the authorization to the outcome in a single sealed, content-private, independently verifiable record.

LayerProtocolWhat it shows
AuthorizationAP2 CartMandateCaller permitted this payment (for up to X, to payee Y)
Neutral recordAAC capsuleCallee sealed what actually happened — approved or refused
Independent proofSCITT receiptThird-party log the capsule was registered; verifiable without trusting the callee
Neither the AP2 mandate nor the payment details travel in the capsule — only their SHA-256 digests. Content stays local; the capsule is content-private by construction.

Field mapping

The capsule binds two things: the authorization that permitted the action (the “may”) and the outcome that resulted (the “did”).

AP2 CartMandate (task_id + session_id + mandate.*) │ ▼ SHA-256(JCS) capsule.agent_input_digest ← the "may" (authorization) Payment outcome (payment_id, amount, payee, status) │ ▼ SHA-256(JCS) capsule.effect.response_digest ← the "did" (outcome) capsule.effect.type = "send_payment" capsule.effect.status = "confirmed" | "dispatched" | "planned" A2A Task state completed → verdict_class: "executed" failed → verdict_class: "blocked" over limit → verdict_class: "blocked", effect.status: "planned"

The effect.status: "planned" value is the auditable record that a payment was evaluated and refused — it never reached the payment network, and the capsule is the only sealed evidence that it was attempted.

Three capsule outcomes

Scenario A — payment approved

Two capsules seal the approved path: one at dispatch (before the payment completes) and one when the network confirms. The second chains to the first via chain.parent_capsule_id.

# Step 1: seal immediately at dispatch — before payment completes
dispatched = seal(
    task.input_dict(),   # AP2 CartMandate → agent_input_digest
    action="send_payment",
    operator="action-state-group",
    developer="ap2-payment-agent@v1",
    agent_output=None,
    verdict="executed",
    effect={"type": "send_payment", "status": "dispatched"},
    anchor=True,
)
# dispatched.capsule_id  =  SHA-256(JCS(capsule_body))

# Step 2: execute the payment (Stripe or sandbox)
result = execute_payment(mandate, amount)

# Step 3: seal the confirmation — chains to dispatched
confirmed = seal(
    task.input_dict(),   # same mandate → same digest
    action="send_payment",
    # …same operator / developer…
    agent_output=result.as_dict(),   # payment outcome → response_digest
    verdict="executed",
    effect={"type": "send_payment", "status": "confirmed"},
    confirms=dispatched.capsule_id,  # chain → dispatch
    anchor=True,
)

Scenario B — over-limit refusal

One capsule seals the refusal. effect.status: "planned" is the invariant that the payment was evaluated but never dispatched. The payment network never sees it; the capsule is the only sealed evidence.

refusal = seal(
    task.input_dict(),   # the mandate we evaluated
    action="send_payment",
    # …operator / developer…
    agent_output={
        "refusal_reason": "over_agent_spend_limit",
        "requested": amount.as_dict(),
        "agent_limit": "1000.00",
    },
    verdict="blocked",
    decision="reject",
    effect={"type": "send_payment", "status": "planned"},  # ← NEVER dispatched
    anchor=True,
)

What a sealed capsule looks like

Both scenarios produce capsules with this shape. Only digests travel — the mandate and payment details stay in your process.

{
  "capsule_id": "cb96…714c",         # SHA-256(JCS(this body)) — the seal
  "format_version": "2",
  "action": "send_payment",
  "operator": "action-state-group",
  "developer": "ap2-payment-agent@v1",
  "timestamp": "2026-06-29T20:30:00Z",
  "agent_input_digest": "a3f1…e842",  # SHA-256(JCS(A2A Task + CartMandate))
  "effect": {
    "type": "send_payment",
    "status": "confirmed",              # dispatched | confirmed | planned
    "response_digest": "2f4d…3949"    # SHA-256(JCS(PaymentResult))
  },
  "disposition": {
    "decision": "accept",
    "verdict_class": "executed",       # executed | blocked
    "approver": "policy"
  },
  "chain": {                            # confirm capsule only
    "parent_capsule_id": "cb96…714c",
    "relation": "confirms"
  }
}

Run it yourself

The full runnable example is in capsule-emit/examples/a2a-ap2/. No API keys needed — runs in sandbox mode by default.

pip install "capsule-emit"
git clone https://github.com/action-state-group/capsule-emit
cd capsule-emit
python examples/a2a-ap2/run_example.py

Expected output:

=== A2A callee-seals capsule example — AP2 payment headline ===
Ledger: /tmp/.../a2a_ap2_ledger.jsonl
Mode:   sandbox (DRY_RUN)

  ✓  dispatched capsule_id: cb96…714c
  ✓  confirmed capsule_id:  00fe…034
     chains → dispatched: cb9678f17cc3d96c…
  ✓  SCITT witnessed:  dispatch leaf=5, confirm leaf=4

  ✓  refusal capsule_id: 16be…3b3
     verdict_class:  blocked
     effect.status:  planned  ← 'planned' = NEVER dispatched
  ✓  SCITT witnessed: leaf=6

  [0] VALID  ✓  cb9678f17cc3d96c56cd…  verdict=executed effect.status=dispatched
  [1] VALID  ✓  00feb2159a5ed32d903e…  verdict=executed effect.status=confirmed
  [2] VALID  ✓  16be4f7951c0c83274a9…  verdict=blocked  effect.status=planned

  All capsules VALID ✓
Every capsule is witnessed by the public SCITT transparency log. After running the example, visit the log explorer to see your capsule digests in the tree.

Verify

# digests, chain links and producer signatures (offline — no network needed)
capsule-emit verify --store /path/to/a2a_ap2_ledger.jsonl

# Inspect the live log
curl https://witness.agentactioncapsule.org/anchor/transparency-log
Content-private by construction. The AP2 CartMandate, payee details, amounts, and payment outcomes never leave your process — only their SHA-256 digests travel in the capsule and to the witness. Pass salt_digests=False only when you need deterministic digests for cross-system comparison (as in this example for reproducibility).

What this is not

  • Not a standalone A2A adapter — agentgateway proxies A2A at the wire; a capsule adapter there seals every A2A action at a single policy point.
  • Not an AP2 adapter — the capsule maps to AP2 concepts (mandate → digest), but does not implement or replace AP2 authorization.
  • Not a payment library — a2a_sandbox.py is a thin demo shim, not a payment integration.