LazorKit LogoLazorKit
Concepts

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 rent

Sessions 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.

ActionEnforces
SolLimitLifetime SOL cap — decrements until exhausted.
SolRecurringLimitSOL cap per window (e.g. 1 SOL/day). Resets each window.
SolMaxPerTxMax SOL gross outflow per execute.
TokenLimitLifetime token cap per mint.
TokenRecurringLimitToken cap per window per mint.
TokenMaxPerTxMax tokens per execute per mint.
ProgramWhitelistSession can only CPI to this program (repeatable).
ProgramBlacklistSession 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 typeOn expiry
Spending limits (SOL/token)Treated as fully exhausted — any spend rejected.
ProgramWhitelistHard deny — all programs blocked.
ProgramBlacklistSilently 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

MetricImmediate ExecuteDeferred (2 txs)
Inner instruction capacity~574 bytes~1,100 bytes (1.9×)
Total CU (post Phase 2.1)~9,495~15,667
Tx fee0.000005 SOL0.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

ItemCost
Session account rent (one-time)0.001448 SOL
CreateSession tx fee0.000005 SOL
Total setup0.001453 SOL
Execute via session (per tx)0.000005 SOL

Session rent is refundable after expiry or revocation via RevokeSession.


Troubleshooting session errors

ErrorCause
0xbd0 ActionSolLimitExceededLifetime SOL cap exhausted — revoke + recreate with higher solLimit.
0xbd1 ActionSolRecurringLimitExceededWindow cap hit. Wait for reset or raise solRecurringLimit.
0xbc1 SessionExpiredSession past its expiresAtSlot — close via revokeSession, make a new one.
0xbcd ActionProgramNotWhitelistedCPI target not in the session's whitelist — add the program or remove the filter.

Full error map in Troubleshooting.