Session Keys
Scoped, time-bound delegation keys — action types, spending limits, deferred execution, and revocation.
A session key is an ephemeral Ed25519 keypair with an expiry slot and optional action constraints. It lets an app or agent execute on behalf of a wallet without a passkey prompt per transaction.
When to use session keys
Session keys are for delegation — giving an app or agent scoped permission to act on the user's behalf. You don't need one for direct user execution: a passkey owner can always sign a transaction themselves.
Reach for a session key when:
- An agent / bot executes on a schedule without the user present
- An app wants to batch or automate actions without prompting every time
- You need to constrain what an executor can do (which programs, what value, until when)
Mental model
1. Owner/Admin creates Session PDA with a fresh Ed25519 keypair + action list
2. App stores the session keypair locally (expo-secure-store, IndexedDB, etc.)
3. App signs Execute with the session key — no passkey prompt
4. Program enforces the action list on every CPI
5. Session expires at the stored slot; account can be closed to reclaim rentSessions are the cheapest execution path — ~4,105 CU vs ~9,495 CU for passkey execution, because no Secp256r1 precompile verification is involved (measured post Phase 2.1 optimizations).
Creating a session
From the React Native hook
The RN SDK uses the raw SessionAction[] catalogue for maximum flexibility:
import { Actions, Keypair, useWallet } from '@lazorkit/wallet-mobile-adapter';
const sessionKp = Keypair.generate();
const currentSlot = BigInt(await connection.getSlot());
const { sessionPda } = await createSession(
{
sessionKey: sessionKp.publicKey,
expiresAtSlot: currentSlot + 216_000n, // ~24h
actions: [
Actions.solRecurringLimit({ limit: 1_000_000_000n, window: 216_000n }),
Actions.solMaxPerTx(500_000_000n),
Actions.programWhitelist(JUPITER_PROGRAM_ID),
],
},
{ redirectUrl: 'myapp://cb' },
);Persist sessionKp.secretKey in secure storage along with sessionPda.
From the React hook
The React SDK ships a higher-level SpendingLimits wrapper for common SOL limits:
import { useWallet } from '@lazorkit/wallet';
const { sessionPda, sessionPublicKey } = await createSession({
expiresInSlots: 216_000n, // ~24h
spendingLimits: {
solLifetimeCap: 5_000_000_000n, // 5 SOL total
solPerTxMax: 500_000_000n, // 0.5 SOL per tx
solRecurring: {
limit: 1_000_000_000n, // 1 SOL per window
windowSlots: 216_000n, // 24h window
},
},
});For token caps or program filters, drop to the Actions catalogue via
LazorKitClient.createSession directly.
From the low-level SDK
import { client, ed25519, secp256r1 } from '@lazorkit/sdk-legacy';
// Ed25519 admin signs immediately:
const { instructions, sessionPda } = await client.createSession({
payer: payer.publicKey,
walletPda,
adminSigner: ed25519(ownerKp.publicKey),
sessionKey: sessionKp.publicKey,
expiresAt: currentSlot + 216_000n,
actions: [/* SessionAction[] */],
});For Secp256r1 (passkey) admins, use the two-phase prepareCreateSession /
finalizeCreateSession flow — see web3.js v1 › Session Keys.
Actions are immutable
Once a session is created, its action list can't be modified. To change limits, revoke the session and create a new one.
Actions reference
Actions constrain what a session key can do. The program validates them before and after
every CPI during Execute. Max 16 actions per session, up to 2048 bytes total.
| Action | Enforces |
|---|---|
SolLimit | Lifetime SOL cap — decrements until exhausted. |
SolRecurringLimit | SOL cap per window (e.g. 1 SOL/day). Resets each window. |
SolMaxPerTx | Max SOL gross outflow per execute. |
TokenLimit | Lifetime token cap per mint. |
TokenRecurringLimit | Token cap per window per mint. |
TokenMaxPerTx | Max tokens per execute per mint. |
ProgramWhitelist | Session can only CPI to this program (repeatable). |
ProgramBlacklist | Session cannot CPI to this program (repeatable). |
Gross outflow tracking
SolMaxPerTx uses per-CPI lamport snapshotting, not net flow. A DeFi round-trip
(swap out + swap back) can't bypass the cap by ending at the same balance.
Builder helpers
import { Actions } from '@lazorkit/wallet-mobile-adapter';
// or '@lazorkit/wallet' / '@lazorkit/sdk-legacy'
Actions.solLimit(lamports, expiresAt?)
Actions.solRecurringLimit({ limit, window, expiresAt? })
Actions.solMaxPerTx(lamports, expiresAt?)
Actions.tokenLimit({ mint, remaining, expiresAt? })
Actions.tokenRecurringLimit({ mint, limit, window, expiresAt? })
Actions.tokenMaxPerTx({ mint, max, expiresAt? })
Actions.programWhitelist(programId, expiresAt?)
Actions.programBlacklist(programId, expiresAt?)Expired action behaviour
Actions have their own expiresAt independent of session expiry.
| Action type | On expiry |
|---|---|
| Spending limits (SOL/token) | Treated as fully exhausted — any spend rejected. |
ProgramWhitelist | Hard deny — all programs blocked. |
ProgramBlacklist | Silently dropped — ban lifted. |
Common session patterns
Unrestricted session (no limits)
await createSession(
{
sessionKey: sessionKp.publicKey,
expiresAtSlot: currentSlot + 9_000n, // 1 hour
},
{ redirectUrl: 'myapp://cb' },
);No actions means "session can do anything the wallet can". Still bounded by expiry.
Jupiter-only swapper
await createSession(
{
sessionKey: sessionKp.publicKey,
expiresAtSlot: currentSlot + 216_000n,
actions: [
Actions.programWhitelist(JUPITER_V6_PROGRAM_ID),
Actions.programWhitelist(TOKEN_PROGRAM_ID), // SPL transfers for ATAs
Actions.solMaxPerTx(1_000_000_000n),
],
},
{ redirectUrl: 'myapp://cb' },
);Usage-capped session (1 SOL per day)
await createSession(
{
sessionKey: sessionKp.publicKey,
expiresAtSlot: currentSlot + 1_512_000n, // ~7 days
actions: [
Actions.solRecurringLimit({ limit: 1_000_000_000n, window: 216_000n }),
],
},
{ redirectUrl: 'myapp://cb' },
);Stablecoin-only spender
await createSession(
{
sessionKey: sessionKp.publicKey,
expiresAtSlot: currentSlot + 216_000n,
actions: [
Actions.tokenLimit({ mint: USDC_MINT, remaining: 100_000_000n }), // 100 USDC
Actions.tokenMaxPerTx({ mint: USDC_MINT, max: 10_000_000n }), // 10 USDC per tx
Actions.programBlacklist(SUSPICIOUS_PROGRAM_ID),
],
},
{ redirectUrl: 'myapp://cb' },
);Executing with a session
React Native
const sig = await signAndSendWithSession({
sessionKeypair: sessionKp,
sessionPda,
instructions: [ix],
});No passkey prompt. The Ed25519 keypair signs at transaction level.
React
const sig = await signAndSendWithSession({ instructions: [ix] });The SDK uses the session keypair it persisted internally during createSession.
Low-level
import { session } from '@lazorkit/sdk-legacy';
const { instructions } = await client.transferSol({
payer: payer.publicKey,
walletPda,
signer: session(sessionPda, sessionKp.publicKey),
recipient,
lamports: 500_000_000n,
});Deferred execution
For payloads too big to fit in a single Secp256r1 Execute transaction (~574 bytes of inner instructions — Jupiter swaps with complex routing), LazorKit provides a 2-transaction deferred flow.
From the SDKs
The React and React Native SDKs wrap both transactions into a single method that takes one passkey prompt:
// React Native
const sig = await authorizeAndExecute(
{ instructions: jupiterSwapIxs },
{ redirectUrl: 'myapp://cb' },
);
// React
const sig = await authorizeAndExecute({ instructions: jupiterSwapIxs });Trade-offs
| Metric | Immediate Execute | Deferred (2 txs) |
|---|---|---|
| Inner instruction capacity | ~574 bytes | ~1,100 bytes (1.9×) |
| Total CU (post Phase 2.1) | ~9,495 | ~15,667 |
| Tx fee | 0.000005 SOL | 0.00001 SOL |
| Temp rent (refunded to payer) | — | 0.002116 SOL |
Expired DeferredExec accounts can be reclaimed via ReclaimDeferred — the original
payer recovers the rent. Only Secp256r1 Owner or Admin can call Authorize (TX1);
TX2 can be submitted by any signer.
Revocation
Sessions can be revoked early by Owner or Admin:
// React Native
await revokeSession(
{ sessionPda, refundDestination: smartWalletPubkey },
{ redirectUrl: 'myapp://cb' },
);
// React
await revokeSession();Closes the session PDA and refunds rent. Works on active or already-expired sessions.
Use refundDestination: smartWalletPubkey (vault) to keep the funds in-wallet.
Costs
| Item | Cost |
|---|---|
| Session account rent (one-time) | 0.001448 SOL |
CreateSession tx fee | 0.000005 SOL |
| Total setup | 0.001453 SOL |
Execute via session (per tx) | 0.000005 SOL |
Session rent is refundable after expiry or revocation via RevokeSession.
Troubleshooting session errors
| Error | Cause |
|---|---|
0xbd0 ActionSolLimitExceeded | Lifetime SOL cap exhausted — revoke + recreate with higher solLimit. |
0xbd1 ActionSolRecurringLimitExceeded | Window cap hit. Wait for reset or raise solRecurringLimit. |
0xbc1 SessionExpired | Session past its expiresAtSlot — close via revokeSession, make a new one. |
0xbcd ActionProgramNotWhitelisted | CPI target not in the session's whitelist — add the program or remove the filter. |
Full error map in Troubleshooting.