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 anchored 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.
| Layer | Protocol | What it proves |
|---|---|---|
| Authorization | AP2 CartMandate | Caller permitted this payment (for up to X, to payee Y) |
| Neutral record | AAC capsule | Callee sealed what actually happened — approved or refused |
| Independent proof | SCITT receipt | Third-party log the capsule was registered; verifiable without trusting the callee |
Field mapping
The capsule binds two things: the authorization that permitted the action (the “may”) and the outcome that resulted (the “did”).
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 = emit(
action="send_payment",
operator="action-state-group",
developer="ap2-payment-agent@v1",
agent_input=task.input_dict(), # AP2 CartMandate → agent_input_digest
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 = emit(
action="send_payment",
# …same operator / developer…
agent_input=task.input_dict(), # same mandate → same digest
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 = emit(
action="send_payment",
# …operator / developer…
agent_input=task.input_dict(), # the mandate we evaluated
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 anchored: dispatch leaf=5, confirm leaf=4
✓ refusal capsule_id: 16be…3b3
verdict_class: blocked
effect.status: planned ← 'planned' = NEVER dispatched
✓ SCITT anchored: 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 ✓
Verify
# Class-1 verification (offline — no network needed)
agent-action-capsule verify --store /path/to/a2a_ap2_ledger.jsonl
# Inspect the live log
curl https://anchor.agentactioncapsule.org/anchor/transparency-log
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.pyis a thin demo shim; the Stripe integration is in capsule-gate-hermes.