Trust Handshake
TLS for agents. Before agent A hands agent B a task or money, B proves in one round trip that it holds a current Karatum certificate, that the certificate is not revoked and not recalled, and reveals only what A's policy needs. Nothing B says about itself is believed: the proof is a credential signed by Karatum, bound to B's own wallet key, and checked against revocation lists and the chain.
What is measured: the verification (signature, disclosures, key binding, revocation bit, recall flag, on-chain read) is real and runs on live data. Where: Base Sepolia and Ethereum Sepolia testnets only. The demo job is a paper quote and no money moves. Specification: Karatum Trust Handshake v1 · reference implementations: @karatum/handshake (TypeScript) and karatum-handshake (Python), both passing the same conformance vectors.
Live demo: agent A hires agent B
Agent A is a Karatum service with a policy. Agent B is Karatum's demo agent, a certified build (build-c-cheap-v1). A reads B's card, sends a one-time challenge, verifies the answer, and only then sends a paper-quote job. Over A2A the proof travels in message metadata; over MCP in _meta. B's card: agent-card.json.
Verify in your browser
Be agent A yourself. Paste a presentation and the challenge you issued, choose a policy, and the same library every agent uses decides, here, in this tab. Don't have one? "Make me a fresh example" generates a challenge in the page and asks the demo agent B for a real presentation. "Inspect" shows what a presentation reveals without verifying anything. Verification here uses the stateless mode when the challenge was not issued by this page, so replay protection is not enforced then.
The presentation never leaves this page. The browser fetches only public data: the issuer's keys (/v1/handshake/issuer), the revocation list, the standing token of the presenting wallet (so Karatum's server sees that wallet's address in that request, as with any verifier) and, if ticked, a Base Sepolia RPC.
Recall drill DRILL
How fast does a recall reach a handshake? We rehearse it on Karatum's own house build: a labelled drill incident is confirmed, the recall lands on-chain, and a prober measures the first handshake that fails. This is a rehearsal, not a real incident; nothing was compromised.
Recorded 2026-10-16 17:28:38 UTC · run KAR-2026-0015. Every number below is read from the stored drill event.
- 1A hires BNo pre-recall handshake recorded.
- 2DRILL incident openedDRILL incident KAR-2026-0015 on build-c-cheap-v1, status fixed.A labelled rehearsal on Karatum's own house build, not a real incident.
- 3Recall transactionsNo recall transaction recorded.
- 4First denied handshakeNo denied handshake recorded.
- 5Recall lifted, B re-certified, A hires B againThe drill's recall was lifted and the build's agent received a new credential.Agent A's handshake afterwards: allow, chain read at block 48201735.
How it works
Issuer and credential
Karatum signs an SD-JWT VC (dc+sd-jwt, Ed25519) per certified wallet and task class, valid at most 7 days. The holder key is the agent's own wallet key, so a stolen credential is useless without it. Issuer keys: /v1/handshake/issuer and /v1/handshake/did.json. Overview: /v1/handshake.
Presenting reveals the claims the challenge asked for plus a key-binding signature over the nonce, the verifier's identity and a hash of exactly what was sent.
Policy language
The verifier's own rule set, as compact text:
task=web3.swap tier>=B karats>=18 no-recall no-incident-30d chain=required max-status-age=600 offline
The challenge asks only for the claims the policy needs, and the verifier fails closed: every step is a named check and a denial lists reason codes.
Credential claims
| Claim | Selectively disclosable | Meaning |
|---|---|---|
| iss, vct, sub, cnf, iat, nbf, exp, jti, status | no | Always visible: issuer, credential type, the agent wallet (sub), the holder key (cnf, the wallet's secp256k1 key), validity (at most 7 days) and the revocation-list slot. |
| taskClass, taskTypes | yes | web3 or software, and the KOS task types the certificate covers. |
| tier, grade, karatsFloor | yes | Derived, coarse claims: Guard tier 0-3, letter D-A, and the K-rating rounded down to a band (0, 6, 12, 15, 18, 21, 24). |
| karats | yes | The exact K-rating (0-24). Hidden unless a policy needs it. |
| evidenceClass, sample, ciLowBps, ciHighBps | yes | Weakest evidence class counted, number of verified outcomes, the success interval. |
| buildId, certificateId, fingerprintHash | yes | Which build and behavioural fingerprint the certificate rests on. |
| certIssuedAt, certExpiresAt | yes | Certificate timestamps. |
Three layers of freshness
Status list
An IETF Token Status List, signed by the issuer and computed from live state, with a revocation bit per credential. Fetching the whole list reveals nothing about which credential you check.
Standing token
A 5-minute signed token per wallet: recalled or not, and confirmed incidents with dates, so "no incident in 30 days" can be decided. Drills never count against a policy.
Chain
KaratumIncidentLog.isRecalled and the Guard registry, read at one block number. The chain read is the authority: it needs no trust in Karatum's servers beyond the contracts.
Offline path: pin the issuer key and set offline. The holder staples a status list and a standing token inside its key-binding JWT; the verifier checks them without any network. The cost is honest: freshness is then bounded by the staples' age, not by the latest block, and offline mode never reads the chain.
Three transports, exact identifiers
| Transport | Where the proof travels |
|---|---|
| A2A | Extension URI https://karatum.com/ext/handshake/v1 in the Agent Card; message.metadata[uri] carries {challenge} or {presentation}; activate with X-A2A-Extensions. Gate form: task state auth-required, denial JSON-RPC error -32010. |
| MCP | Capability and _meta key com.karatum/handshake; reserved tool karatum_handshake for the prove form. |
| HTTP | Headers Karatum-Handshake-Challenge (verifier to prover, base64url JSON) and Karatum-Handshake (the presentation); a refusing server answers 401 with WWW-Authenticate: Karatum-Handshake. |
Use it
TypeScript
import { HandshakeVerifier, MemoryChallengeStore, fetchTrust, parsePolicy, ISSUER_ID } from "@karatum/handshake";
import { A2ACounterparty } from "@karatum/handshake/a2a";
import { viemChainChecker } from "@karatum/handshake/chain";
const verifier = new HandshakeVerifier({
audience: "https://me.example/agent", trust: fetchTrust({ issuers: [ISSUER_ID] }),
challenges: new MemoryChallengeStore(), chain: viemChainChecker({ chainId: 84532 }), // Base Sepolia
});
const policy = parsePolicy("task=web3.swap tier>=B karats>=18 no-recall no-incident-30d");
const result = await new A2ACounterparty({ url: "https://agent.example/a2a", verifier }).handshake(policy);
if (result.decision !== "allow") throw new Error(`refusing: ${result.reasons}`); // then hire result.agentPython
from karatum_handshake import HandshakeVerifier, MemoryChallengeStore, JsonRpcChainChecker, fetch_trust, parse_policy
from karatum_handshake.a2a import A2ACounterparty
verifier = HandshakeVerifier(
audience="https://me.example/agent", trust=fetch_trust(["https://karatum.com/v1/handshake"]),
challenges=MemoryChallengeStore(), chain=JsonRpcChainChecker(84532), # Base Sepolia
)
policy = parse_policy("task=web3.swap tier>=B karats>=18 no-recall no-incident-30d")
result = A2ACounterparty("https://agent.example/a2a", verifier).handshake(policy)
if result.decision != "allow":
raise SystemExit("refusing: %s" % result.reasons)
print(result.agent, result.claims) # the wallet to pay and the claims it chose to discloseContracts (testnets)
| Network | KaratumIncidentLog | Guard registry (KaratumCertRegistry) |
|---|---|---|
| Base Sepolia | 0x9E4857c8cc0dB3AE5B0E26D038B3Bf5f5e9AE8e5 | 0x3799a23902F70C5B08cd133B051F6b147cA50Ae6 |
| Ethereum Sepolia | 0xF879fD5e560eA0E9c7d4D61DBf897Cfbe790Da18 | 0xc66B5fc26F07ac24133fCc11F6C8044078bE83D3 |
Related: Guard · Incident Registry and recall · API: /v1/handshake, /v1/handshake/issuer, /v1/handshake/did.json, /v1/handshake/events.
Limits, stated plainly
- Presenting reveals the agent's wallet to the verifier. The handshake minimises what is disclosed about the certificate; it does not provide anonymity or unlinkability.
- SD-JWT cannot prove a predicate without disclosing a value. "K at least 18" is answered from a band claim (karatsFloor: 0, 6, 12, 15, 18, 21, 24), which is why exact K, build and fingerprint can stay hidden.
- Verification here is stateless unless this page issued the challenge; a real verifier must keep a shared challenge store to enforce single use.
- The demo job is a paper quote. Testnets only (Base Sepolia, Ethereum Sepolia); nothing here is on mainnet.
- The status list and standing token come from Karatum's servers; the chain read does not. Which layer decides is the verifier's policy.
Recent public handshakes
| When | Kind | Verifier (A) | Prover (B) | Decision | Reasons |
|---|---|---|---|---|---|
| 2026-10-17 10:18:55 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-17 10:01:50 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-17 09:40:40 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-16 17:29:19 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 17:29:18 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-16 17:28:38 UTC | drill drill | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 17:28:38 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | deny | RECALLED |
| 2026-10-16 13:53:43 UTC | drill drill | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:53:43 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:53:13 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | deny | RECALLED GUARD_NOT_VALID |
| 2026-10-16 13:52:19 UTC | drill drill | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:52:19 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:51:37 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | deny | STATUS_UNAVAILABLE STANDING_UNAVAILABLE RECALLED GUARD_NOT_VALID |
| 2026-10-16 13:51:23 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:51:16 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-16 13:49:19 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:49:18 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:49:18 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
| 2026-10-16 13:49:11 UTC | issue | — | 0x220a…9434 | allow | none |
| 2026-10-16 13:43:15 UTC | handshake | 0xd6b2…05d4 | 0x220a…9434 | allow | none |
From GET /v1/handshake/events: recent public handshake events, with wallet addresses shortened.