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

LayerProtocolWhat it proves
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 = 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 
Every capsule is anchored to the public SCITT transparency log. After running the example, visit the log explorer to see your capsule digests in the tree.

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
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 anchor. 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 librarya2a_sandbox.py is a thin demo shim; the Stripe integration is in capsule-gate-hermes.