SessionKit docs
Scopes
What you can ask for, what the user sees, and what can never be granted.
A scope is one thing you're asking the user to share. You request them in
connect(); the wallet shows each one in plain language and the
user approves or declines. Nothing outside the granted set is ever sent.
The catalog
| Scope | You receive | The user sees |
|---|---|---|
identity | user.id, user.pubKey, user.proof | "Sign you in" |
profile:username | user.username | "See your username" |
profile:avatar | user.avatar, user.avatarMimeType | "See your profile picture" |
address:solana | user.solanaAddress, with a possession proof | "See your Solana address" |
address:evm | user.evmAddress, with a possession proof | "See your Ethereum address" |
favorites:read | user.favoriteMarkets | "See your favorite markets" |
sign:solana | session.request for Solana messages and transactions,
plus address:solana | "Ask you to sign Solana transactions" |
sign:evm | session.request for EVM messages and transactions,
plus address:evm | "Ask you to sign Ethereum & Polygon transactions" |
identity is added automatically — every session returns a
verifiable user, that's the point of signing in. Each
sign:* scope brings its address scope with it. The rest you
ask for explicitly.
Signing scopes
A sign:* scope keeps the encrypted channel open after approval
so your dapp can propose signatures. The wallet shows each proposal
decoded, with its BLACWALL risk verdict and your verified origin, and the
user approves every one of them. The wallet fills nonce, gas and fees
itself, and refuses blind hash signing and contract creation.
sign:evm covers transactions on Ethereum and Polygon. Messages
and typed data also work on Arbitrum One (eip155:42161).
What can never be granted
There is no trading scope, and there never will be. A SessionKit session can never act on the user's BLAC trading account: it cannot place, modify, or cancel orders, and cannot change leverage or margin. The wallet enforces this by audience, not by who you are — so it holds even if your key leaks, even if your site is compromised.
Without a sign:* scope, the worst case of a breach on your
side is the data the user explicitly agreed to share. With one, an attacker
can still only propose: nothing is signed without the user's approval in
the wallet. Requesting a scope outside the catalog throws
invalid_scope before anything is rendered.
Ask for less
Every extra scope is a reason to decline. Approval rates fall as the list grows, and unused data is a liability you have to store, secure, and delete.
- Just signing people in? Request nothing —
identityalone is a complete login. - Showing who they are? Add
profile:usernameandprofile:avatar. - Checking NFTs or token holdings? Add
address:solanaoraddress:evm. - Running a dapp that needs signatures? Add
sign:solanaorsign:evm, and nothing more.
Handling partial grants
A user can approve the login and decline an optional scope. Check
grantedScopes, and degrade rather than fail:
const session = await connect({
scopes: ["identity", "profile:username", "address:solana"],
mount, sign
});
if (session.grantedScopes.includes("address:solana")) {
await loadNfts(session.user.solanaAddress);
} else {
showConnectWalletLater(); // they're still logged in
} Adding chains
New scopes are added additively, with no protocol version bump. To request
one you update SessionKit, because connect() throws
invalid_scope for any scope this version does not know. A
scope the user's wallet does not support yet simply comes back ungranted,
so check grantedScopes from day one.