Most projects should use @alchemy/wallet-apis
@alchemy/wallet-apis supports session keys through wallet_createSession and the grantPermissions SDK flow. This page covers the lower-level path: uninstalling Modular Account V2 session-key validations directly with @alchemy/smart-accounts and viem's bundler client. Use it when you need lower-level control for custom onchain validation and permission wiring.
To remove a session key, extend the bundler client with installValidationActions and call encodeUninstallValidation. It returns calldata; send it as a user operation from the account.
import { createBundlerClient } from "viem/account-abstraction";
import { createClient } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { alchemyTransport } from "@alchemy/common";
import {
toModularAccountV2,
installValidationActions,
SingleSignerValidationModule,
DefaultModuleAddress,
} from "@alchemy/smart-accounts";
import { estimateFeesPerGas } from "@alchemy/aa-infra";
const transport = alchemyTransport({ apiKey: "your-api-key" });
const rpcClient = createClient({ chain: sepolia, transport });
const account = await toModularAccountV2({
client: rpcClient,
owner: privateKeyToAccount(generatePrivateKey()),
});
const bundlerClient = createBundlerClient({
account,
client: rpcClient,
chain: sepolia,
transport,
userOperation: { estimateFeesPerGas },
}).extend(installValidationActions);
const sessionKeyEntityId = 1;
const callData = await bundlerClient.encodeUninstallValidation({
moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
uninstallData: SingleSignerValidationModule.encodeOnUninstallData({
entityId: sessionKeyEntityId,
}),
hookUninstallDatas: [],
});
await bundlerClient.sendUserOperation({ callData });If the validation has hooks installed, you have to provide uninstall data for each one. Every module exports an encodeOnUninstallData helper.
import { createBundlerClient } from "viem/account-abstraction";
import { createClient } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { alchemyTransport } from "@alchemy/common";
import {
toModularAccountV2,
installValidationActions,
SingleSignerValidationModule,
AllowlistModule,
DefaultModuleAddress,
} from "@alchemy/smart-accounts";
import { estimateFeesPerGas } from "@alchemy/aa-infra";
const transport = alchemyTransport({ apiKey: "your-api-key" });
const rpcClient = createClient({ chain: sepolia, transport });
const account = await toModularAccountV2({
client: rpcClient,
owner: privateKeyToAccount(generatePrivateKey()),
});
const bundlerClient = createBundlerClient({
account,
client: rpcClient,
chain: sepolia,
transport,
userOperation: { estimateFeesPerGas },
}).extend(installValidationActions);
const sessionKeyEntityId = 1;
const hookEntityId = 1;
const target = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
// Removing a session key with an allowlist hook
const callData = await bundlerClient.encodeUninstallValidation({
moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
uninstallData: SingleSignerValidationModule.encodeOnUninstallData({
entityId: sessionKeyEntityId,
}),
hookUninstallDatas: [
AllowlistModule.encodeOnUninstallData({
entityId: hookEntityId,
inputs: [
{
target,
hasSelectorAllowlist: false,
hasERC20SpendLimit: false,
erc20SpendLimit: 0n,
selectors: [],
},
],
}),
],
});
await bundlerClient.sendUserOperation({ callData });