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;

  // Fix the v value of the 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}`;
}

3. Configure Viem Client with Custom Sign Message

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, // Optional: for gasless transactions
  },
});

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";

// Example contract ABI
const exampleAbi = [
  {
    inputs: [{ internalType: "uint256", name: "_x", type: "uint256" }],
    name: "changeX",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
];

// Create batch operations
const demoUserOperations: BatchUserOperationCallData = [1, 2, 3, 4, 5].map((x) => ({
  target: EXAMPLE_CONTRACT_ADDRESS,
  data: encodeFunctionData({
    abi: exampleAbi,
    functionName: "changeX",
    args: [x],
  }),
}));

// Execute the operations
const userOperationResult: SendUserOperationResult = await alchemyClient.sendUserOperation({
  uo: demoUserOperations,
});

// Optional: Wait for the operation to be included in a block
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:

// Configuration with gas management
const alchemyClientWithGas = await createModularAccountAlchemyClient({
  apiKey: ALCHEMY_API_KEY,
  chain: arbitrumSepolia,
  signer: walletClientSigner,
  gasManagerConfig: {
    policyId: ALCHEMY_GAS_POLICY_ID,
    entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", // Optional: custom entryPoint
  },
});

// Transactions will now be gasless for users
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,
  });

  // Wait for transaction confirmation
  const txHash = await alchemyClient.waitForUserOperationTransaction({
    hash: result.hash,
    timeout: 60000, // Optional: timeout in milliseconds
  });
} 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: