<!-- markdown mirror of https://mintid.net/en/sdk/agent-to-agent-handshake — generated at build time -->

> Before an agent accepts a task or runs a tool over A2A or MCP, the other agent proves it is human-backed, at a grade, for this kind of work — the ordinary 10-second presentation, carried in the session setup. Provisional message shapes.

index — 22 documents

1.  [overviewsdk/00](/en/sdk)
2.  [getting startedsdk/01](/en/sdk/getting-started)
3.  [python sdksdk/02](/en/sdk/python)
4.  [service modesdk/03](/en/sdk/service-mode)
5.  [typescript clientsdk/04](/en/sdk/typescript-client)
6.  [mcp serversdk/04a](/en/sdk/mcp-server)
7.  [conformancesdk/05](/en/sdk/conformance)
8.  [reason codessdk/06](/en/sdk/reason-codes)
9.  [substrate readssdk/07](/en/sdk/substrate-reads)
10.  [credentials & holderssdk/08](/en/sdk/credentials-and-holders)
11.  [holder · wasmsdk/holder](/en/sdk/holder)
12.  [agent clientssdk/15](/en/sdk/agent-clients)
13.  [agent-to-agent handshakesdk/14](/en/sdk/agent-to-agent-handshake)
     *   [provisional](#overview)
     *   [the flagship demo](#demo)
     *   [who can require it](#who-can-require)
     *   [which package each side installs](#which-package)
     *   [the flow](#flow)
     *   [message shapes](#message-shapes)
     *   [what the responder learns](#what-the-responder-learns)
     *   [reason codes](#reason-codes)
     *   [not yet](#not-yet)
14.  [payment identitysdk/09](/en/sdk/payment-identity-profile)
15.  [becoming a verifiersdk/10](/en/sdk/becoming-a-verifier)
16.  [protocol statesdk/11](/en/sdk/protocol-state)
17.  [verify a releasesdk/18](/en/sdk/verify-a-release)
18.  [browser checksdk/19](/en/sdk/check-a-presentation-in-the-browser)
19.  [record · assurance vocabularyrecord/01](/en/sdk/kyc-trust-model)
20.  [record · packagingrecord/02](/en/sdk/packaging)
21.  [record · multi-languagerecord/03](/en/sdk/multi-language-clients)
22.  [record · service contractrecord/04](/en/sdk/verifier-service-contract)

SDK docs · sdk/14

# Agent-to-agent handshake

Before an agent accepts a task or runs a tool over A2A or MCP, the other agent proves it is human-backed, at a grade, for this kind of work — the ordinary 10-second presentation, carried in the session setup. Provisional message shapes.

## Proving a human behind the other agent

Two agents meet over A2A or MCP. One of them — the **responder** — will not accept a task or run a tool until it knows there is an accountable human behind the other. The handshake is how it finds out: at session setup, before any task or tool call, the requester proves _human-backed, at assurance grade ≥ G, delegated for this kind of work_, and nothing else. It is the ordinary 10-second presentation of a relying party, carried inside the agent protocol's own session-setup messages. No new cryptography, no new verifier class, no new chain state.

> **Provisional.** The message shapes on this page are the reference binding (`mintid-handshake/0`). They may still change; they enter the canonical contract set with a version once the live end-to-end run of the agent profile passes. The verification underneath them does not change: it is the frozen verifier-service contract, agent profile.

## The flagship demo: a backed agent and an anonymous bot

A responder agent exposes an MCP tool. Three clients connect:

1.  **An anonymous bot** — no signing key, no credential. The responder requires signed requests: its `initialize` is refused with `mintid_handshake:peer_signature_missing`, and every later call with `mintid_handshake:handshake_required`.
2.  **Identified software** — it signs its requests (Web Bot Auth) but has no human behind it. It gets the challenge, cannot answer it, and every `tools/list` or `tools/call` is refused with `handshake_required`. If it sends a forged answer, the responder's service rejects it, the session is closed, and from then on every call is `handshake_closed`. The gate never falls open.
3.  **A human-backed agent** — it signs its requests and holds an agent credential. It answers the challenge once, is admitted, and its tool calls are served for the rest of the session.

That is the ladder of the payment profile — anonymous → identified software → identified software with an accountable, unrevealed human behind it — applied to agent sessions instead of paid requests.

## Who can require the handshake

Only a **registered verifier**. The responder's operator holds a verifier registration, an exact origin and a request key on the chain — the same registration any relying party needs ([becoming a verifier](/en/sdk/becoming-a-verifier)) — and runs its **own** verifier service. The challenge comes from that service and the answer is validated on that service; the responder never decides a presentation itself and never asks someone else's service to decide for it. An agent without that registration can still _answer_ handshakes; it cannot _require_ them.

## Which package each side installs

The two sides ship apart, so each installs only what it runs:

Side

Python

What it brings

requester (answers)

`mintid-agent`: `from mintid_agent.handshake import HandshakeClient`

the requester's checks, the message shapes, the message signing for transports without headers; the holder library proves

responder (requires)

`mintid-verifier-sdk`: `from verifier_core.a2a import HandshakeGate`

the gate, its port to the responder's own verifier service, the check of signed requests

An agent runtime answers a handshake with the agent client alone; it does not install the verifier package. `verifier_core.a2a.HandshakeClient` still resolves, to the same class, when `mintid-agent` is installed beside it — kept for code written before the move. A requester can also ask its credential before any session whether a declared policy is within reach, with nothing sent: `AgentClient.can_satisfy` ([agent clients](/en/sdk/agent-clients#checking-a-policy-before-any-challenge)).

## The flow

1.  **Declaration.** The responder publishes what it requires: an agent-profile claim policy (`human_backed`, `assurance_grade >= G`, optional scope predicates such as the delegated action, `credential_active`, and `unexpired` — mandatory since contract 1.5.0: a gate policy without it is refused at configuration), its issuer policy and its verifier origin. The gate always asks for the pairwise pseudonym for its own verifier id — mandatory in every agent policy since contract 1.4.0 — and says so (`agent_recognition.pairwise_pseudonym: true`). Policy only — no nonce, no lifetime, no audience override.
2.  **Challenge.** When a session opens, the responder asks its service for a challenge (`createChallenge`, agent profile, that policy) and embeds the answer — `{request, signature_hex}` — unmodified in the session-setup message.
3.  **Checks on the requester's side.** Before it proves anything, the requester checks that the challenge is for the agent profile, for **exactly the origin it dialed**, not past its deadline, and not answered before; then that its credential can satisfy the policy (grade, custody, limit, scope, expiry). An unsatisfiable policy costs no proof and leaks nothing — and the holder library refuses on its own anyway.
4.  **Answer.** The requester hands the challenge — request and signature — to its holder, which checks the signature against the responder's registered request key before it proves anything ([the challenge signature](/en/sdk/credentials-and-holders)), and presents once. The answer is the holder's envelope for that challenge.
5.  **Decision.** The responder validates the envelope on its service against the request _it_ issued — never one the peer sends back. Every condition of the 10-second presentation is decided there: verifier and exact origin active, request key registered, nonce unused and consumed, inside the 10 seconds, fully bound, every predicate proven, issuer and status root current, state rechecked.
6.  **Admission or closure.** Accepted: the transport session (the MCP session, the A2A context) is bound to the presentation and, when requests are signed, to the signing key; calls are admitted until the responder's local session lifetime ends. Anything else: the session is closed with `mintid_handshake:<reason_code>`.

**Mutual handshake.** Both sides may require the other; the two handshakes run one after the other. The requester sends its own challenge along with its answer; the responder admits the requester first and only then answers the counter challenge; the requester validates that answer on _its_ service. If the responder cannot or will not prove itself, the requester closes the session. In A2A the requester sends no task content until both sides are admitted.

## Message shapes (provisional)

### MCP

The client advertises that it can present (and, for a mutual handshake, what it requires) in `initialize`; the server answers with its requirement and the challenge:

jsonsdk/14 · initialize

```
{ "method": "initialize", "params": { "protocolVersion": "2025-06-18",
  "capabilities": { "experimental": { "mintid_handshake": {
    "version": "mintid-handshake/0", "present": true, "declaration": { "…": "only for a mutual handshake" } } } } } }
```

jsonsdk/14 · the server's answer

```
{ "result": { "capabilities": { "tools": {}, "experimental": { "mintid_handshake": {
  "version": "mintid-handshake/0", "required": true,
  "declaration": { "version": "mintid-handshake/0", "verifier_origin": "https://responder.example",
                   "profile": "agent_v1",
                   "claim_policy": [ { "claim": "human_backed", "value": true },
                                     { "claim": "assurance_grade", "operator": ">=", "value": "A2" },
                                     { "claim": "delegated_scope_includes", "value": "data.read" } ],
                   "issuer_policy": {}, "agent_recognition": { "pairwise_pseudonym": true } },
  "challenge": { "request": { "…": "the service's signed request, unmodified" }, "signature_hex": "…" } } } } } }
```

Before anything else the client sends `mintid/present`; the result admits it:

jsonsdk/14 · mintid/present

```
{ "method": "mintid/present", "params": { "session_id": "<the request's session_id>",
  "envelope": { "…": "the holder's envelope" }, "counter_challenge": { "…": "only for a mutual handshake" } } }
{ "result": { "admitted": true, "session_id": "…", "expires_at_unix": 1760000930,
              "counter_envelope": { "…": "only for a mutual handshake" } } }
```

Before admission, anything other than `initialize`, `notifications/initialized`, `ping` and `mintid/present` is a JSON-RPC error `-32001` with message `mintid_handshake:handshake_required`. A refused answer is the same error with its reason code, and the server closes the session. A requester whose own gate refuses the counter answer sends `notifications/mintid/closed` `{"reason": "mintid_handshake:<code>"}` and disconnects. Later calls carry the admitted session id in `params._meta["mintid/session"]`.

### A2A

The responder's agent card declares the requirement as an extension:

jsonsdk/14 · agent card

```
{ "capabilities": { "extensions": [ {
  "uri": "https://mintid.net/a2a/ext/handshake/v0", "required": true,
  "description": "MintID human-backing handshake …",
  "params": { "…": "the declaration, as in MCP" } } ] } }
```

The first `message/send` of a context is answered with a task in state `auth-required` whose status message carries a data part `{"mintid_handshake": {"challenge": {…}}}`. The requester answers on the same task and context with a data part `{"mintid_handshake": {"session_id", "envelope"[, "counter_challenge"]}}`. Admitted: the queued task runs, and the status message adds `{"mintid_handshake": {"admitted": true, "session_id", "expires_at_unix"[, "counter_envelope"]}}`. Refused: the task ends `rejected` with the text `mintid_handshake:<reason_code>` and the context is closed. For a mutual handshake the requester's own declaration rides in the first message's `params.metadata["mintid/declaration"]`. Later messages carry `params.metadata["mintid/session"]`.

### Signed requests

Over HTTP, the requester's messages carry ordinary Web Bot Auth headers (see the [payment-identity profile](/en/sdk/payment-identity-profile#request-signing)). Over a transport without headers (MCP over stdio), the same parameters travel inside the message, in `params._meta["mintid/signature"]` (MCP) or `params.metadata["mintid/signature"]` (A2A): the signature agent, `keyid`, `created`, `expires`, `nonce`, `alg: "ed25519"`, `tag: "web-bot-auth"` and the signature, over an RFC 9421-shaped base covering the responder's origin, the signature agent and a SHA-256 content digest of the message without the signature slot. The association of the key with the session is session-level only — no key-binding semantics are claimed.

## What the responder learns — and what it never learns

It learns that the peer answered _its_ challenge, for _its_ policy, in _this_ session: every predicate of the policy holds for a credential of an accepted issuer, active and unrevoked. It also learns the pairwise pseudonym (always asked for since contract 1.4.0): a value that is stable for this agent and this responder and unrelated for any other responder or any sibling agent — enough to recognise a returning agent, not enough to link it anywhere else.

What it keeps is the shape of a session and nothing more: the handshake session id, the transport session, the digest of the bound context, the digest of the policy, the signing key id, the pseudonym, and two timestamps. Its service keeps the usual minimal decision record.

It never learns who the principal is, which other agents the principal runs, the exact grade, the exact limit, any scope value beyond what its own policy named, or the credential's identifier. It stores no proof, no envelope and no claim value, and nothing about the handshake touches the chain.

## Reason codes

A refusal is spelled `mintid_handshake:<reason_code>`. Every presentation decision of the responder's service passes through verbatim ([reason codes](/en/sdk/reason-codes)) — `deadline_exceeded`, `nonce_replayed`, `proof_invalid`, `status_root_stale` and the rest. Around it, the gate adds:

Code

Meaning

`handshake_required`

a call before admission, or a message where the answer was expected

`handshake_closed`

the session was closed by an earlier refusal

`challenge_unavailable` · `service_unavailable`

the responder's service would not issue a challenge for its policy, or could not be reached or answered unexpectedly — the responder refuses rather than admitting

`presentation_malformed`

the answer did not carry a usable envelope

`session_mismatch` · `session_exists`

the answer or call names another handshake session; a session id was reused

`session_expired`

past the responder's local session lifetime

`session_key_mismatch`

signed by a key that is not the session's (refused without closing the session)

`pseudonym_missing`

the service's accepted decision did not carry the pseudonym (it always should)

`peer_signature_missing` · `_malformed` · `_expired` · `_replayed` · `_invalid` · `peer_key_unknown` · `peer_directory_unavailable`

the request signature, when the responder requires one

The requester declines locally — nothing is sent — with `unsatisfiable` (naming the predicate it cannot prove), `audience_mismatch`, `profile_mismatch`, `challenge_expired`, `challenge_malformed`, `already_presented` or `challenge_unverified` (its holder refused the challenge's signature; the detail is the holder's reason code, such as `challenge_signature_invalid`).

## Not yet

*   **Key binding.** Proving that the key signing a request belongs to the credentialled agent is a new predicate and is deliberately not specified. The association is session-level.

Where to go next:

*   [Payment-identity profile](/en/sdk/payment-identity-profile) — the same presentation on a payment rail, and the request-signing tier.
*   [Reason codes](/en/sdk/reason-codes) — the service vocabulary that passes through the handshake.
*   [Becoming a verifier](/en/sdk/becoming-a-verifier) — the registration a responder needs before it can require anything.

[previous← agent clients](/en/sdk/agent-clients)[nextpayment identity →](/en/sdk/payment-identity-profile)

---
Source: https://mintid.net/en/sdk/agent-to-agent-handshake · Agent-to-agent handshake · MintID SDK docs
