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.

5
active credentials (1 wallet)
8
revoked
0
expired, in 1 status list

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.

Runs on the live demo service: real credential, real revocation list, real Base Sepolia read. The job itself is a paper quote; no funds move.

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.

  1. 1
    A hires B
    No pre-recall handshake recorded.
  2. 2
    DRILL incident opened
    DRILL incident KAR-2026-0015 on build-c-cheap-v1, status fixed.
    A labelled rehearsal on Karatum's own house build, not a real incident.
  3. 3
    Recall transactions
    No recall transaction recorded.
  4. 4
    First denied handshake
    No denied handshake recorded.
  5. 5
    Recall lifted, B re-certified, A hires B again
    The 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

ClaimSelectively disclosableMeaning
iss, vct, sub, cnf, iat, nbf, exp, jti, statusnoAlways 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, taskTypesyesweb3 or software, and the KOS task types the certificate covers.
tier, grade, karatsFlooryesDerived, 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).
karatsyesThe exact K-rating (0-24). Hidden unless a policy needs it.
evidenceClass, sample, ciLowBps, ciHighBpsyesWeakest evidence class counted, number of verified outcomes, the success interval.
buildId, certificateId, fingerprintHashyesWhich build and behavioural fingerprint the certificate rests on.
certIssuedAt, certExpiresAtyesCertificate 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

TransportWhere the proof travels
A2AExtension 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.
MCPCapability and _meta key com.karatum/handshake; reserved tool karatum_handshake for the prove form.
HTTPHeaders 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.agent

Python

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 disclose

Contracts (testnets)

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

WhenKindVerifier (A)Prover (B)DecisionReasons
2026-10-17 10:18:55 UTCissue—0x220a…9434allownone
2026-10-17 10:01:50 UTCissue—0x220a…9434allownone
2026-10-17 09:40:40 UTCissue—0x220a…9434allownone
2026-10-16 17:29:19 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 17:29:18 UTCissue—0x220a…9434allownone
2026-10-16 17:28:38 UTCdrill drill0xd6b2…05d40x220a…9434allownone
2026-10-16 17:28:38 UTChandshake0xd6b2…05d40x220a…9434denyRECALLED
2026-10-16 13:53:43 UTCdrill drill0xd6b2…05d40x220a…9434allownone
2026-10-16 13:53:43 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:53:13 UTChandshake0xd6b2…05d40x220a…9434denyRECALLED GUARD_NOT_VALID
2026-10-16 13:52:19 UTCdrill drill0xd6b2…05d40x220a…9434allownone
2026-10-16 13:52:19 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:51:37 UTChandshake0xd6b2…05d40x220a…9434denySTATUS_UNAVAILABLE STANDING_UNAVAILABLE RECALLED GUARD_NOT_VALID
2026-10-16 13:51:23 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:51:16 UTCissue—0x220a…9434allownone
2026-10-16 13:49:19 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:49:18 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:49:18 UTChandshake0xd6b2…05d40x220a…9434allownone
2026-10-16 13:49:11 UTCissue—0x220a…9434allownone
2026-10-16 13:43:15 UTChandshake0xd6b2…05d40x220a…9434allownone

From GET /v1/handshake/events: recent public handshake events, with wallet addresses shortened.