docs / guide

view as .md

User gating (user-zk)#

A human proves membership of your app's own user allowlist in zero-knowledge — your app learns "an enrolled member of tenant T logged in" plus a stable, app-scoped pseudonym, and nothing else. No wallet address, no name, no membership list position: your app cannot tell which enrolled member just signed in, only that one did. See Concepts for the mental model.

user-zk sits beside agent-zk as a kind of deed — same zero-knowledge membership proof, same on-chain revocation, same billing check — proved against a separate, admin-curated user tree instead of the agent tree. It is additive alongside user-sig/user-passkey/user-1271: pick it when a human logging in must be on YOUR allowlist first, not merely hold a key.

Mint + verify, at a glance#

// ZkUser ships from the same low-level module ZkAgent does —
// pkg/grantor_agent_wasm.js (built by `just agent-ts`), not the
// authenticate()/discover() high-level module — same convention
// agent-tokens.md documents for ZkAgent.
import { Registry } from "../../sdk/agent/ts/src/registry.js";

// once, per member: derive the identity from a wallet signature and hand
// the PUBLIC commitment to your admin flow — never the signature itself.
const user = ZkUser.fromWalletSignature(walletSignature);
const commitment = user.commitment(); // -> tenant admin calls registerZkUser(tenantId, commitment)

// each login: sync the user tree from chain, prove membership, mint a
// user-zk deed bound to a challenge YOUR app issued.
const deed = await user.mintUserDeed(
  rpcUrl, Registry.canonical(), tenantId, audience, origin, challenge, expUnix,
  vouchSignature, vouchEpoch, vouchExp, allowInsecureOrigin, nowUnix,
);
// your app: the same DeedGuard.verify call every mode goes through —
// excerpted from examples/mcp-server/server.mjs's own handler.
const claims = await g.guard.verify(deedJson, challenge);
// claims.sub is the pseudonym — log ONLY this, never a wallet address.
console.log(`an enrolled member of tenant ${claims.tenant} logged in (sub=${claims.sub})`);

mintUserDeed's parameters mirror ZkAgent.mintDeed exactly (the registry ref, the origin vouch, allowInsecureOrigin, nowUnix) — see Agent tokens § The agent-zk recipe for what each one does; the provenance check runs the same way, before proving, on the mint path here too. Your app verifies a user-zk deed exactly the way it verifies every other mode — the same DeedGuard/DeedVerifier, the same verify_deed call. See Verify a deed.

Admin enrollment (once per member, on-chain)#

Before a human can mint a user-zk deed, the tenant admin enrolls their public commitment in the tenant's on-chain user tree — a tree the GrantorRegistry keeps entirely separate from the agent tree agent-zk proves against, with its own cap and its own counter:

  1. The human derives their identity from a wallet signature (ZkUser.fromWalletSignature, every language) and reads off the public commitment(). The signature itself never leaves their machine; only the commitment crosses to the admin.
  2. The admin calls registerZkUser(tenantId, commitment) — or registerZkUserBatch(tenantId, commitments) to enroll several members in one transaction, atomically checked against the tier's maxUsers cap before any of them lands.
  3. The admin publishes an origin vouch for the app's origin, the same mechanism agent-zk uses, so mintUserDeed's provenance check can pass — see Sovereign tier § Origin provenance.

Anonymous membership and the pseudonym#

A user-zk deed proves "this signer is one of the tenant's enrolled users" without revealing which one — the same Semaphore zero-knowledge membership proof agent-zk uses, over a different tree. The sub your app receives is a stable pseudonym, scoped to (the member's identity, tenant, audience): the same member gets the same sub on every login to the same app, and a different, uncorrelatable sub at a different app. Use it as your primary key for per-member state; do not expect it to reveal, or be derivable from, a wallet address.

Revocation#

The admin revokes a member with revokeZkUser(tenantId, commitment). This advances the on-chain user tree's root immediately — the member's prior membership proof stops being valid input to a new proof at once, and any user-zk deed already minted against the pre-revocation root stops verifying on its next presentation, because your app's verify_deed call checks root recency on every request, not just at mint time. There is no grace period and nothing for the revoked member to do; the next login simply fails the same way an expired or never-enrolled one would.

The wallet-derived-identity requirement#

user-zk identities are derived deterministically from a wallet signature (ZkUser.fromWalletSignature), the same derivation agent-zk uses for an agent key. A member needs a signing key to enroll and to log in — there is no keyless or browser-only variant of this mode today.

Not built yet#

Two related gating shapes are deliberately out of scope for this mode and are not shipped:

  • Token-gating / DAO membership — proving membership via an on-chain token balance or a DAO's own membership set, rather than an admin-curated allowlist. This needs a maintained snapshot tree or storage proofs, which is a materially larger, separate piece of work.
  • Passkey-derived anonymous identities — a user-zk-shaped mode identity-derived from a passkey instead of a wallet signature. There is no stable secret a passkey can deterministically re-derive across devices the way a wallet signature can, so this has no clean derivation to build on yet.

user-zk itself is complete for admin-curated, wallet-derived, anonymous, pseudonymous, revocable human membership — that is its whole scope.

See also#

  • Agent tokensagent-zk, the same proof shape for enrolled machines.
  • Wallet loginuser-sig, permissionless login with no enrollment or revocation.
  • Verify a deed — the relying-party side.
  • Concepts — the mental model.
  • Errors — every error code a relying party branches on.

This page is also served as Markdown — agents should read that. The whole tree is indexed for machines in llms.txt.