Skip to content

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

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 purchase
// 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 chain
bundle := &ratify.ProofBundle{
AgentID: agentB.ID,
AgentPubKey: agentB.PublicKey,
Delegations: []*ratify.DelegationCert{originalCert, subCert},
Challenge: challengeFromVerifier,
ChallengeAt: time.Now().Unix(),
ChallengeSig: signedByAgentB,
}

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
→ valid

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

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; proceed

This is two challenge-response rounds, one per direction. Both agents prove they hold the live key for their respective identities.

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.

PitfallWhy it’s wrongFix
Sub-delegating a wider scope than receivedVerifier rejects via intersection; chain produces empty effective scopeSub-delegate a subset
Sub-delegation expiry longer than parentVerifier rejects: child cert is expired-by-parent at the parent’s expires_atSet child expiry ≤ parent expiry
Forgetting to include the parent cert in the bundleVerifier can’t find Agent-A’s pubkey to verify A→B’s signature; bad_signatureInclude every cert in the chain
Reusing a challenge from a previous handshakeVerifier rejects: stale challenge_at. Mutual auth must use fresh nonces per direction.Generate fresh nonces

A2A patterns and the receipt envelope are documented in docs/AGENT_TO_AGENT.md in the protocol repo.