wallets & agents · oauth 2.0 / oidc

Stop shipping
static API keys.

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 — your app gets a pairwise pseudonym, never an email, password, or address. Both arrive as a standard OIDC token your stack already verifies.

00 — the whole thing

Verify a deed.
That's the integration.

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 — proof

Don't take it on faith.
Read the credential.

Every mode below ships today. Underneath is the whole exchange — what the caller runs, what crosses the wire, and what you run.

Every combination ships today.
who's authenticatingsovereign
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
the caller mints a deed
// 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
the deed — this is the whole credential
{
  "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.

you verify 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.

Try it live

02 — agent-native

Your agents adopt it
themselves.

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.

  1. 1
    Fetch a challenge from your API
    your own nonce, single-use
  2. 2
    Prove membership, in zero knowledge
    against the on-chain tree
  3. 3
    Attach the deed to the request
    X-Grantor-Deed header
  4. 4
    Call the API
    verified in-process, no round trip
sovereign — no issuer, no token endpoint
# 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.

Gate an MCP server. No authorization server.

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.

user-sig

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.

agent-zk

Fleet door

Only a commitment the tenant admin registered can produce a proof. REQUIRE_MODE=agent-zk refuses anything else at the exchange.

server.mjs — mount the guard
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.

for machines If you're an AI agent, start here → /llms.txt

beyond identity

A deed carries more
than who you are.

The same self-certifying credential can carry bounded authority, prove private membership, or stand in for a software license — each checked by the same local verify call.

preview The deepest rung — structure-hiding multi-hop delegation that folds an entire chain into a single proof, hiding even the hop count and the intermediaries — ships as a preview: novel cryptography that has not yet been independently audited. Explore it; don't yet rely on it for production authority. Everything else here is live today.

03 — pricing

Pay the registry.
Not a vendor.

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.

Free
$0capped · time-boxed

A real taste — the whole product, on-chain.

  • 1 app · 1 signing key · 2 agents
  • 1 team member
  • No feature gates on Free, either
  • Expires a fixed window after signup
  • Full docs & llms.txt
Start free Available at launch
Scale
$45/mo

For growing products & agent fleets.

  • 25 apps · 10 signing keys · 250 agents
  • 25 team members
  • High volume · usage analytics
  • Grace period, never a cliff
  • Priority support
Get started Available at launch
Enterprise
Customlicense · support

Commercial terms and a human to call.

  • Commercial license & security review
  • Custom caps and terms
  • SLAs & dedicated support
  • Deployment help, in your infra
Talk to us

“Why pay you if you run nothing?”

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.

“What happens if you disappear?”

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.

“Is this production ready?”

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.

You can take your balance back.

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, anchored to signup so switching tiers doesn't reset it.