Public door
Any wallet may mint — permissionless by design. The deed proves the tenant's bill is paid; authorization past that is your own layer.
agents & wallets · oauth 2.0 / oidc
Agent authority infrastructure — with no authorization server.
A static API key is all-or-nothing: whoever holds it can do everything, until you rotate it and break everyone. A deed is bounded authority — scoped to named actions, short-lived, revocable on-chain — and an agent can hand a sub-agent a narrower slice, never a wider one.
People sign in the same way — wallet or passkey, arriving as a pairwise pseudonym, never an email — and everything lands as a standard OIDC token your stack already verifies.
human or agent checks the chain, then mints a deed — your app verifies against the same chain
the whole idea
A grant is a tree. Every hand-off can shrink what a sub-agent may do — scope, uses, time — and can never grow it. Revoke once, on-chain, and the whole subtree dies.
you
└─ grant search, fetch · 20 uses · 60 min
└─ research agent
└─ delegate search · 5 uses · 10 min — narrower ⇒ verifies
└─ worker
├─ search → ALLOWED
├─ fetch → DENIED not delegated to this hop
└─ write → DENIED never granted anywhere
# one on-chain call revokes the cohort — every deed below it dies at once
agent → GET https://api.example/tools # no credential
← 401 WWW-Authenticate: Grantor-Deed realm="https://api.example",
discovery="/.well-known/grantor-deed"
agent discovery → challenge → mints a bounded deed # no signup, no human
agent → GET https://api.example/tools X-Grantor-Deed: eyJ…
← 200 # verified in-process, against the chain
agent delegate search · 5 uses · 10 min → sub-agent # narrower ⇒ verifies
sub → search "release notes" → ALLOWED
sub → delete_everything → DENIED CapabilityDenied — no effective grant
you grantor-mcp revoke --child sub # one on-chain call
sub → search "release notes" → DENIED EpochRevoked — the subtree is dead
# that's grantor.
Watch a denial happen in your own terminal, zero setup:
npx -y @grantor/mcp demo ·
the 60-second guide → · or
the five-act browser demo →
00 — the whole thing
No SDK ceremony, no callback dance, no service to call. You hold a deed the caller presented; you check it against the chain; you get claims back.
// npm i @grantor/verify
import { DeedVerifier, Registry } from "@grantor/verify";
// Registry.canonical() resolves the on-chain registry for you — no address to pin.
const verifier = new DeedVerifier(RPC, Registry.canonical(), CHAIN_ID, TENANT, AUDIENCE, ORIGIN, 120, 30, false, now);
// the challenge is yours — your nonce, your session store, single-use
const claims = await verifier.verifyAt(deed, challenge, now);
// membership, revocation and billing were all checked against the
// chain inside that one call.
// then mint your session JWT — yours, or one call:
// const jwt = sessionJwt(claims.sub, AUD, TENANT, YOUR_SIGNING_KEY, ...);
// your OIDC stack carries on unchanged either way.
# pip install grantor-verify
from grantor_verify import DeedVerifier, registry
verifier = DeedVerifier(RPC, registry.canonical(), CHAIN_ID, TENANT, AUDIENCE, ORIGIN, 120, 30, False, now)
# the challenge is yours — your nonce, your session store, single-use
claims = await verifier.verify_at(deed, challenge, now)
# then mint your session JWT — yours, or one call:
# jwt = session_jwt(claims, YOUR_SIGNING_KEY, ...)
# your OIDC stack carries on unchanged either way.
// go get chaingrantor.com/grantor-verify-go
v, err := verify.NewDeedVerifier(rpc, verify.RegistryCanonical(), chainId, tenant, audience, origin, 120, 30, false, now)
// the challenge is yours — your nonce, your session store, single-use
claims, err := v.VerifyAt(deed, challenge, now)
// then mint your session JWT — yours, or one call:
// jwt, err := verify.SessionJwt(claims, YOUR_SIGNING_KEY, ...)
// your OIDC stack carries on unchanged either way.
// Cargo.toml: grantor-verify = { features = ["sovereign-chain"] }
use grantor_verify::sovereign_gate::verify_deed;
// the challenge is yours — your nonce, your session store, single-use
let claims = verify_deed(
&deed, &policy, &challenge, now, &gate,
).await?;
// then mint your session JWT — yours, or one call:
// let jwt = session_jwt(&claims, YOUR_SIGNING_KEY, ...)?;
// your OIDC stack carries on unchanged either way.
// an agent mints its own deed — no token endpoint, no issuer
const deed = await agent.mintDeed(
RPC, Registry.canonical(), TENANT,
audience,
origin, // where YOU are
challenge, // yours
now + 60,
// vouch — before proving
vouchSig, vouchEpoch, vouchExp,
false, now,
);
// attach it to the request; you verify it the same way every time
fetch(url, { headers: { "X-Grantor-Deed": deed } });
// the key must be registered on-chain for this tenant.
Same call in every SDK — TypeScript, Python, Go and Rust today, more as bandwidth allows. grantor-verify is a library, not an endpoint.
Build without spending a cent: docker run grantor-devnet stands up the whole thing on your machine — registry, funded tenant, vouch — and hands your SDK a config to point at. Develop locally →
01 — agent-native
No sales call, no human approving a ticket. An agent mid-task reads a machine spec and wires itself in — keys registered, scoped and revoked on-chain. The agent's proof is the credential; there's no endpoint to call.
# sovereign: there is no issuer, and no token endpoint
GET https://api.your-company.example/challenge
# ← { "challenge": "b1c4…" } the RP's own nonce
# the agent proves membership of the on-chain tree, in zero knowledge,
# bound to that challenge — the proof IS the credential
GET https://api.your-company.example/resource
X-Grantor-Deed: eyJtb2RlIjoiYWdlbnQtemsi…
# ← 200. verified in-process, against the chain. nothing else ran.
MCP authorization is optional — but when a server implements it, the spec's route is
standing behind an OAuth 2.1 authorization server. A deed-gated server installs a
library instead: DeedGuard verifies the credential in-process, against
the same on-chain registry that gates billing. Two doors in.
And for the other direction — what your own sub-agents are allowed to do —
the @grantor/mcp permission broker
replaces handing them your credentials. One command wraps any stdio MCP server,
unmodified, in an enforced grant — named tools, a use budget, an expiry:
npx -y @grantor/mcp wrap --tools search,fetch --max-uses 20 --ttl-secs 3600 -- npx some-mcp-server
Sub-agents narrow the grant onward, never widen it; escalations are
denied; revocation is one on-chain call. Zero-setup sandbox, live against
the production registry: npx -y @grantor/mcp serve
Any wallet may mint — permissionless by design. The deed proves the tenant's bill is paid; authorization past that is your own layer.
Only a commitment the tenant admin registered can produce a proof.
REQUIRE_MODE=agent-zk refuses anything else at the exchange.
const g = grantorExpress({
verifier,
app,
challengeEndpoint: "/auth/challenge",
chainId: CHAIN_ID,
modes: ["user-sig", "agent-zk"],
vouchSignature: VOUCH_SIGNATURE,
vouchEpoch: VOUCH_EPOCH,
vouchExp: VOUCH_EXP,
});
app.get("/auth/challenge", g.challenge);
Read the MCP guide → — the quickstart runs the whole proof on a local chain: just mcp-e2e.
For agents you build or control — off-the-shelf MCP hosts (Claude Desktop et al.) speak spec OAuth.
02 — proof
Every mode below ships today. Underneath is the whole exchange — what the caller runs, what crosses the wire, and what you run.
| who's authenticating | sovereign |
|---|---|
| human | wallet-derived, app-scoped key user-sig |
| human | passkey, browser-origin-bound user-passkey |
| smart wallet | EIP-1271 contract signature (Safe & co) user-1271 |
| human | zero-knowledge proof of your user allowlist user-zk |
| agent | zero-knowledge membership proof agent-zk |
// an agent, or a wallet
const deed = await agent.mintDeed(
RPC, Registry.canonical(), TENANT,
audience,
origin, // where YOU are
challenge, // yours
now + 60,
// vouch — before proving
vouchSig, vouchEpoch, vouchExp,
false, now,
);
// no issuer was called
{
"v": 1,
"mode": "user-sig",
"tenant": 7,
"aud": "https://app.example",
"challenge": "rp-challenge-1",
"exp": 123456,
"sub": "794b8e61a658…",
"pubkey": "02881ee55896…",
"signature": "d33b8f4077…"
}
mode is user-sig or
agent-zk. sub is recomputed from the key
on verify, never trusted from the wire. No issuer signed any of it.
// 1. a challenge you remember
const challenge = randomUUID();
// 2. check what comes back
const claims = await v.verifyAt(
deed, challenge, now,
);
// throws ChallengeMismatch,
// Expired, BadProof, StaleRoot,
// TenantInactive, Chain
// 3. your session, your rules
setCookie(mySign(claims.sub));
A revoked agent is rejected. An unpaid tenant is rejected. A replayed challenge is rejected. Enforced by a public contract and your own verify call.
Signing up is the same story: createTenant on that public contract, then
USDC to it. No form, no account, no server of ours in either path. Our own dashboard is no
exception — it signs in with a deed (admin-sig) through
grantor-verify, on its own walled-off entry point: an ordinary relying party
cannot accept an admin deed, by design.
beyond identity
Not three more products — the same deed, carrying more. One credential can confer bounded authority, prove private membership, or stand in for a software license — and your app checks all of it with the same local verify call it already makes.
I need to grant scoped authority, not just prove identity.
A deed can confer bounded, delegable, revocable authority. Scope it to specific actions, then hand a narrowed slice to a sub-agent — which can only ever attenuate it, never widen it. Verify the whole chain locally; revoke a cohort instantly with one on-chain call. The delegator can even stay hidden — an enrolled member delegated this, not which one — and separate chains can be made unlinkable from each other. Ship it today as an MCP tool: the @grantor/mcp broker gives your agent fleet exactly this, one npx away.
Members-only — but private by construction.
A human proves membership of your allowlist in zero knowledge. Your app learns “an enrolled member logged in” plus a stable pseudonym — never which member. Enroll a commitment on-chain, revoke it on-chain; there's no directory of who your members are, anywhere.
anonymous gating →License my software without a license server.
A license is a deed. Your software verifies it with a local call — no license server, no phone-home — revokes on-chain, and rides paid tiers on delegated capability grants. You learn “a valid licensee is running this,” not who.
software licensing →wrap / serve) & DeedGuardStructure-hiding multi-hop delegation (zk-chain): one proof folds the whole chain, hiding even the hop count and the intermediaries. Novel cryptography awaiting an independent circuit audit and a multi-party setup ceremony. Explore it; don't yet rely on it for production authority.
03 — pricing
There's nothing to buy but capacity — apps, signing keys, agents, team members — metered on-chain, the same way for every tenant. Your tenant is a row in a public contract.
A real taste — the whole product, on-chain.
For devs shipping to production.
For growing products & agent fleets.
Commercial terms and a human to call.
An OAuth token says this client has permission. A deed can say this agent holds a narrowed slice of that permission, received from another agent — delegation and attenuation live in the credential, no child ever exceeds its parent, and the whole chain checks in one local call. And no authorization server had to mint it.
You're buying standing in the registry, not a service — verification itself is gated by it, and a tier is capacity. The public record anyone can check is the part worth paying for.
Nothing. No process of ours runs in your auth path; the trust anchor is a public contract, readable by anyone. No auth server, no auth company to withdraw. It lives on Base today, and the verifier is chain-agnostic — another chain is one more entry in the canonical map.
Invariant + fuzz coverage on the contract, end-to-end suites on the stack, no runtime dependency on us to fail. Don't trust it — run the suites first.
Topping up credits your tenant, it doesn't pay us — funds sit in the contract, and withdrawBalance returns anything undrawn to an address you choose. Only periods you actually consumed are ours. There is no minimum, no lock-in and nobody to email, because the withdrawal is a function call and the balance was never in our custody. The registry has no owner power to touch it: an invariant test asserts its USDC holdings always equal the sum of tenant balances. Fund once and forget it: periods draw themselves from your prepaid balance — no transactions, no gas, nothing to remember until your next top-up.
Billed on-chain in USDC per active period — rates are published in the registry contract, readable before you pay a cent. Free tenants are capped and expire a fixed window after creation, anchored to signup so switching tiers doesn't reset it. New to USDC? Fund with a card — card → USDC on Base in ~15 minutes; KYC with the exchange, never with us. Or pay in any liquid token — ETH, WETH, USDT, DAI, cbBTC — live on Base today: swapped to USDC on-chain, inside your own transaction.