Adding session keys
Adding a session key is simple with Account Kit! To add a session key, you need to 1) decide what permissions you want to grant the new key and 2) call the installValidation
method on the account. This method will send a single user operation that will add the session key with scoped permission to your smart contract account on-chain. You can then use that session key to sign transactions for your account within the defined permissions!
Adding scoped permissions to keys will happen via permission modules that you can pass as configuration parameters on the installValidation
method. Permissions can be combined to fit your use case (e.g. you can limit a session key to only be able to spend 10 USDC within the next 24 hours on behalf of your account).
Adding a global session key (i.e. additional owner)
This example shows:
- Adding a global session key to your Modular Account V2. This essentially gives the session key full control of your account. Functionally, this is how you can add another owner on your smart account.
- Adding a session key that can only call 'execute' on the account. Functionally, this allows the session key to have full control of the account other than changing the underlying account implementation.
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import {
installValidationActions,
getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
semiModularAccountBytecodeAbi,
} from "@account-kit/smart-contracts/experimental";
import { LocalAccountSigner } from "@aa-sdk/core";
import { sepolia, alchemy } from "@account-kit/infra";
import { generatePrivateKey } from "viem/accounts";
import { toFunctionSelector, getAbiItem } from "viem";
import { type SmartAccountSigner } from "@aa-sdk/core";
const client = (
await createModularAccountV2Client({
chain: sepolia,
transport: alchemy({ apiKey: "your-api-key" }),
signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
})
).extend(installValidationActions);
let sessionKeyEntityId = 1;
const ecdsaValidationModuleAddress =
getDefaultSingleSignerValidationModuleAddress(client.chain);
const sessionKeySigner: SmartAccountSigner =
LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
// 1. Adding a session key with full permissions
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [],
});
// 2. Adding a session key that can only call `execute` or `executeBatch` on the account
sessionKeyEntityId = 2;
const executeSelector = toFunctionSelector(
getAbiItem({
abi: semiModularAccountBytecodeAbi,
name: "execute",
})
);
const executeBatchSelector = toFunctionSelector(
getAbiItem({
abi: semiModularAccountBytecodeAbi,
name: "executeBatch",
})
);
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: false,
isSignatureValidation: false,
isUserOpValidation: true,
},
selectors: [executeSelector, executeBatchSelector],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [],
});
Adding a session key with permissions
Time range
Configuring a session key with a time range allows you to limit how long the session key is valid for (e.g. only allow this key to sign on my account for the next day). The Time Range Module is used to enforce time-based validation for User Operations (UOs) in the system. This example will show you how to add a session key that starts in a day and expires in two days.
Additional Notes
- the interval is inclusive i.e.
[beginningOfInterval, endOfInterval]
- the values
beginningOfInterval
andendOfInterval
are unix timestamps with a maximum size of uint32 - the timestamp specifying the end of the interval must be strictly greater than the beginning of the interval
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import {
HookType,
installValidationActions,
getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
getDefaultTimeRangeModuleAddress,
TimeRangeModule,
} from "@account-kit/smart-contracts/experimental";
import { LocalAccountSigner } from "@aa-sdk/core";
import { sepolia, alchemy } from "@account-kit/infra";
import { generatePrivateKey } from "viem/accounts";
import { type SmartAccountSigner } from "@aa-sdk/core";
const client = (
await createModularAccountV2Client({
chain: sepolia,
transport: alchemy({ apiKey: "your-api-key" }),
signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
})
).extend(installValidationActions);
let sessionKeyEntityId = 1;
const ecdsaValidationModuleAddress =
getDefaultSingleSignerValidationModuleAddress(client.chain);
const sessionKeySigner: SmartAccountSigner =
LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
const validAfter = 0; // valid once added
const validUntil = validAfter + 2 * 86400; // validity ends 2 days from now
// Adding a session key that starts in a day and expires in two days
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [
{
hookConfig: {
address: getDefaultTimeRangeModuleAddress(client.chain),
entityId: hookEntityId,
hookType: HookType.VALIDATION, // fixed value
hasPreHooks: true, // fixed value
hasPostHooks: false, // fixed value
},
initData: TimeRangeModule.encodeOnInstallData({
entityId: hookEntityId,
validAfter,
validUntil,
}),
},
],
});
Paymaster guard
Purpose
This module provides the ability to limit a session key to only be able to use a specific single paymaster.
Additional Notes
- you MUST specify a paymaster when using this module
- if the paymaster that is registered with this module decides to no longer sponsor your user operations, the entity associated with this hook would no longer be able to send user operations.
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import {
HookType,
installValidationActions,
getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
getDefaultPaymasterGuardModuleAddress,
PaymasterGuardModule,
} from "@account-kit/smart-contracts/experimental";
import { LocalAccountSigner } from "@aa-sdk/core";
import { sepolia, alchemy } from "@account-kit/infra";
import { generatePrivateKey } from "viem/accounts";
import { type SmartAccountSigner } from "@aa-sdk/core";
const client = (
await createModularAccountV2Client({
chain: sepolia,
transport: alchemy({ apiKey: "your-api-key" }),
signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
})
).extend(installValidationActions);
let sessionKeyEntityId = 1;
const ecdsaValidationModuleAddress =
getDefaultSingleSignerValidationModuleAddress(client.chain);
const sessionKeySigner: SmartAccountSigner =
LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
const paymasterAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
// Adding a session key that can only use the above paymaster for user operations
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [
{
hookConfig: {
address: getDefaultPaymasterGuardModuleAddress(client.chain),
entityId: hookEntityId,
hookType: HookType.VALIDATION, // fixed value
hasPreHooks: true, // fixed value
hasPostHooks: false, // fixed value
},
initData: PaymasterGuardModule.encodeOnInstallData({
entityId: hookEntityId,
paymaster: paymasterAddress,
}),
},
],
});
Native token and/or gas limit
This module provides native token spending limits for modular accounts. Below we will show an example of adding a session key that has a 1 eth native token spend limit. Functionally, this module is enabled by:
- Tracking and limiting total native token spending across transactions
- Monitoring both direct transfers and gas costs from UserOperations
- Supporting special paymaster configurations for complex gas payment scenarios
Token Limit Features
- Tracks native token spending across:
- Direct transfers via
execute
- Batch transfers via
executeBatch
- Contract creation via
performCreate
- UserOperation gas costs (when applicable)
- Direct transfers via
- Supports special paymaster configurations for:
- Standard paymasters (gas costs don't count against limit)
- Special paymasters (gas costs do count against limit)
- Maintains separate limits per entity ID
Gas Cost Tracking
For UserOperations, the module tracks:
- Pre-verification gas
- Verification gas
- Call gas
- Paymaster verification gas (for special paymasters)
- Paymaster post-op gas (for special paymasters)
Additional Notes
- The module must be installed with both validation and execution hooks, the validation hook track gas, whereas the execution hook tracks value
- The module maintains a global singleton state for all accounts
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import {
HookType,
installValidationActions,
getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
getDefaultNativeTokenLimitModuleAddress,
NativeTokenLimitModule,
} from "@account-kit/smart-contracts/experimental";
import { LocalAccountSigner } from "@aa-sdk/core";
import { sepolia, alchemy } from "@account-kit/infra";
import { generatePrivateKey } from "viem/accounts";
import { parseEther } from "viem";
import { type SmartAccountSigner } from "@aa-sdk/core";
const client = (
await createModularAccountV2Client({
chain: sepolia,
transport: alchemy({ apiKey: "your-api-key" }),
signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
})
).extend(installValidationActions);
let sessionKeyEntityId = 1;
const ecdsaValidationModuleAddress =
getDefaultSingleSignerValidationModuleAddress(client.chain);
const sessionKeySigner: SmartAccountSigner =
LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
// Adding a session key that has a 1 eth native token spend limit
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [
{
hookConfig: {
address: getDefaultNativeTokenLimitModuleAddress(client.chain),
entityId: hookEntityId,
hookType: HookType.VALIDATION, // fixed value
hasPreHooks: true, // fixed value
hasPostHooks: false, // fixed value
},
initData: NativeTokenLimitModule.encodeOnInstallData({
entityId: hookEntityId,
spendLimit: parseEther("1"),
}),
},
{
hookConfig: {
address: getDefaultNativeTokenLimitModuleAddress(client.chain),
entityId: hookEntityId,
hookType: HookType.EXECUTION, // fixed value
hasPreHooks: true, // fixed value
hasPostHooks: false, // fixed value
},
initData: "0x", // no initdata required as the limit was set up in the above installation call
},
],
});
Allowlist or an ERC20 token limit
This module provides two key security features for modular accounts:
- Allowlisting - Controls which addresses and functions can be called
- ERC-20 Spend Limits - Manages spending limits for ERC-20 tokens
Allowlist Features
- Can specify permissions for:
- Specific addresses + specific functions
- Specific addresses + all functions (wildcard)
- All addresses + specific functions (wildcard)
- Only applies to execute and executeBatch functions
- Permission checks follow this order:
- If wildcard address → Allow
- If wildcard function → Allow
- If specific address + specific function match → Allow
- Otherwise → Revert
ERC-20 Spend Limit Features
- Only allows transfer and approve functions for tracked tokens
- Works with standard execution functions:
- execute
- executeWithRuntimeValidation
- executeUserOp
- executeBatch
Additional Notes
- Module must be installed/uninstalled on an entity ID basis
- Uninstalling for one entity ID doesn't affect other entities
- Settings are stored in a global singleton contract
- All permissions and limits can be updated dynamically
- The module is intentionally restrictive about which ERC-20 functions are allowed to prevent edge cases (e.g., DAI's non-standard functions)
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import {
HookType,
installValidationActions,
getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
getDefaultAllowlistModuleAddress,
AllowlistModule,
} from "@account-kit/smart-contracts/experimental";
import { LocalAccountSigner } from "@aa-sdk/core";
import { sepolia, alchemy } from "@account-kit/infra";
import { generatePrivateKey } from "viem/accounts";
import { parseEther } from "viem";
import { type SmartAccountSigner } from "@aa-sdk/core";
const client = (
await createModularAccountV2Client({
chain: sepolia,
transport: alchemy({ apiKey: "your-api-key" }),
signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
})
).extend(installValidationActions);
let sessionKeyEntityId = 1;
const ecdsaValidationModuleAddress =
getDefaultSingleSignerValidationModuleAddress(client.chain);
const sessionKeySigner: SmartAccountSigner =
LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
const allowlistInstallData = AllowlistModule.encodeOnInstallData({
entityId: hookEntityId,
inputs: [
{
target: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
hasSelectorAllowlist: false, // whether to limit the callable functions on call targets
hasERC20SpendLimit: false, // If "target" is an ERC20 token with a spend limit
erc20SpendLimit: parseEther("100"), // The spend limit to set, if relevant
selectors: [], // The function selectors to allow, if relevant
},
],
});
// Adding a session key that has a 100 ERC token spend limit
await client.installValidation({
validationConfig: {
moduleAddress: ecdsaValidationModuleAddress,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
signer: await sessionKeySigner.getAddress(), // Address of the session key
}),
hooks: [
{
hookConfig: {
address: getDefaultAllowlistModuleAddress(client.chain),
entityId: hookEntityId,
hookType: HookType.VALIDATION, // fixed value
hasPreHooks: true, // fixed value
hasPostHooks: false, // fixed value
},
initData: allowlistInstallData,
},
],
});
Install validation method
This method is used to add session keys to your account, with the following configurable parameters.
validationConfig
: The validation configuration for the session key, containing the following fields:
validationModule
: This is the address of the validation module to use for this key. SingleSignerValidationModule provides ECDSA validation and WebauthnModule provides WebAuthn validation. If you wish to use a custom validation module such as a multisig validation, this would be specified here.entityId
: This is a uint32 identifier for validation chosen by the developer. The only rule here is that you cannot pick an entityId that already is used on the account. Since the owner's entityId is 0, you can start from 1.isGlobal
: This is a boolean that specifies if the validation can be used to call any function on the account. If this is set to false, the validation can only be used to call functions that are specified in theselectors
array. It's recommended to leave this asfalse
and use the selector array instead, as a key with global permissions has the authority to upgrade the account to any other implementation, which can change the ownership of the account in the same transaction.isSignatureValidation
: This is a boolean that specifies if the validation can be used for ERC-1271 signature validation, which can be used for signing permit2 token permits. It's recommended to leave this asfalse
for security reasons.isUserOpValidation
: This is a boolean that specifies if the key can perform user operations on behalf of the account. For most use cases, this should be set totrue
.
selectors
: This is an array of function selectors that the key can call. If isGlobal
is set to true
, the limits in this array will not be applied. If isGlobal
is set to false
, the key can only call functions that are specified in this array. It's recommended to only have ModularAccount.execute.selector
and ModularAccount.executeBatch.selector
in this array.
installData
: This is the installation data that is passed to the validation module on installation. Each module has their own encoding for this data, so you would need to use the helper functions provided by that module.
hooks
: This is an array of hooks to be installed on the key. Each element in the array contains a hookConfig object as well as initData to pass to the hook module.
hookConfig
: This is the hook configuration to be applied to the session key.
address
: This is the address of the hook module to be installed on the session key.entityId
: This is a hook module entity id that is different from the validation entity id. The decoupling enables multiple hooks provided by the same module to be applied on the same key. The only restriction here is that the hook module entity id should not be an entity id that's currently in use for the account.hookType
: This specifies which phase should the hook be applied on, either HookType.VALIDATION to be a validation hook, or HookType.EXECUTION to be a pre and/or post-execution hook.hasPreHooks
: This specifies if the hook is supposed to run before validation or execution.hasPostHooks
: This specifies if the hook is supposed to run after validation or execution.