# 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](concepts.md) for the mental model.

`user-zk` sits beside [`agent-zk`](agent-tokens.md) 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

```ts
// 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,
);
```

```js
// 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](agent-tokens.md#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](verify-tokens.md).

## 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](../sovereign-tier.md#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 tokens](agent-tokens.md) — `agent-zk`, the same proof shape for
  enrolled machines.
- [Wallet login](wallet-login.md) — `user-sig`, permissionless login with no
  enrollment or revocation.
- [Verify a deed](verify-tokens.md) — the relying-party side.
- [Concepts](concepts.md) — the mental model.
- [Errors](errors.md) — every error code a relying party branches on.
