Introduction
Account Kit enables smart account creation and management for every user in your application. Using Account Kit with Capsule, you
can:
- Create and manage smart accounts directly in your app
- Submit gasless transactions using Alchemy’s Gas Manager
- Execute batch transactions via UserOperations
- Handle transactions without requiring users to leave your application
Prerequisites
Before you begin, ensure you have:
For gasless transactions, you’ll need to set up a Gas Manager Policy in your Alchemy Dashboard. This allows you to
sponsor gas fees for your users’ transactions.
Installation
Setup
1. Create Capsule Viem Client
First, create a Viem client configured with your Capsule account:
import { LocalAccount, WalletClient } from "viem";
import { sepolia } from "viem/chains";
import { http } from "viem";
const viemCapsuleAccount: LocalAccount = createCapsuleAccount(capsuleClient);
const viemClient: WalletClient = createCapsuleViemClient(capsuleClient, {
account: viemCapsuleAccount,
chain: sepolia,
transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
});
2. Implement Custom Sign Message
Due to MPC requirements, implement a custom sign message function to handle the signature’s v value:
async function customSignMessage(capsule: CapsuleServer, message: SignableMessage): Promise<Hash> {
const hashedMessage = hashMessage(message);
const res = await capsule.signMessage(Object.values(capsule.wallets!)[0]!.id, hexStringToBase64(hashedMessage));
let signature = (res as SuccessfulSignatureRes).signature;
const lastByte = parseInt(signature.slice(-2), 16);
if (lastByte < 27) {
const adjustedV = (lastByte + 27).toString(16).padStart(2, "0");
signature = signature.slice(0, -2) + adjustedV;
}
return `0x${signature}`;
}
Override the default signMessage method:
viemClient.signMessage = async ({ message }: { message: SignableMessage }): Promise<Hash> => {
return customSignMessage(capsuleClient, message);
};
4. Initialize Alchemy Client
Create the Alchemy client with your configuration:
import { createModularAccountAlchemyClient } from "@alchemy/aa-alchemy";
import { WalletClientSigner } from "@alchemy/aa-core";
const walletClientSigner = new WalletClientSigner(viemClient, "capsule");
const alchemyClient = await createModularAccountAlchemyClient({
apiKey: ALCHEMY_API_KEY,
chain: arbitrumSepolia,
signer: walletClientSigner,
gasManagerConfig: {
policyId: ALCHEMY_GAS_POLICY_ID,
},
});
Usage
Creating UserOperations
UserOperations represent transaction intentions that will be executed by your smart account. Here’s an example of
creating and executing a batch of UserOperations:
import { encodeFunctionData } from "viem";
import { BatchUserOperationCallData, SendUserOperationResult } from "@alchemy/aa-core";
const exampleAbi = [
{
inputs: [{ internalType: "uint256", name: "_x", type: "uint256" }],
name: "changeX",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];
const demoUserOperations: BatchUserOperationCallData = [1, 2, 3, 4, 5].map((x) => ({
target: EXAMPLE_CONTRACT_ADDRESS,
data: encodeFunctionData({
abi: exampleAbi,
functionName: "changeX",
args: [x],
}),
}));
const userOperationResult: SendUserOperationResult = await alchemyClient.sendUserOperation({
uo: demoUserOperations,
});
const txHash = await alchemyClient.waitForUserOperationTransaction({
hash: userOperationResult.hash,
});
UserOperations are bundled together and executed in a single transaction, making them more gas-efficient than
executing multiple separate transactions.
Handling Gas Management
When using Alchemy’s Gas Manager, transactions can be executed without requiring users to have ETH in their account:
const alchemyClientWithGas = await createModularAccountAlchemyClient({
apiKey: ALCHEMY_API_KEY,
chain: arbitrumSepolia,
signer: walletClientSigner,
gasManagerConfig: {
policyId: ALCHEMY_GAS_POLICY_ID,
entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
},
});
const gaslessOperation = await alchemyClientWithGas.sendUserOperation({
uo: demoUserOperations,
});
Error Handling
When working with UserOperations, handle potential errors appropriately:
try {
const result = await alchemyClient.sendUserOperation({
uo: demoUserOperations,
});
const txHash = await alchemyClient.waitForUserOperationTransaction({
hash: result.hash,
timeout: 60000,
});
} catch (error) {
if (error.code === "USER_OPERATION_REVERTED") {
console.error("UserOperation reverted:", error.message);
} else if (error.code === "TIMEOUT") {
console.error("Operation timed out:", error.message);
} else {
console.error("Unexpected error:", error);
}
}
If you need access to Capsule or help getting set up, please refer to our quick start guide: