Wallet login (user-sig)#
A human signs in with their wallet and your app receives a stable, app-scoped pseudonym — not their address. There is no browser redirect, no issuer, and no session-granting server in between: the wallet signs, the SDK mints a deed, and your app verifies it directly against the on-chain registry. If you haven't already, read Concepts for the mental model.
The flow#
- Your app issues a challenge —
GET /challengeon your own deed guard. - The wallet signs. The SDK has the wallet sign a human-readable message naming the origin (twice, compared for determinism) to derive a
root_seed, then derives a per-(tenant, origin)app key from it and signs the deed with that key. One wallet popup per origin, not one for the whole web. - Your app verifies the deed with
DeedGuard/DeedVerifierand mints its own session, however it already does. See Verify a deed.
The full walkthrough — the exact derivation (derive_root_seed → derive_app_key → mint_user_deed), what is and is not in the key-derivation scope (tenant + origin, deliberately not aud), and what a sub looks like across audiences/origins/tenants — is documented once, in Sovereign tier § User login (user-sig). Read that rather than a second copy here.
TypeScript: signInWithDeed#
@grantor/sdk exports signInWithDeed, which composes the wallet-signing steps above (and the origin-provenance check, below) into one call:
import { signInWithDeed } from "@grantor/sdk";
const deed = await signInWithDeed({
signMessage, // wraps the wallet's personal_sign
tenantId,
audience,
origin, // the origin YOU are actually running on
challenge, // from your app's own /challenge
exp,
vouch, // the RP's published origin_vouch
chainRegistry: REGISTRY,
chainReader,
});
Python, Go and Rust expose the same steps (deriveRootSeed/deriveAppKey/ mintUserDeed, in their language-specific casing) as separate calls rather than one wrapper. The Rust snippet in Sovereign tier § User login is the source of truth; sdk/README.md maps the equivalent calls per language.
Origin provenance: required before signing#
signInWithDeed (and authenticate(), the equivalent agent-side helper) refuses to sign before it signs anything — not after — if the origin cannot show a tenant admin's on-chain vouch that it speaks for (tenant, audience). This is what stops a hostile origin that republishes a victim tenant's tenant/audience from harvesting a pseudonym by simply asking a holder to sign in. chainReader/chainRegistry are REQUIRED — omitting either is a fail-closed error, not a skipped check. See Sovereign tier § Origin provenance and § Constructing a chainReader.
What this proves — and what it does not#
user-sig proves "I control a key", not "I control a wallet". Permissionless login is open by definition — anyone can generate a key. The wallet's role is portability of identity across devices, not gatekeeping. There is no on-chain user revocation, because there is no on-chain user state to revoke; an RP that wants to ban someone bans the sub on its own side.
personal_sign also means the wallet cannot tell who is asking: a hostile page can request the victim's origin-scoped message, and the only control left is a human reading a prompt that names an origin they are not on. See Sovereign tier § What S1 cost, and what it did not buy for the full residual-risk statement — do not describe this mode as a hard phishing-resistance guarantee.
See also#
- Verify a deed — the relying-party side.
- Agent tokens — the
agent-zkdeed, for enrolled agents. - Sovereign tier — the full reference.
- Errors — every error code a relying party branches on.