docs / guide

view as .md

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.com fails at admin.example.com. A malicious RP cannot replay what it receives.
  • Challenge-bound. Your app picks the challenge; the caller cannot. This is the OIDC nonce shape 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#

ModeWho holds itProvesOn-chain check
user-siga human with a walletcontrol of a wallet-derived, app-scoped keytenant billing status
agent-zkan enrolled agentZK membership of the tenant's registry, without revealing which memberroot recency (revocation) + billing
admin-siga Grantor dashboard admincontrol of a wallet addressnone, 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.

  • topUp credits the tenant's balance (it does not pay the operator).
  • drawPeriod moves 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.
  • withdrawBalance returns 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.

TypeScriptPythonGoRust
user-sig holder
agent-zk holder (ZK proving)
admin-sig holder
Deed verification
Deed guard

The polyglot split#

  • On-chain = Solidity. GrantorRegistry is 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.

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