Three bad options, one bad assumption
Static API keys, an auth SaaS, or building it yourself — all three assume the caller is a person. Yours isn't.
wallets & agents · oauth 2.0 / oidc
OAuth with no authorization server.
Agents authenticate with a key they already hold — scoped to one audience, short-lived, revocable on-chain. People sign in with a wallet they already have — no email, no password, nothing personal to store or leak. Both arrive as a standard OIDC token your stack already verifies.
human or agent checks the chain, then mints a deed — your app verifies there too, no issuer, ever
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 } from "@grantor/verify";
const verifier = new DeedVerifier(RPC, REGISTRY, TENANT, AUDIENCE, ORIGIN, 120, 30);
// the challenge is yours — your nonce, your session store, single-use
const claims = await verifier.verifyAt(deed, challenge, now);
// then mint your own JWT; your OIDC stack carries on unchanged.
// membership, revocation and billing were all checked against the
// chain inside that one call.
# pip install grantor-verify
from grantor_verify import DeedVerifier
verifier = DeedVerifier(RPC, REGISTRY, TENANT, AUDIENCE, ORIGIN, 120, 30)
# the challenge is yours — your nonce, your session store, single-use
claims = await verifier.verify_at(deed, challenge, now)
# then mint your own JWT; your OIDC stack carries on unchanged.
// go get authgrantor.com/grantor-verify-go
v, err := verify.NewDeedVerifier(rpc, registry, tenant, audience, origin, 120, 30)
// the challenge is yours — your nonce, your session store, single-use
claims, err := v.VerifyAt(deed, challenge, now)
// then mint your own JWT; your OIDC stack carries on unchanged.
// Cargo.toml: grantor-verify = { features = ["sovereign"] }
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 own JWT; your OIDC stack carries on unchanged.
// an agent mints its own deed — no token endpoint, no issuer
const deed = await agent.mintDeed(
RPC, REGISTRY, TENANT,
audience,
origin, // where YOU are
challenge, // yours
now + 60,
// vouch — before proving
vouchSig, vouchEpoch, vouchExp,
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 each SDK — TypeScript, Python, Go and Rust today, more as bandwidth allows. grantor-verify is a library — there is no endpoint, and nothing of ours is running.
which one are you?
“My agents need to call APIs.”
Today that's one static key per agent. Instead: a short-lived token scoped to one audience, minted from a key registered on-chain — revoke one agent without touching the rest.
agent auth →“My users are wallets, not emails.”
Sign-in is a wallet signature. Your app receives a pairwise pseudonym — no email, no password, not even the wallet address. Nothing personal to store, reset or leak.
the proof →“I'm shipping an MCP server, or a tool API for agents.”
They present a deed — a credential that proves itself. You verify it in-process against a public registry, issuing no keys and storing no secrets. No authorization server to stand up.
gate an mcp server →“Nothing third-party in my auth path.”
Nobody runs anything — the credential verifies against the public chain. No service of ours ever sits between your users and your API.
how it deploys →01 — why
Static API keys, an auth SaaS, or building it yourself — all three assume the caller is a person. Yours isn't.
Your app receives a pairwise pseudonym — never an email, a password, or the wallet address. Nothing to breach, nothing to reset. Sovereign sign-in scopes it per app, so it is unlinkable across your other apps too.
The credential is a deed: a self-proving instrument backed by a public register of record, not by a server you have to trust. Your app adds grantor-verify, checks the deed against the registry, and mints its own JWT — no crypto in your request path, no SDK lock-in.
02 — proof
Every cell below ships today. Underneath is the entire exchange — what the caller runs, what crosses the wire, and what you run. Three moving parts, none of them ours.
| who's authenticating | sovereign |
|---|---|
| human | wallet-derived, app-scoped key user-sig |
| agent | zero-knowledge membership proof agent-zk |
// an agent, or a wallet
const deed = await agent.mintDeed(
RPC, REGISTRY, TENANT,
audience,
origin, // where YOU are
challenge, // yours
now + 60,
// vouch — before proving
vouchSig, vouchEpoch, vouchExp,
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 — with none of our infrastructure running, because there isn't any.
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.
03 — agent-native
No sales call, no human approving a ticket. An agent mid-task reads a machine spec and wires itself in. Keys are registered, scoped and revoked on-chain — verifiable by anyone, controlled by no one. There's no endpoint to call at all: the agent's proof is the credential.
# 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.
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.
04 — pricing
There's nothing to buy but capacity. Your tenant lives in a public contract, and a tier is simply capacity — apps, signing keys, agents, team members — metered the same way on-chain for every tenant.
A real taste, on-chain. No card.
For devs shipping to production.
For growing products & agent fleets.
Commercial terms and a human to call.
You're not buying a service — there isn't one running. You're buying standing in the registry: verification itself is gated by it, and a tier is simply capacity. Software is the easy part; standing on a public record anyone can check is the part worth paying for.
Nothing. In sovereign mode no process of ours runs in your auth path, and the trust anchor is a public contract readable from any RPC, by anyone. No auth server. No auth company. That's an architecture, not a slogan — there is no service to withdraw.
The contract carries invariant and fuzz coverage, the verifier and the full stack ship end-to-end suites, and the sovereign path has no runtime dependency on us to fail. Don't take it on trust — run the suites before you commit a line of your own code.
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.
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; the window is anchored to signup, so switching tiers doesn't reset it. Long enough to ship something real, short enough to stay a trial rather than a permanent free tier.
get started
Add the verifier library — that's the integration. The free tier is a real, capped tenant registered on-chain — no card, no hosted sign-up. Scale up whenever you're ready; same registry, same tenant.
agents → /llms.txt · enterprise terms: get in touch