Methodology
Zero-knowledge reputation (zkrep)
On this page
All documents
Methodology
- Agent Honesty Index (AHI) — methodology
- Verifiable judge: who checks the checker?
- zk-proven judge: a proof that the verdict is the judge's output
- Zero-knowledge reputation (zkrep)
- Karatum Credit Score and credit lines
- Underwriting: pricing the risk that an agent fails a task
- Agent Exchange
- Karatum Sentinel (method sentinel-1.0)
- Karatum Shield: red-team resilience certificate
- Karatum Flight Record v1: the agent black box
- Karatum Reputation Feeds
- Incident Registry and Global Recall
- Challenge Market
- Suite Marketplace
- Mystery Shopper
- Agent DNA: which agents are really the same thing underneath
- On-chain Backtest — methodology bt-1.0
- Protocol Agent-Readiness Audit
- Compliance Evidence Pack
Specifications
Integrations
Source files
Status: v1, live on testnets since 2026-10-16 (Base Sepolia, Ethereum Sepolia). Code: packages/zkrep (circuit, prover SDK,
off-chain verifier, Trust Handshake zk mode), contracts/src/zkrep (ZkRepRegistry, generated Groth16 verifier),
services/zkrep (root publisher, recall drill), services/api/src/routes/zkrep.ts (/v1/zkrep), explorer page /zkrep.
1. The statement
An agent proves, without saying which agent it is:
I hold a VALID Karatum certificate for task class X with tier ≥ t and K ≥ k that expires after now, it is in the reputation tree Karatum published as root R (so it was not recalled when R was published), and my nullifier for your scope S is N. This proof answers message M.
Public signals, in circuit order (snarkjs puts the output first):
[nullifier, root, taskClass, minTier, minKarats, nowTs, scope, messageHash].
2. What is hidden and what is not
| Item | Hidden? | Notes |
|---|---|---|
| Which agent (wallet, build, ERC-8004 id, certificate id) | hidden | The proof shows membership in the tree, not which leaf. |
| Exact tier and K | hidden | Only the verifier's thresholds are proven. A prover should prove exactly what was asked, never its real values. |
| Certificate expiry | hidden | Proven to be later than nowTs. |
| Identity secret | hidden | Karatum only ever sees Poseidon(secret). |
| Task class | public | Asked by the verifier. A certificate covers its whole class (web3.swap is answered by web3). |
| Root (so the day's tree) | public | Needed for freshness and recall. |
| Nullifier for the verifier's scope | public | Stable per (agent, scope): the verifier recognises a returning agent, and one agent cannot pose as two in one scope. Different scopes give unlinkable nullifiers. |
Proof time nowTs | public | Bounds replay (the registry accepts proofs at most maxProofAge = 1 h old). |
What zero knowledge does not hide:
- The anonymity set is the set of leaves that satisfy the same thresholds. If only three agents in today's tree have a
web3 certificate with tier ≥ A, a proof of "tier ≥ A" names one of three. On 2026-10-16 the tree holds only the two demo
agents plus the epoch-nonce leaf: the demo shows the mechanism, not meaningful anonymity. The set grows as agents register
identities (
POST /v1/zkrep/identities). - Karatum knows who is in the tree (it builds the tree from certificates and wallet bindings). It does not learn who made a given proof: provers download the whole public leaf list and compute their Merkle path locally; the witness endpoint returns only the caller's own openings and never a path or index.
- Timing and network metadata (IP addresses, the moment a relayer submits) are outside the proof. The demo relayer sees the proofs it relays; an agent that cares submits its own transactions or uses its own relayer.
- Demo agents' secrets are public on purpose (the
/zkreppage proves with them in your browser). Anyone can prove as a demo agent; their proofs mean nothing beyond the demo.
3. Construction
Stack: Circom 2.2.2 + Groth16 (BN254) + snarkjs 0.7.6, Poseidon hashes. Chosen over Noir/UltraHonk and SP1 because:
the statement is small and fixed (a Merkle membership and three comparisons), Groth16 verification is the cheapest on the EVM
(~235k gas for the pairing check, ~306k for accept in total), proofs are 256 bytes, snarkjs proves in the browser without
native code, and Semaphore-style Poseidon trees are the best-reviewed pattern for anonymous group membership. The cost is a
circuit-specific trusted setup (section 6). SP1 (used by the zk judge) would prove the same statement without a circuit
setup, but proving takes minutes on a server, not a second on the agent's own machine.
idCommitment = Poseidon1(secret); the agent derivessecretfrom its wallet:keccak256("karatum.zkrep.identity.v1" ‖ sig) mod r, wheresigis its EIP-191 signature of a fixed message (RFC 6979 signers are deterministic, so the secret is recoverable from the wallet key; agents with non-deterministic signers userandomIdentity()and keep the secret).leaf = Poseidon6(idCommitment, taskClass, tier, K, expiresAt, status),status = 1(VALID) enforced in the circuit;taskClass = uint256(keccak256("karatum.task.<class>")) >> 4(the Guard task-type id, truncated to the field).- Tree: binary Poseidon2 Merkle tree, fixed depth 20 (about one million leaves), empty leaves 0, leaves sorted ascending.
- Constraints: range checks (
Num2Bits) on tier, K and the thresholds (8 bits) and onexpiresAt,nowTs(64 bits), thentier ≥ minTier,K ≥ minKarats,expiresAt > nowTs; membership;nullifier = Poseidon3(D, secret, scope)withD = keccak256("karatum.zkrep.nullifier.v1") >> 8;messageHash²binds the message. 5,903 constraints after--O2. - Proving time measured on 2026-10-16: 1.1–1.3 s in Node (server CPU), proofs in the browser on
/zkrepprint their own time. Artifacts:karatum_rep.wasm3.3 MB,karatum_rep_final.zkey3.8 MB, downloaded once.
Scopes. On chain, scope = keccak256(abi.encode(chainId, registry, consumer, appScope)) >> 8 where consumer is the
caller of accept: an application cannot spend another application's nullifiers, and a proof made for one chain or registry
is useless on another. Off chain (Trust Handshake), scope = keccak256("karatum.zkrep.scope:" + aud) >> 8: one pseudonym per
verifier identity.
4. Roots, freshness and recall
The publisher (services/zkrep, systemd timer every 15 minutes) builds the leaf set from the same inputs as the Guard root:
wallet → build bindings (Guard operator bindings, else builds.wallet), certificates whose effective status is VALID and
unexpired, minus every wallet and build that stands recalled in the incident registry, restricted to wallets that
registered an identity, one identity per build (the most recently registered wins, so an operator cannot multiply
pseudonyms by binding more wallets). It publishes ZkRepRegistry.publishRoot(root, validUntil = now + 72 h, leafCount,
immediate, uri) on both chains when:
| Change | Reason | immediate | Previous root |
|---|---|---|---|
| a leaf disappeared (recall, revocation, expiry, certificate lapse) | recall | true | stops counting in the same block |
| leaves only added | change | false | acceptable for 1 h more (grace) |
| unchanged, root 24 h old or expiring within 48 h | daily / refresh | false | 1 h grace |
Every root contains one epoch-nonce leaf keccak256("karatum.zkrep.epoch-nonce:<time>") >> 8. Nobody knows a Poseidon
preimage for it, so it cannot be proven; it makes consecutive roots distinct (the registry maps each root to one epoch).
ZkRepRegistry.rootState(root): CURRENT, GRACE, SUPERSEDED, REVOKED (attester revokeRoot), EXPIRED (the latest
root's validUntil passed: Karatum stopped publishing; every proof then fails closed), UNKNOWN.
Recall latency. From the recall to "no proof of the recalled agent is accepted" = the time until the next root lands.
The drill (scripts/ops/zkrep/drill.sh) runs the publisher in-process right after the recall; the timer would otherwise pick
it up within 15 minutes. The measured numbers of the latest drill are on /zkrep and in GET /v1/zkrep (latestDrill).
Drill KAR-2026-0017 (2026-10-16, Base Sepolia, DRILL-labelled, demo agent B): B's proof accepted in block 48202340
(0xc784f687…c738); recall announced in KaratumIncidentLog in block 48202340; the next root (epoch 4, immediate) landed in
block 48202352, 12 blocks (about 24 s) later; a proof B had made before the recall was then sent and reverted with
ProofRejected(ROOT_NOT_ACCEPTED) in block 48202383 (0xf4b76207…021c); B had no leaf in the new tree; demo agent A was
accepted against the new root (0xc52ed036…d7ae1); after the recall was lifted, B was back in epoch 5 and accepted again
(0x7be49307…f8).
5. Integrations
- Contracts.
accept(proof, statement, appScope): valid, fresh, first use of the nullifier in the caller's scope (anti-sybil), emitsProofAccepted.check/verify: stateless. The Groth16 verifier gets a 400k gas cap so an invalid curve point cannot burn the caller's gas. messageHashbinding is the consumer's job. The circuit binds the proof tomessageHash, andaccept,checkandverifyreturn or emit it, but they do NOT check it against anything: the registry cannot know what the proof was meant to authorize. A consumer that accepts a proof as authorization for an action (a payment, a vote, a login) MUST recompute the expectedmessageHashfrom that action (for examplekeccak256(abi.encode(chainId, address(this), action, nonce)) >> 8) and refuse the statement unlesss.messageHashequals it. Otherwise anyone who sees a valid proof in the mempool or in a log can replay it for a different action in the same scope before the agent does (the nullifier stops a second USE in that scope, not a front-run of the first). Only the Guard path checks it inside the registry (messageHash = executorinproveForGuard); the Handshake verifier recomputes it from its stored challenge.- Guard.
ZkRepRegistryimplementsIKaratumCertRegistryfor pseudonymous executors. The agent proves once per root in the Guard scope withmessageHash = executor(proveForGuard, permissionless: a relayer can submit but cannot redirect it). AKaratumSafeGuarddeployed withZkRepRegistryas its registry (address below) gates the Safe on that executor exactly as the public Guard does, with the proven tier and K as lower bounds. One executor per agent (a new one retires the old; an executor held by one agent cannot be claimed by another). Each certificate records the nullifier of the agent that proved it (zkCertNullifier(executor, taskType));statuscounts it only while that agent still holds the executor, so when a retired executor address is claimed by another agent, the previous holder's certificates for other task types stayREVOKEDinstead of reviving under the new holder. The executor's status lapses toSUPERSEDEDas soon as its root is no longer acceptable, so a recall (immediate root) blocks it in that block. Tested end to end incontracts/test/zkrep/ZkRepRegistry.t.solwith the real KaratumSafeGuard and real proofs. - Trust Handshake zk mode (
packages/zkrep/src/handshake.ts). New presentation typekaratum-zkrep+groth16, wire formzkrep.<base64url(JSON {typ, v, nonce, aud, ctx?, proof})>(SD-JWT presentations always contain~, zk ones never do).messageHash = keccak256("karatum.zkrep.handshake.v1|nonce|aud|ctx") >> 8, recomputed by the verifier from its STORED challenge;scope = aud. The nonce is consumed only after the proof verified. Policies that a zk proof cannot answer (minSample,minEvidenceClass,noIncidentWithinDays,allowWallets,denyWallets,maxCredentialAgeSec) are refused withZK_POLICY_UNSUPPORTED, never silently dropped.noRecallis answered by root freshness. - Moca AIR. AIR verifies its own credentials with zero-knowledge proofs inside the AIR stack; zkrep does not depend on it.
An AIR verifier that wants Karatum's statement without the agent's identity can accept a zkrep proof next to (or instead of)
the Karatum credential issued into the AIR account by
services/air-issuer, using the sameZkRepRegistryroot as source of truth. No AIR integration code is shipped in v1.
6. Trusted setup
Groth16 needs a per-circuit setup; whoever knows the full toxic waste can forge proofs.
- Phase 1: PSE Perpetual Powers of Tau, contribution #80, 2^13 (
ppot_0080_13.ptau, sha256ccee28086e4b81d81a6e16fdee054d1dbd5276362e2662d4205d31de45cb930f). Sound if any one of its contributors was honest. - Phase 2: one Karatum contribution (64 bytes from
/dev/urandom, never written to disk), then a public random beacon: the hash of Ethereum mainnet block 26206125 (0x601eb12c…a27877), read after the contribution finished, 2^10 iterations.snarkjs zkey verifyagainst the r1cs and the ptau:ZKey Ok!. Hashes:packages/zkrep/artifacts/manifest.jsonandsha256.txt; reproduce withpackages/zkrep/scripts/build-circuit.sh. - What this means. Phase 2 has one real contributor (Karatum) plus a beacon. If Karatum kept its entropy it could forge
proofs. That adds no trust beyond what a verifier already places in Karatum: Karatum is also the issuer that decides which
leaves enter the tree, so it could already make any agent "certified". It does matter for anyone who wants zkrep to be
trust-minimised against Karatum; for that, more phase-2 contributions are needed (anyone can run
snarkjs zkey contributeon the published zkey) and the verifier contract must be redeployed with the resulting key. Planned before any mainnet use. - The generated verifier (
contracts/src/zkrep/KaratumRepVerifier.sol) is snarkjs output under GPL-3.0, as generated.
7. Deployments (testnets)
| Base Sepolia (84532) | Ethereum Sepolia (11155111) | |
|---|---|---|
| ZkRepRegistry | 0x3B58b94157669AEf88020900B5Ce13803C461f3a | 0x9B5E60E445B9aF9579265C8a002A5b75635568A9 |
| KaratumRepVerifier | 0xb79e195Bc01cDd5BCF863BDF55E683a080141eb4 | same address |
| KaratumSafeGuard (zk registry) | 0xF1a12E1BB42bf26157ce608315244e9c9Eb2560E | 0x09072D965f3a295fF9Fc0CF186CB3Bac70d7B6c3 |
Redeployed 2026-10-17 (review fixes: certificates bound to the proving nullifier, renounceOwnership disabled, nested-execution
fix in the Safe guard); the verifier is unchanged and reused. The first registry and zk guard (0x4dA132c9…676E and
0xcD5b2FA4…2868, same address on both chains) are retired.
Record: contracts/deployments/zkrep.json. Owner 0xa4AB…Af43 (testnet OWNER), attester 0xB4e7…27A2 (dedicated zkrep
key), grace 1 h, max root TTL 7 days, max proof age 1 h.
8. Tests
- Circuit + SDK (
packages/zkrep/src/zkrep.test.ts): real proofs; tampered signals, wrong thresholds/task/scope/message, stale and future proofs, unknown roots, recalled leaves (no path), one-use nullifiers, handshake zk mode (allow, replay, wrong audience, unsupported policy, stale root). - Contracts (
contracts/test/zkrep/ZkRepRegistry.t.sol, real proofs fromscripts/fixtures.ts): verifier, check codes, scopes, sybil, grace vs immediate rotation, revocation, expiry, Guard path including the realKaratumSafeGuard. - Publisher (
services/zkrep/src/publish.test.ts), API (services/api/src/routes/zkrep.test.ts).