Concepts#
The mental model. If you read one page, read this one.
Grantor's premise is that a credential can certify itself. A caller mints a deed that proves what it needs to prove, and your app checks it against a public on-chain registry — with no authorization server in between, because there is nothing in between at all.
What a deed is#
A deed is a small JSON envelope carrying a proof, an audience, a challenge and an expiry. It is self-certifying: everything needed to check it is either inside it or on a public chain. Nothing issued it, so nothing can forge it, observe it, or link its uses.
{ "v": 1,
"mode": "user-sig", // or "agent-zk" / "admin-sig"
"tenant": 42,
"aud": "api.example.com", // who it is FOR — a deed for someone else is invalid here
"challenge": "9f2c…", // the value YOUR app issued, one use only
"exp": 1753700000,
… per-mode proof material … }
Three properties do the work:
- Audience-bound. A deed minted for
api.example.comfails atadmin.example.com. A malicious RP cannot replay what it receives. - Challenge-bound. Your app picks the challenge; the caller cannot. This is the OIDC
nonceshape and it is the entire replay defence. - Expiring. Short-lived by construction.
The exchange#
your app caller
│ 1. challenge (random, single-use) ──────▶ │
│ │ 2. sign / prove against it
│ ◀────────────────────────── 3. the deed │
│
│ 4. verify_deed(deed, policy, challenge, now, gate)
│ ├─ cryptography: signature or ZK proof
│ ├─ audience, challenge, expiry
│ └─ eth_call ─────▶ GrantorRegistry ── is the tenant paid up?
│ is the member un-revoked?
│ 5. mint your own session, however you already do
Step 4 is a read. It costs no gas, needs no API key, and any RPC provider serves it. Step 5 is yours — Grantor has no opinion about your session format and never sees it.
Three kinds of deed#
| Mode | Who holds it | Proves | On-chain check |
|---|---|---|---|
user-sig | a human with a wallet | control of a wallet-derived, app-scoped key | tenant billing status |
agent-zk | an enrolled agent | ZK membership of the tenant's registry, without revealing which member | root recency (revocation) + billing |
admin-sig | a Grantor dashboard admin | control of a wallet address | none, deliberately |
user-sig and agent-zk are what your app accepts, and one call — verify_deed — handles both. admin-sig authenticates Grantor's own dashboard admins and verifies through a separate entry point; verify_deed rejects it outright. That isolation is structural rather than configurable: there is no flag to flip. See why.
Pseudonymous by construction#
Your app receives a pseudonym, not a wallet address.
For user-sig, the wallet signs one fixed domain string to derive a root_seed, and per-app keys come from it:
app_key = HKDF(root_seed, "tenant:{tenant}|audience:{aud}")
sub = derived from the app key's public key
Because the tenant and audience are both in the derivation scope, the same wallet yields a different, unlinkable sub at every app — pairwise subjects computed rather than stored. The verifier recomputes sub from the public key in the deed rather than trusting a claim, so it cannot be forged.
The seed derivation signs twice and compares: a wallet with a non-deterministic signature fails loudly instead of silently re-identifying its owner as somebody new on every login.
For agent-zk the proof reveals membership without revealing which member, so the agent is anonymous even to the tenant that enrolled it. admin-sig is the deliberate exception — it reveals the address, because administration is an identified context, not an anonymous one.
The trust anchor: GrantorRegistry#
A Solidity contract that answers, publicly:
- Is this tenant paid up?
status(id)→Active/Grace/Inactive. - Is this agent still a member? via the membership tree's current root.
Anyone can call it from any RPC provider with no API key and no operator in the loop. Trust is a public fact rather than a row in a company database — which is what lets the verifier be a library instead of a service.
Tenants, tiers, and billing#
A tenant is a billing and ownership unit, represented on-chain as an ERC-721 "org pass." It has a tier — a capacity plan (Free/Pro/Scale/ Enterprise), not a deployment mode — a prepaid balance, a set of registered keys, and a derived status.
Billing is a flat subscription with prepaid funding, not usage metering. The tier caps capacity, not consumption — and it must, because deed verification is an eth_call the contract cannot observe. Metering deeds would require your app to report back, which would reintroduce exactly the server this product removes.
topUpcredits the tenant's balance (it does not pay the operator).drawPeriodmoves one period's fee to the treasury. It is permissionless — a keeper, a bot or anyone may call it once the period lapses, which is what makes renewal unattended. The balance is the renewal engine.withdrawBalancereturns undrawn balance. Only consumed periods are non-refundable; unbilled prepayment is your money for service not rendered.
Grace, never a cliff. When balance runs out a tenant enters Grace before Inactive; existing credentials keep working throughout. Even at Inactive, deeds already minted stay valid until their own exp — going unpaid never invalidates a credential already in the world.
A tenant that tops up but never draws keeps its balance and gets nothing: status stays Inactive until a period is drawn, so funds are never held while the product is in use.
Every capability, every language#
All languages or no go — a capability that ships in one language has not shipped. just capability-matrix enforces it against the generated binding surfaces, not the Rust source, so it measures what a consumer actually installs.
| TypeScript | Python | Go | Rust | |
|---|---|---|---|---|
user-sig holder | ✅ | ✅ | ✅ | ✅ |
agent-zk holder (ZK proving) | ✅ | ✅ | ✅ | ✅ |
admin-sig holder | ✅ | ✅ | ✅ | ✅ |
| Deed verification | ✅ | ✅ | ✅ | ✅ |
| Deed guard | ✅ | ✅ | ✅ | ✅ |
The polyglot split#
- On-chain = Solidity.
GrantorRegistryis money-handling code, so it gets the most conservative toolchain: Foundry, heavy invariant and fuzz coverage, minimal owner powers, an immutable treasury. - Off-chain = Rust, compiled once and shipped everywhere — wasm-bindgen for TypeScript, UniFFI for Python and Go. One implementation of the crypto, four languages, proven identical by shared conformance vectors.
See also#
Wallet login, Agent tokens and Verify a deed walk through minting and verifying each kind of deed. Sovereign tier is the full reference.