Skip to content

Getting started with Session Keys

@alchemy/aa-accounts exports all of the definitions you need to use session keys with a Modular Account. We provide a simple SessionKeySigner class that generates session keys on the client and can be used as the signer for the Multi Owner Modular Account. We also export the necessary decorators which can be used to extend your SmartAccountClient to make interacting with session keys easy.

Usage

Let's take a look at a full example that demonstrates how to use session keys with a Modular Account.

ts
import {
  SessionKeyAccessListType,
  SessionKeyPermissionsBuilder,
  SessionKeyPlugin,
  SessionKeySigner,
  sessionKeyPluginActions,
} from "@alchemy/aa-accounts";
import { createModularAccountAlchemyClient } from "@alchemy/aa-alchemy";
import { LocalAccountSigner, sepolia } from "@alchemy/aa-core";

const chain = sepolia;
// this is the signer to connect with the account, later we will create a new client using a session key signe
const signer = LocalAccountSigner.mnemonicToAccountSigner("MNEMONIC");
const sessionKeySigner = new SessionKeySigner();
const client = (
  await createModularAccountAlchemyClient({
    chain,
    apiKey: "ALCHEMY_API_KEY",
    signer,
  })
).extend(sessionKeyPluginActions);

// 1. check if the plugin is installed
const isPluginInstalled = await client
  .getInstalledPlugins({})
  // This checks using the default address for the chain, but you can always pass in your own plugin address here as an override
  .then((x) => x.includes(SessionKeyPlugin.meta.addresses[chain.id]));

// 2. if the plugin is not installed, then install it and set up the session key
if (!isPluginInstalled) {
  // lets create an initial permission set for the session key giving it an eth spend limit
  const initialPermissions = new SessionKeyPermissionsBuilder()
    .setNativeTokenSpendLimit({
      spendLimit: 1000000n,
    })
    // this will allow the session key plugin to interact with all addresses
    .setContractAccessControlType(SessionKeyAccessListType.ALLOW_ALL_ACCESS)
    .setTimeRange({
      validFrom: Math.round(Date.now() / 1000),
      // valid for 1 hour
      validUntil: Math.round(Date.now() / 1000 + 60 * 60),
    });

  const { hash } = await client.installSessionKeyPlugin({
    // 1st arg is the initial set of session keys
    // 2nd arg is the tags for the session keys
    // 3rd arg is the initial set of permissions
    args: [
      [await sessionKeySigner.getAddress()],
      ["0x0"],
      [initialPermissions.encode()],
    ],
  });

  await client.waitForUserOperationTransaction({ hash });
}

// 3. set up a client that's using our session key
const sessionKeyClient = (
  await createModularAccountAlchemyClient({
    chain,
    signer: sessionKeySigner,
    apiKey: "ALCHEMY_API_KEY",
    // this is important because it tells the client to use our previously deployed account
    accountAddress: client.getAddress(),
  })
).extend(sessionKeyPluginActions);

// 4. send a user operation using the session key
const result = await sessionKeyClient.executeWithSessionKey({
  args: [
    [{ target: "0x1234", value: 1n, data: "0x" }],
    await sessionKeySigner.getAddress(),
  ],
});

Breaking it down

Determine where the session key is stored

Session keys can be held on the client side or on a backend agent. Client side session keys are useful for skipping confirmations, and agent side keys are useful for automations.

In the above example, we use a client-side key using the SessionKeySigner exported from @alchemy/aa-accounts.

ts
import { SessionKeySigner } from "@alchemy/aa-accounts";

const sessionKeySigner = new SessionKeySigner();

If you are using backend agent controlled session keys, then the agent should generate the private key and send only the address to the client. This protects the private key by not exposing it to the user.

Extend your client with Modular Account Decorators

The base SmartAccountClient and AlchemySmartAccountClient, only include base functionality for sending user operations. If you are using a ModularAccount, then you will want to extend your client with the various decorators exported by @alchemy/aa-accounts.

ts
import { smartAccountClient } from "./smartAccountClient";
import {
  accountLoupeActions,
  multiOwnerPluginActions,
  sessionKeyPluginActions,
  pluginManagerActions,
} from "@alchemy/aa-accounts";

const extendedClient = smartAccountClient
  .extend()
  // These are the base decorators for using Modular Accounts with your client
  .extend(pluginManagerActions)
  .extend(accountLoupeActions)
  // These two decorators give you additional utilities for interacting with the
  // MultiOwnerPlugin (default ownership plugin)
  // and SessionKeyPlugin
  .extend(multiOwnerPluginActions)
  .extend(sessionKeyPluginActions);
ts
import { createAlchemySmartAccountClient } from "@alchemy/aa-alchemy";
import { polygonMumbai } from "@alchemy/aa-core";

const chain = polygonMumbai;

export const smartAccountClient = createAlchemySmartAccountClient({
  apiKey: "demo",
  chain,
});

Check if the Session Key Plugin is installed

Before you can start using session keys, you need to check whether the user’s account has the session key plugin installed. You can perform this check using the account loupe decorator, which lets you inspect the state of installed plugins on a Modular Account.

ts
// 1. check if the plugin is installed
const extendedClient = await client
  .getInstalledPlugins({})
  // This checks using the default address for the chain, but you can always pass in your own plugin address here as an override
  .then((x) => x.includes(SessionKeyPlugin.meta.addresses[chain.id]));

Install the Session Key Plugin

If the Session Key Plugin is not yet installed, you need to install it before it can be used. To simplify the workflow, it is also possible to batch the plugin installation along with creating session keys and performing other actions, which combines all of these steps into one user operation.

ts
// 2. if the plugin is not installed, then install it and set up the session key
if (!isPluginInstalled) {
  // lets create an initial permission set for the session key giving it an eth spend limit
  // if we don't set anything here, then the key will have 0 permissions
  const initialPermissions =
    new SessionKeyPermissionsBuilder().setNativeTokenSpendLimit({
      spendLimit: 1000000n,
    });

  const { hash } = await extendedClient.installSessionKeyPlugin({
    // 1st arg is the initial set of session keys
    // 2nd arg is the tags for the session keys
    // 3rd arg is the initial set of permissions
    args: [
      [await sessionKeySigner.getAddress()],
      ["0x0"],
      [initialPermissions.encode()],
    ],
  });

  await extendedClient.waitForUserOperationTransaction({ hash });
}

Construct the initial set of permissions

Session keys are powerful because of permissions that limit what actions they can take. When you add a session key, you should also specify the initial permissions that apply over the key.

Default Values

Permissions start with the following default values:

PermissionDefault Value
Access control listType: allowlist
The list starts empty. When the allowlist is empty, all calls will be denied.
Time rangeUnlimited
Native token spend limit0
This means all calls spending the native token will be denied, unless the limit is updated or removed.
ERC-20 spend limitUnset. If you want to enabled an ERC-20 spend limit, add the ERC-20 token contract to the access control list and set the spending limit amount.
Gas spend limitsUnset. When defining the session key’s permissions, you should specify either a spending limit or a required paymaster.

Importance of gas limits

Gas spend limits are critically important to protecting the account. If you are using a session key, you should configure either a required paymaster rule or a gas spend limit. Failing to do so could allow a malicious session key to drain the account’s native token balance.

View the full set of supported permissions here
ts
import { encodeFunctionData, type Address, type Hex } from "viem";
import { SessionKeyPermissionsUpdatesAbi } from "./SessionKeyPermissionsUpdatesAbi.js";

export enum SessionKeyAccessListType {
  ALLOWLIST = 0,
  DENYLIST = 1,
  ALLOW_ALL_ACCESS = 2,
}

export type ContractAccessEntry = {
  // The contract address to add or remove.
  contractAddress: Address;
  // Whether the contract address should be on the list.
  isOnList: boolean;
  // Whether to check selectors for the contract address.
  checkSelectors: boolean;
};

export type ContractMethodEntry = {
  // The contract address to add or remove.
  contractAddress: Address;
  // The function selector to add or remove.
  methodSelector: Hex;
  // Whether the function selector should be on the list.
  isOnList: boolean;
};

export type TimeRange = {
  validFrom: number;
  validUntil: number;
};

export type NativeTokenLimit = {
  spendLimit: bigint;
  // The time interval over which the spend limit is enforced. If unset, there is no time
  /// interval by which the limit is refreshed.
  refreshInterval?: number;
};

export type Erc20TokenLimit = {
  tokenAddress: Address;
  spendLimit: bigint;
  // The time interval over which the spend limit is enforced. If unset, there is no time
  /// interval by which the limit is refreshed.
  refreshInterval?: number;
};

// uint256 spendLimit, uint48 refreshInterval
export type GasSpendLimit = {
  spendLimit: bigint;
  // The time interval over which the spend limit is enforced. If unset, there is no time
  /// interval by which the limit is refreshed.
  refreshInterval?: number;
};

export class SessionKeyPermissionsBuilder {
  private _contractAccessControlType: SessionKeyAccessListType =
    SessionKeyAccessListType.ALLOWLIST;
  private _contractAddressAccessEntrys: ContractAccessEntry[] = [];
  private _contractMethodAccessEntrys: ContractMethodEntry[] = [];
  private _timeRange?: TimeRange;
  private _nativeTokenSpendLimit?: NativeTokenLimit;
  private _erc20TokenSpendLimits: Erc20TokenLimit[] = [];
  private _gasSpendLimit?: GasSpendLimit;
  private _requiredPaymaster?: Address;

  public setContractAccessControlType(aclType: SessionKeyAccessListType) {
    this._contractAccessControlType = aclType;
    return this;
  }

  public addContractAddressAccessEntry(entry: ContractAccessEntry) {
    this._contractAddressAccessEntrys.push(entry);
    return this;
  }

  public addContractFunctionAccessEntry(entry: ContractMethodEntry) {
    this._contractMethodAccessEntrys.push(entry);
    return this;
  }

  public setTimeRange(timeRange: TimeRange) {
    this._timeRange = timeRange;
    return this;
  }

  public setNativeTokenSpendLimit(limit: NativeTokenLimit) {
    this._nativeTokenSpendLimit = limit;
    return this;
  }

  public addErc20TokenSpendLimit(limit: Erc20TokenLimit) {
    this._erc20TokenSpendLimits.push(limit);
    return this;
  }

  public setGasSpendLimit(limit: GasSpendLimit) {
    this._gasSpendLimit = limit;
    return this;
  }

  public setRequiredPaymaster(paymaster: Address) {
    this._requiredPaymaster = paymaster;
    return this;
  }

  public encode(): Hex[] {
    return [
      encodeFunctionData({
        abi: SessionKeyPermissionsUpdatesAbi,
        functionName: "setAccessListType",
        args: [this._contractAccessControlType],
      }),
      ...this._contractAddressAccessEntrys.map((entry) =>
        encodeFunctionData({
          abi: SessionKeyPermissionsUpdatesAbi,
          functionName: "updateAccessListAddressEntry",
          args: [entry.contractAddress, entry.isOnList, entry.checkSelectors],
        })
      ),
      ...this._contractMethodAccessEntrys.map((entry) =>
        encodeFunctionData({
          abi: SessionKeyPermissionsUpdatesAbi,
          functionName: "updateAccessListFunctionEntry",
          args: [entry.contractAddress, entry.methodSelector, entry.isOnList],
        })
      ),
      this.encodeIfDefined(
        (timeRange) =>
          encodeFunctionData({
            abi: SessionKeyPermissionsUpdatesAbi,
            functionName: "updateTimeRange",
            args: [timeRange.validFrom, timeRange.validUntil],
          }),
        this._timeRange
      ),
      this.encodeIfDefined(
        (nativeSpendLimit) =>
          encodeFunctionData({
            abi: SessionKeyPermissionsUpdatesAbi,
            functionName: "setNativeTokenSpendLimit",
            args: [
              nativeSpendLimit.spendLimit,
              nativeSpendLimit.refreshInterval ?? 0,
            ],
          }),
        this._nativeTokenSpendLimit
      ),
      ...this._erc20TokenSpendLimits.map((erc20SpendLimit) =>
        encodeFunctionData({
          abi: SessionKeyPermissionsUpdatesAbi,
          functionName: "setERC20SpendLimit",
          args: [
            erc20SpendLimit.tokenAddress,
            erc20SpendLimit.spendLimit,
            erc20SpendLimit.refreshInterval ?? 0,
          ],
        })
      ),
      this.encodeIfDefined(
        (spendLimit) =>
          encodeFunctionData({
            abi: SessionKeyPermissionsUpdatesAbi,
            functionName: "setGasSpendLimit",
            args: [spendLimit.spendLimit, spendLimit.refreshInterval ?? 0],
          }),
        this._gasSpendLimit
      ),
      this.encodeIfDefined(
        (paymaster) =>
          encodeFunctionData({
            abi: SessionKeyPermissionsUpdatesAbi,
            functionName: "setRequiredPaymaster",
            args: [paymaster],
          }),
        this._requiredPaymaster
      ),
    ].filter((x) => x !== "0x");
  }

  private encodeIfDefined<T>(encode: (param: T) => Hex, param?: T): Hex {
    if (!param) return "0x";

    return encode(param);
  }
}

Let's use the permission builder to build a set of permissions that sets a spend limit:

ts
const initialPermissions =
  new SessionKeyPermissionsBuilder().setNativeTokenSpendLimit({
    spendLimit: 1000000n,
  });

const result = await extendedClient.updateKeyPermissions({
  args: [sessionKeyAddress, initialPermissions.encode()],
});

Creating, deleting, and editing session keys

Add a Session Key

Session keys can be added either during installation, or using the addSessionKey function.

ts
import { SessionKeyPermissionsBuilder } from "@alchemy/aa-accounts";
import { keccak256 } from "viem";
import { client } from "./base-client.js";

const result = await client.addSessionKey({
  key: "0xSessionKeyAddress",
  // tag is an identifier for the emitted SessionKeyAdded event
  tag: keccak256(new TextEncoder().encode("session-key-tag")),
  permissions: new SessionKeyPermissionsBuilder().encode(),
});

Remove a Session Key

Session keys can be removed using the removeSessionKey function.

ts
import { client } from "./base-client.js";

const result = await client.removeSessionKey({
  key: "0xSessionKeyAddress",
});

Update a Key's permissions

Session key permissions can be edited after creation using the updateKeyPermissions function. Note that you should configure initial permissions when the key is added, and not rely on a second user operation to set the permissions.

ts
import { SessionKeyPermissionsBuilder } from "@alchemy/aa-accounts";
import { client } from "./base-client.js";

const result = await client.updateSessionKeyPermissions({
  key: "0xSessionKeyAddress",
  // add other permissions to the builder
  permissions: new SessionKeyPermissionsBuilder()
    .setTimeRange({
      validFrom: Math.round(Date.now() / 1000),
      // valid for 1 hour
      validUntil: Math.round(Date.now() / 1000 + 60 * 60),
    })
    .encode(),
});

Rotate a Session Key

If the key is no longer available, but there exists a tag identifying a previous session key configured for your application, you may instead choose to rotate the previous key’s permissions. This can be performed using rotateKey .

ts
import { client } from "./base-client.js";

const result = await client.rotateSessionKey({
  oldKey: "0xOldKey",
  newKey: "0xNewKey",
});