Agent-to-Agent (A2A)
A2A — one agent transacting with another — is the same primitive as human-to-agent. Same
DelegationCert, same ProofBundle, same verifier algorithm. The only thing that changes is
who’s at the root of the chain.
Human-to-agent Agent-to-agent ────────────── ──────────────
Alice signs Alice signs │ │ ▼ ▼ Cert: Alice → Agent-A Cert: Alice → Agent-A │ │ │ │ then Agent-A signs: │ │ │ ▼ │ Cert: Agent-A → Agent-B │ │ ▼ ▼ Agent-A presents Agent-B presents bundle bundle with with TWO certs in chain ONE cert in chain [Alice→A, A→B] │ │ ▼ ▼ Verifier accepts Verifier walks the chain and intersects scopesThree patterns
Section titled “Three patterns” Pattern 1: Sub-delegation Pattern 2: Mutual auth ───────────────────────── ─────────────────────
Agent-A hires Agent-B for a Two agents need to specific scoped task. Agent-B transact with each other. acts under Agent-A's authority, Both present bundles to transitively under Alice's. the other; both verify.
Pattern 3: Transaction receipt ──────────────────────────────
Agent-B accepts the proof from Agent-A and emits a signed receipt that records what happened. Useful for audit trails of agent-to- agent commerce.Pattern 1: Sub-delegation
Section titled “Pattern 1: Sub-delegation”The classic case. Alice’s calendaring agent (Agent-A) needs to hire a travel-booking agent (Agent-B) to handle the actual reservation. Alice doesn’t know Agent-B at all — she trusts Agent-A to scope it appropriately.
Alice │ │ scope: [calendar:write, commerce:purchase, payment:approve($500)] │ ▼ Agent-A (calendaring) │ │ scope: [commerce:purchase, payment:approve($500)] │ — Agent-A drops calendar:write since Agent-B doesn't need it │ ▼ Agent-B (travel booking) │ │ Agent-B presents bundle to the airline API: │ delegations: [Alice→A, A→B] │ challenge_sig signed by Agent-B │ ▼ Airline API ✓ Both signatures valid ✓ Chain well-formed (Alice→A's subject == A→B's issuer) ✓ Effective scope = [commerce:purchase, payment:approve($500)] ✓ Cert not expired, not revoked ✓ Challenge fresh → accept the purchaseIssuing the sub-delegation
Section titled “Issuing the sub-delegation”// Agent-A (calendaring) sub-delegates to Agent-B (travel booking)subCert := &ratify.DelegationCert{ Version: 1, IssuerID: agentA.ID, IssuerPubKey: agentA.PublicKey, SubjectID: agentB.ID, SubjectPubKey: agentB.PublicKey, Scope: []string{"commerce:purchase", "payment:approve"}, IssuedAt: time.Now().Unix(), ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // shorter than parent}ratify.IssueDelegation(subCert, agentAPriv)
// Agent-B builds a bundle with BOTH certs in the chainbundle := &ratify.ProofBundle{ AgentID: agentB.ID, AgentPubKey: agentB.PublicKey, Delegations: []*ratify.DelegationCert{originalCert, subCert}, Challenge: challengeFromVerifier, ChallengeAt: time.Now().Unix(), ChallengeSig: signedByAgentB,}const subCert: DelegationCert = { cert_id: crypto.randomUUID(), version: PROTOCOL_VERSION, issuer_id: agentA.id, issuer_pub_key: agentA.public_key, subject_id: agentB.id, subject_pub_key: agentB.public_key, scope: ["commerce:purchase", "payment:approve"], issued_at: Math.floor(Date.now() / 1000), expires_at: Math.floor(Date.now() / 1000) + 24 * 3600, signature: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() },};await issueDelegation(subCert, agentAPrivateKey);
const bundle: ProofBundle = { agent_id: agentB.id, agent_pub_key: agentB.public_key, delegations: [originalCert, subCert], challenge: challengeFromVerifier, challenge_at: Math.floor(Date.now() / 1000), challenge_sig: await signChallenge(challenge, challengeAt, agentBPriv),};sub_cert = DelegationCert( cert_id=secrets.token_hex(8), version=PROTOCOL_VERSION, issuer_id=agent_a.id, issuer_pub_key=agent_a.public_key, subject_id=agent_b.id, subject_pub_key=agent_b.public_key, scope=["commerce:purchase", "payment:approve"], issued_at=int(time.time()), expires_at=int(time.time()) + 24 * 3600,)issue_delegation(sub_cert, agent_a_priv)
bundle = ProofBundle( agent_id=agent_b.id, agent_pub_key=agent_b.public_key, delegations=[original_cert, sub_cert], challenge=challenge_from_verifier, challenge_at=int(time.time()), challenge_sig=sign_challenge(challenge, challenge_at, agent_b_priv),)let mut sub_cert = DelegationCert { version: PROTOCOL_VERSION, cert_id: uuid::Uuid::new_v4().to_string(), issuer_id: agent_a.id.clone(), issuer_pub_key: agent_a.public_key.clone(), subject_id: agent_b.id.clone(), subject_pub_key: agent_b.public_key.clone(), scope: vec!["commerce:purchase".into(), "payment:approve".into()], issued_at: now, expires_at: now + 24 * 3600, ..Default::default()};issue_delegation(&mut sub_cert, &agent_a_priv)?;
let bundle = ProofBundle { agent_id: agent_b.id.clone(), agent_pub_key: agent_b.public_key.clone(), delegations: vec![original_cert, sub_cert], challenge: challenge_from_verifier, challenge_at: now, challenge_sig: sign_challenge(&challenge, now, &agent_b_priv)?,};Verifier behavior with a chain
Section titled “Verifier behavior with a chain”The verifier walks the chain from root to leaf:
For chain [Alice→A, A→B]:
1. Both signatures on Alice→A verify with Alice's pubkey 2. Both signatures on A→B verify with Agent-A's pubkey (whose identity is established by Alice→A.subject_pub_key) 3. Alice→A.subject_id == A→B.issuer_id (chain well-formed) 4. Both certs are within their expiry windows 5. Challenge signature verifies with Agent-B's pubkey (whose identity is established by A→B.subject_pub_key) 6. Effective scope = scope[Alice→A] ∩ scope[A→B] = [calendar:write, commerce:purchase, payment:approve($500)] ∩ [commerce:purchase, payment:approve] = [commerce:purchase, payment:approve] 7. required_scope ("commerce:purchase") ∈ effective scope ✓ 8. No constraints violated 9. No cert in chain is revoked
→ validThe intersection is strict: Agent-B never gets more than Agent-A was given, which is never more than Alice gave. This is the structural invariant.
Pattern 2: Mutual authentication
Section titled “Pattern 2: Mutual authentication”Two agents need to trust each other. Each presents a bundle; each runs the verifier on the other’s bundle.
┌───────────────────────────────────────────────┐ │ │ │ Agent-A Agent-B │ │ │ │ ───── here's my bundle ─────▶ │ │ ◀──── verify_bundle(bundle_a) ── │ │ ◀──── here's my bundle ───── │ │ ──── verify_bundle(bundle_b) ─▶ │ │ │ │ Both ✓ → trust established │ │ Either ✗ → abort, log, no transaction │ │ │ └───────────────────────────────────────────────┘The challenge in each direction can use a fresh nonce per side so neither agent can replay the other’s bundle. A typical handshake:
1. Agent-A → Agent-B: nonce_a (32 random bytes) 2. Agent-B → Agent-A: nonce_b, bundle_b (Agent-B signed nonce_a in challenge_sig) 3. Agent-A: verify_bundle(bundle_b, challenge_was: nonce_a) → must pass 4. Agent-A → Agent-B: bundle_a (Agent-A signed nonce_b in challenge_sig) 5. Agent-B: verify_bundle(bundle_a, challenge_was: nonce_b) → must pass 6. Both trust each other; proceedThis is two challenge-response rounds, one per direction. Both agents prove they hold the live key for their respective identities.
Pattern 3: Transaction receipt (v1.1)
Section titled “Pattern 3: Transaction receipt (v1.1)”After a successful A2A transaction, the receiving agent can emit a signed receipt recording what happened. Useful for audit trails of agent-to-agent commerce — proves that “yes, Agent-B accepted Agent-A’s proof for X scope at time T.”
{ "version": 1, "transaction_id": "tx-abc-123", "verifier_id": "<Agent-B identity>", "presenter_id": "<Agent-A identity>", "bundle_hash": "<sha256 of the verified bundle>", "granted_scope": ["commerce:purchase", "payment:approve"], "action": "purchase_executed", "timestamp": 1799999999, "signature": { ed25519: "...", ml_dsa_65: "..." } }The receipt is signed by the verifier (Agent-B). Anyone with Agent-B’s public key can later audit: “did Agent-B in fact accept Agent-A’s proof and execute this transaction?”
Receipts are v1.1 — not yet stable. They’re optional; nothing breaks if you don’t emit them. See docs/TRANSACTION_RECEIPTS.md in the protocol repo for the v1.1 receipt envelope design.
Common pitfalls
Section titled “Common pitfalls”| Pitfall | Why it’s wrong | Fix |
|---|---|---|
| Sub-delegating a wider scope than received | Verifier rejects via intersection; chain produces empty effective scope | Sub-delegate a subset |
| Sub-delegation expiry longer than parent | Verifier rejects: child cert is expired-by-parent at the parent’s expires_at | Set child expiry ≤ parent expiry |
| Forgetting to include the parent cert in the bundle | Verifier can’t find Agent-A’s pubkey to verify A→B’s signature; bad_signature | Include every cert in the chain |
| Reusing a challenge from a previous handshake | Verifier rejects: stale challenge_at. Mutual auth must use fresh nonces per direction. | Generate fresh nonces |
Source
Section titled “Source”A2A patterns and the receipt envelope are documented in docs/AGENT_TO_AGENT.md in the protocol repo.
Where to next
Section titled “Where to next”- MCP guide — A2A in the specific context of Model Context Protocol
- Delegate, Present, Verify — chain verification details
- Scopes — what scope intersection looks like