docs / guide

view as .md

Software licensing with Grantor#

A license is a deed. The licensed software verifies it locally against the public on-chain registry — no license server, no phone-home, no activation backend to run or keep online. Revocation, tiers and billing all ride primitives that already ship: a license is an enrolled user-zk member, a paid tier is a delegated capability grant, and the license fee is the tenant's on-chain billing. If you haven't already, read Concepts for the deed mental model this builds on.

The runnable reference this guide describes lives at examples/licensing/ and crates/grantor-verify/tests/licensing_e2e.rs (just licensing-e2e).

Check a license, at a glance#

The licensed software's side — verify, then gate the paid tier:

// the licensed software: verify the presented license deed locally against
// the public registry — no license server anywhere in this call.
const { claims, grants } = await verifier.verifyWithCapabilityAt(deedJson, challenge, now);

// pseudonym-only: the one line the software is allowed to log about who is
// running it. No wallet address, no Semaphore identity, no commitment.
console.log(`valid license for tenant ${claims.tenant} — basic unlocked (sub=${claims.sub})`);
import { CapabilityGuard } from "@grantor/verify";

// gate a pro-only feature on the vendor's delegated tier grant. A basic-only
// license (no grant attached) is DENIED here with CapabilityDenied — the
// license itself is still perfectly valid, it just carries no pro grant.
CapabilityGuard.authorize(grants, {
  res: "app://features/pro", act: "use", args: { tier: "pro" }, now,
});

grants is [] for a basic-only license, so the same verifyWithCapabilityAt call is safe to run on every check — a feature that needs no tier just never calls CapabilityGuard.authorize at all. See Verify a deed and Capabilities for what each call does underneath.

A license is a user-zk enrollment#

Issuing a license is enrolling a licensee's public commitment in the vendor's own on-chain user tree — the exact mechanism User gating documents for anonymous, allowlisted human login, reframed here as "issuing a license":

  1. The licensee derives an identity from a wallet signature (ZkUser.fromWalletSignature) and hands the vendor the public commitment() — the signature itself never leaves their machine.
  2. The vendor calls registerZkUser(tenantId, commitment) — this is issuing the license.
  3. Each time the licensed software runs, it syncs the vendor's user tree from chain, proves membership, and mints a user-zk deed bound to a challenge the software itself issued (ZkUser.mintUserDeed).

The software verifies that deed with the exact same verify_deed / verifyWithCapabilityAt call every other deed mode goes through — there is no license-specific verification path.

The pro-tier grant#

A paid tier is a capability the vendor delegates to that specific license, not a second credential. The vendor's registered agent key is the delegation root (the same registerAgentKey requirement every capability chain has); it signs a Delegation naming the tier and handing it to the licensee's own user-zk sub:

{
  "res": "app://features/*",
  "act": ["use"],
  "cav": { "nb": { "tier": "pro" } }
}

The delegation's last to equals the license's own proven sub — the capability chain's binding rule — so the tier is cryptographically bound to this license, not a transferable second token. The licensee's software attaches it with mintCapabilityDeed and presents one deed carrying both the license and the tier; the vendor's software never issues or checks a separate "tier credential."

Revocation — two independent channels#

Licensing has two separate revocation levers, each proven live against its own chain deployment:

  • Revoking the license itself: the vendor calls revokeZkUser(tenantId, commitment). The next time the licensed software checks, the deed fails closed — StaleRoot — because verify_deed checks root recency on every request, not just at mint time. There is no grace period.
  • Revoking a tier, in bulk, instantly: the vendor calls bumpEpoch on the tier grant's delegation cohort. Every deed carrying a link signed against the old epoch value is refused — EpochRevoked — while the license's own user-zk membership is completely untouched: a licensee who loses the pro tier this way keeps their basic license working.

These are two distinct mechanisms, not one revocation flow with two outcomes: revoking a license does not touch any tier cohort, and bumping a tier's epoch does not touch membership. Pick whichever matches what you're actually taking away — the whole license, or just a paid tier.

Anonymity#

The vendor learns "a valid licensee is running this" plus a stable, app-scoped pseudonym (claims.sub) — never a wallet address, never the licensee's underlying Semaphore identity or commitment. The same licensee gets the same pseudonym on every check against the same software, so it is a safe primary key for per-license state, but it reveals nothing about which enrolled member it belongs to. This is the differentiated pitch over a conventional license-key or account-based scheme: the vendor cannot build a profile of who is running licensed copies, only how many valid ones are active.

Billing is the license fee#

There is no separate payment processor and no license-issuing backend to keep funded. The vendor's tenant billing (topUp/drawPeriod on GrantorRegistry) is the license revenue: a tenant that is Active or in its Grace window issues and verifies licenses normally; a tenant that lapses goes Inactive, and every license — even one for a genuinely enrolled, never-revoked member — starts failing TenantInactive on its next check. Reactivating the tenant (drawing a period) makes every outstanding license start verifying again immediately, with nothing to reissue.

Also possible: per-deployment licensing with agent-zk#

Everything above uses user-zk — an anonymous per-user license, one enrollment per human. The same model works with agent-zk instead, for a per-deployment or per-server license (one enrollment per install rather than per person) — same revocation, same tier-via-delegation composition, same billing gate. This variant is a straightforward substitution of the membership tree, not a different mechanism, but it is not what the reference proves live; only the user-zk shape above has a running end-to-end test.

⚠️ Not to be confused with an enterprise dedicated-registry license — an unrelated concept that reuses the word "license" for a different thing: this page is about YOUR product's end-user software licenses, built as a composition of Grantor primitives on the shared canonical registry; the enterprise page is about Grantor's own operator-signed grant that lets an SDK point at a custom registry address at all.

See also#

  • User gatinguser-zk, the enrollment/revocation mechanism a license is built on.
  • Capabilities — the delegated-grant mechanism a tier is built on, including bumpEpoch and the full delegation-chain rules.
  • Agent tokensagent-zk, the per-deployment variant.
  • Verify a deed — the relying-party side in general.
  • Errors — every error code a relying party branches on (StaleRoot, EpochRevoked, TenantInactive, CapabilityDenied).
  • Enterprise registries — a different, unrelated "license": Grantor's own operator-signed grant for a custom registry deployment.

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