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":
- The licensee derives an identity from a wallet signature (
ZkUser.fromWalletSignature) and hands the vendor the publiccommitment()— the signature itself never leaves their machine. - The vendor calls
registerZkUser(tenantId, commitment)— this is issuing the license. - Each time the licensed software runs, it syncs the vendor's user tree from chain, proves membership, and mints a
user-zkdeed 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— becauseverify_deedchecks root recency on every request, not just at mint time. There is no grace period. - Revoking a tier, in bulk, instantly: the vendor calls
bumpEpochon the tier grant's delegation cohort. Every deed carrying a link signed against the old epoch value is refused —EpochRevoked— while the license's ownuser-zkmembership is completely untouched: a licensee who loses theprotier this way keeps theirbasiclicense 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 gating —
user-zk, the enrollment/revocation mechanism a license is built on. - Capabilities — the delegated-grant mechanism a tier is built on, including
bumpEpochand the full delegation-chain rules. - Agent tokens —
agent-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.