diff --git a/content/tutorials/getting-started/alchemy-quickstart-guide.mdx b/content/tutorials/getting-started/alchemy-quickstart-guide.mdx
index 057a344bc..d04e5966e 100644
--- a/content/tutorials/getting-started/alchemy-quickstart-guide.mdx
+++ b/content/tutorials/getting-started/alchemy-quickstart-guide.mdx
@@ -109,35 +109,32 @@ Ship smart wallets in minutes with the Account Kit SDK:
Ship for web and mobile with React, React Native, or other JS frameworks.
-Check out the [Smart Wallets quickstart](/docs/wallets/react/quickstart) to get started.
+Check out the [Smart Wallets quickstart](/docs/wallets/quickstart) to get started.
### Send a gas-free smart wallet transaction
```ts
-// yarn add @account-kit/infra @account-kit/smart-contracts
-import {
- alchemy,
- createAlchemySmartAccountClient,
- sepolia,
-} from "@account-kit/infra";
-import { createLightAccount } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { generatePrivateKey } from "viem/accounts";
-
-const alchemyTransport = alchemy({
- apiKey: "YOUR_API_KEY", // from Alchemy dashboard
+// yarn add @alchemy/wallet-apis viem
+import { createSmartWalletClient, alchemyWalletTransport } from "@alchemy/wallet-apis";
+import { arbitrumSepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { zeroAddress } from "viem";
+
+export const client = createSmartWalletClient({
+ transport: alchemyWalletTransport({
+ apiKey: "YOUR_API_KEY", // from Alchemy dashboard
+ }),
+ chain: arbitrumSepolia,
+ signer: privateKeyToAccount(generatePrivateKey()),
+ paymaster: {
+ policyId: "YOUR_POLICY_ID", // create a gas policy in the dashboard
+ },
});
-export const client = createAlchemySmartAccountClient({
- transport: alchemyTransport,
- policyId: "YOUR_POLICY_ID", // create a gas policy in the dashboard
- chain: sepolia,
- account: await createLightAccount({
- chain: sepolia,
- transport: alchemyTransport,
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
- }),
+const { id } = await client.sendCalls({
+ calls: [{ to: zeroAddress, value: BigInt(0) }],
});
+const status = await client.waitForCallsStatus({ id });
```
***
diff --git a/content/wallets/pages/authentication/login-methods/bring-your-own-auth.mdx b/content/wallets/pages/authentication/login-methods/bring-your-own-auth.mdx
index a7e5cb04b..4c310da6b 100644
--- a/content/wallets/pages/authentication/login-methods/bring-your-own-auth.mdx
+++ b/content/wallets/pages/authentication/login-methods/bring-your-own-auth.mdx
@@ -4,6 +4,8 @@ description: Integrate your existing authentication system with Wallet APIs usin
slug: wallets/authentication/login-methods/bring-your-own-auth
---
+
+
Integrate your existing authentication provider and add smart wallet functionality to your app without changing your users' login experience. Wallet APIs supports JWT-based and OIDC-compliant authentication providers.
## Prerequisites
diff --git a/content/wallets/pages/authentication/login-methods/email-magic-link.mdx b/content/wallets/pages/authentication/login-methods/email-magic-link.mdx
index 9fd4b26aa..5319caddc 100644
--- a/content/wallets/pages/authentication/login-methods/email-magic-link.mdx
+++ b/content/wallets/pages/authentication/login-methods/email-magic-link.mdx
@@ -4,6 +4,8 @@ description: How to implement Email Magic Link authentication across different f
slug: wallets/authentication/login-methods/email-magic-link
---
+
+
Email magic link authentication is a two-step process:
1. The user enters their email address and requests a magic link
diff --git a/content/wallets/pages/authentication/login-methods/email-otp.mdx b/content/wallets/pages/authentication/login-methods/email-otp.mdx
index 97d625243..c58077ae1 100644
--- a/content/wallets/pages/authentication/login-methods/email-otp.mdx
+++ b/content/wallets/pages/authentication/login-methods/email-otp.mdx
@@ -4,6 +4,8 @@ description: How to implement Email OTP authentication across different framewor
slug: wallets/authentication/login-methods/email-otp
---
+
+
Email OTP (One-Time Password) authentication is a two-step process:
1. The user enters their email address and requests a verification code
diff --git a/content/wallets/pages/authentication/login-methods/passkey-signup.mdx b/content/wallets/pages/authentication/login-methods/passkey-signup.mdx
index 757446897..ae735e701 100644
--- a/content/wallets/pages/authentication/login-methods/passkey-signup.mdx
+++ b/content/wallets/pages/authentication/login-methods/passkey-signup.mdx
@@ -4,6 +4,8 @@ description: How to implement Passkey Signup authentication across different fra
slug: wallets/authentication/login-methods/passkey-signup
---
+
+
Passkeys provide a secure, passwordless authentication method that can be used to create wallets for your users without going through email verification flows. You can implement passkey signup with or without an associated email address.
diff --git a/content/wallets/pages/authentication/login-methods/sms-login.mdx b/content/wallets/pages/authentication/login-methods/sms-login.mdx
index 0d7001284..5a8b29ee4 100644
--- a/content/wallets/pages/authentication/login-methods/sms-login.mdx
+++ b/content/wallets/pages/authentication/login-methods/sms-login.mdx
@@ -4,6 +4,8 @@ description: How to authenticate users with phone number and SMS OTP code
slug: wallets/authentication/login-methods/sms-login
---
+
+
SMS auth is currently in closed alpha. If you'd like early access please reach
out to [wallets@alchemy.com](mailto:wallets@alchemy.com).
diff --git a/content/wallets/pages/authentication/login-methods/social-login.mdx b/content/wallets/pages/authentication/login-methods/social-login.mdx
index 9d93aaa5b..0227b99df 100644
--- a/content/wallets/pages/authentication/login-methods/social-login.mdx
+++ b/content/wallets/pages/authentication/login-methods/social-login.mdx
@@ -4,6 +4,8 @@ description: How to implement Social Login authentication across different frame
slug: wallets/authentication/login-methods/social-login
---
+
+
Social login authentication allows users to authenticate using OAuth providers like Google, Facebook, or custom providers through Auth0. There are two authentication flows available:
1. **Redirect flow**: Redirects the user to the provider's login page and back to your application
diff --git a/content/wallets/pages/bundler-api/bundler-sponsored-operations.mdx b/content/wallets/pages/bundler-api/bundler-sponsored-operations.mdx
index 785b69386..7e12953d7 100644
--- a/content/wallets/pages/bundler-api/bundler-sponsored-operations.mdx
+++ b/content/wallets/pages/bundler-api/bundler-sponsored-operations.mdx
@@ -67,77 +67,68 @@ For more details, see the [Compute Unit Costs documentation](/docs/reference/com
To enable bundler sponsorship, you need to:
1. **Create a Bundler Sponsored Operations policy** in your dashboard with the required Max Spend Per UO configuration
-2. **Include the policy ID** in your requests via the `x-alchemy-policy-id` header
-3. **Set gas fees to zero** in your user operation overrides
+2. **Include the policy ID** in your requests via the `x-alchemy-policy-id` header on the bundler transport
+3. **Zero out `maxFeePerGas`, `maxPriorityFeePerGas`, and `preVerificationGas`** on the user operation
### Example implementation
-Here's a complete example using Wallet APIs to send a sponsored user operation:
+Below is the low-level (viem) pattern. The policy ID rides on the bundler transport's `fetchOptions.headers`; the bundler fills in the actual gas values server-side when it sees the zeroed fields.
-```typescript
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { alchemy, worldChain } from "@account-kit/infra";
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+```ts
+import { createClient, type Hex } from "viem";
+import { createBundlerClient } from "viem/account-abstraction";
+import { privateKeyToAccount } from "viem/accounts";
+import { worldchain } from "viem/chains";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-const RPC_URL = process.env.RPC_URL!;
+const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!;
const POLICY_ID = process.env.POLICY_ID!;
-const PRIVATE_KEY = process.env.PRIVATE_KEY!;
-
-(async () => {
- try {
- const chain = worldChain;
-
- // Configure transport with bundler sponsorship header
- const transport = alchemy({
- rpcUrl: RPC_URL,
- fetchOptions: {
- headers: {
- "x-alchemy-policy-id": POLICY_ID
- }
- }
- });
-
- const signer = LocalAccountSigner.privateKeyToAccountSigner(
- PRIVATE_KEY as `0x${string}`
- );
-
- const client = await createModularAccountV2Client({
- chain,
- transport,
- signer,
- });
-
- // Send user operation with bundler sponsorship
- const uo = await client.sendUserOperation({
- overrides: {
- maxFeePerGas: "0x0",
- maxPriorityFeePerGas: "0x0",
- },
- uo: {
- target: "0x0000000000000000000000000000000000000000",
- data: "0x",
- },
- });
-
- const txHash = await client.waitForUserOperationTransaction({
- hash: uo.hash,
- });
-
- console.log("Tx Hash: ", txHash);
-
- } catch (err) {
- console.error("Error:", err);
- }
-})();
+const PRIVATE_KEY = process.env.PRIVATE_KEY! as Hex;
+
+// The policy ID rides on the transport's fetch options.
+const transport = alchemyTransport({
+ apiKey: ALCHEMY_API_KEY,
+ fetchOptions: {
+ headers: { "x-alchemy-policy-id": POLICY_ID },
+ },
+});
+
+const rpcClient = createClient({ chain: worldchain, transport });
+
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(PRIVATE_KEY),
+});
+
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: worldchain,
+ transport,
+ userOperation: { estimateFeesPerGas },
+});
+
+// Zero ALL three gas fields — the bundler fills them in.
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x000000000000000000000000000000000000dEaD", data: "0x" }],
+ maxFeePerGas: 0n,
+ maxPriorityFeePerGas: 0n,
+ preVerificationGas: 0n,
+});
+
+const receipt = await bundlerClient.waitForUserOperationReceipt({ hash });
+console.log("Tx:", receipt.receipt.transactionHash);
```
### Key configuration points
1. **Policy Type**: Ensure you've created a policy of type "Bundler Sponsored Operations" with Max Spend Per UO configured.
-2. **Transport Configuration**: The `x-alchemy-policy-id` header must be included in the transport configuration's `fetchOptions.headers`.
+2. **Transport Configuration**: The `x-alchemy-policy-id` header must be included on the **bundler** transport via `fetchOptions.headers`. No `createPaymasterClient` needed — BSO does not use a paymaster contract.
-3. **Gas Fee Overrides**: Set both `maxFeePerGas` and `maxPriorityFeePerGas` to `"0x0"` to indicate that the bundler should sponsor all gas costs.
+3. **Gas Fee Overrides**: Set `maxFeePerGas`, `maxPriorityFeePerGas`, **and** `preVerificationGas` to `0n` on the user operation. All three must be zero — the bundler treats that as the signal to cover gas under the BSO policy.
## Requirements
diff --git a/content/wallets/pages/concepts/middleware.mdx b/content/wallets/pages/concepts/middleware.mdx
index 61c9489d6..bea793c8f 100644
--- a/content/wallets/pages/concepts/middleware.mdx
+++ b/content/wallets/pages/concepts/middleware.mdx
@@ -4,6 +4,8 @@ description: What is Middleware?
slug: wallets/concepts/middleware
---
+
+
The [Smart Account Client](/docs/wallets/concepts/smart-account-client) is extended with a series of middleware. When building transactions for signing and sending, the flow can be involved.
Sending a transaction requires getting the latest nonce for an account, checking if the account is deployed to set the `initCode` or `factory` and `factoryData`, estimating fees, estimating gas, and then signing the transaction.
If you want to sponsor gas, you need to generate some `dummyPaymasterAndData` to use during gas estimation, and after estimating gas you can request gas sponsorship.
diff --git a/content/wallets/pages/core/overview.mdx b/content/wallets/pages/core/overview.mdx
index 68fee7c46..2cf136b79 100644
--- a/content/wallets/pages/core/overview.mdx
+++ b/content/wallets/pages/core/overview.mdx
@@ -4,6 +4,8 @@ description: How to use Wallet APIs with other JavaScript frameworks
slug: wallets/core/overview
---
+
+
There are no current plans to support other JavaScript front-end frameworks such as Vue, Angular, or Svelte. However, there are a number of different ways to integrate Wallet APIs within your application if you're using one of these JavaScript frameworks.
diff --git a/content/wallets/pages/core/quickstart.mdx b/content/wallets/pages/core/quickstart.mdx
index a95b85987..471b66c4f 100644
--- a/content/wallets/pages/core/quickstart.mdx
+++ b/content/wallets/pages/core/quickstart.mdx
@@ -4,6 +4,9 @@ description: Learn how to get started with the Wallet APIs Core package
slug: wallets/core/quickstart
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
If you're not using React, but still want a number of the abstractions that make the React package straightforward to use, leverage the `@account-kit/core` package directly.
In this guide, learn how to use this package to send a transaction, while using the reactive utilities exported by this package.
diff --git a/content/wallets/pages/infra/quickstart.mdx b/content/wallets/pages/infra/quickstart.mdx
index 745b63427..d137360ed 100644
--- a/content/wallets/pages/infra/quickstart.mdx
+++ b/content/wallets/pages/infra/quickstart.mdx
@@ -4,6 +4,8 @@ description: Get started with Wallet APIs infrastructure
slug: wallets/infra/quickstart
---
+
+
Wallet APIs is composed of some lower-level libraries if you're looking for further customization of your stack or want more control over how you build your application.
One of these libraries is `@account-kit/infra` which allows you to interact with our infrastructure directly, while bringing your own smart contracts or signer.
In this guide, learn how to get started with `@account-kit/infra` and send a transaction. For smart contracts, the guide leverages `@account-kit/smart-contracts` to use `LightAccount`,
diff --git a/content/wallets/pages/low-level-infra/bundler/overview.mdx b/content/wallets/pages/low-level-infra/bundler/overview.mdx
index fd90bea87..574dc4457 100644
--- a/content/wallets/pages/low-level-infra/bundler/overview.mdx
+++ b/content/wallets/pages/low-level-infra/bundler/overview.mdx
@@ -4,9 +4,10 @@ description: Raw EIP-4337 Bundler APIs for advanced developers
slug: wallets/transactions/low-level-infra/bundler/overview
---
-The Bundler is a key component in the ERC-4337 stack, responsible for collecting, validating, and bundling UserOperations (UserOps) into transactions that are submitted to the EntryPoint contract on the blockchain. The Bundler APIs provide direct access to high-performance, scalable infrastructure (powered by Rundler), enabling developers to interact with a production-grade ERC-4337 bundler for reliable onchain submission of UserOps.
-These APIs are part of the `@account-kit/infra` library, allowing advanced customization when building smart wallet applications. They follow the standard ERC-4337 JSON-RPC endpoints and support versions v0.6, v0.7, and v0.8 of the protocol, with optimizations for gas estimation, validation, and error handling to ensure UserOps land efficiently while protecting against attacks.
+The Bundler is a key component in the ERC-4337 stack, responsible for collecting, validating, and bundling UserOperations (UserOps) into transactions submitted to the EntryPoint contract on the blockchain. The Bundler APIs provide direct access to Alchemy's high-performance, scalable infrastructure (powered by Rundler), letting developers interact with a production-grade ERC-4337 bundler for reliable onchain submission of UserOps.
-For full control, bring your own smart contracts or authentication providers while leveraging the bundler for scalability. If you prefer a higher-level abstraction, use the Wallet APIs or aa-sdk instead.
+These endpoints follow the standard ERC-4337 JSON-RPC interface and support v0.6, v0.7, and v0.8 of the protocol, with optimizations for gas estimation, validation, and error handling.
+
+If you want SDK-level access, use viem's [`createBundlerClient`](https://viem.sh/account-abstraction/clients/bundler) with `alchemyTransport({ apiKey })` from [`@alchemy/common`](/docs/wallets/reference/common) as the transport, and pass [`@alchemy/aa-infra`](/docs/wallets/reference/aa-infra)'s `estimateFeesPerGas` helper as the fee estimator. For a higher-level abstraction, use [`@alchemy/wallet-apis`](/docs/wallets/quickstart) instead.
diff --git a/content/wallets/pages/low-level-infra/bundler/sdk.mdx b/content/wallets/pages/low-level-infra/bundler/sdk.mdx
index 5f9df1d94..e5a3f02ba 100644
--- a/content/wallets/pages/low-level-infra/bundler/sdk.mdx
+++ b/content/wallets/pages/low-level-infra/bundler/sdk.mdx
@@ -1,68 +1,57 @@
---
title: Bundler API - Using SDK
-description: Use the @account-kit/infra SDK to interact with Alchemy's ERC-4337 bundler for advanced UserOperation management, including gas fee estimation and sending.
+description: Use viem's bundler client with @alchemy/aa-infra to interact with Alchemy's ERC-4337 bundler for advanced UserOperation management.
---
-**Required SDK version**: ^v4.59.1
+## Manage UserOperations with viem + `@alchemy/aa-infra`
-## Manage UserOperations with `@account-kit/infra`
-
-The `@account-kit/infra` library enables direct interaction with Alchemy's ERC-4337 bundler for advanced UserOperation management. The `alchemyFeeEstimator` function leverages underlying APIs, including `rundler_maxPriorityFeePerGas` and `eth_estimateUserOperation`, to estimate gas fees for UserOperations. Below are examples demonstrating how to estimate and send a UserOperation and how to retrieve a UserOperation by its hash using low-level Bundler APIs.
+The v5 stack uses viem's [`createBundlerClient`](https://viem.sh/account-abstraction/clients/bundler) for direct interaction with Alchemy's ERC-4337 bundler. [`@alchemy/aa-infra`](/docs/wallets/reference/aa-infra)'s `estimateFeesPerGas` helper wraps `rundler_getUserOperationGasPrice` so the bundler picks user-op-aware fees. Below: estimate-and-send a UserOperation, and retrieve a UserOperation by hash.
```ts title="config.ts"
-import { alchemy, sepolia } from "@account-kit/infra";
+import { sepolia } from "viem/chains";
-const YOUR_API_KEY = "";
-export const YOUR_PRIVATE_KEY = "";
export const chain = sepolia;
-
-export const transport = alchemy({
- apiKey: YOUR_API_KEY,
-});
+export const ALCHEMY_API_KEY = "";
+export const PRIVATE_KEY = "";
```
```ts title="estimateAndSendUserOperation.ts"
-import { alchemyFeeEstimator } from "@account-kit/infra";
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { entryPoint07Address } from "viem/account-abstraction";
-import {
- chain,
- transport
- YOUR_PRIVATE_KEY,
-} from "./config.ts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, type Hex } from "viem";
+import { privateKeyToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+import { chain, ALCHEMY_API_KEY, PRIVATE_KEY } from "./config";
+
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
export async function estimateAndSendUserOperation() {
- const client = await createModularAccountV2Client({
- signer: LocalAccountSigner.privateKeyToAccountSigner(YOUR_PRIVATE_KEY),
+ const rpcClient = createClient({ chain, transport });
+
+ const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(PRIVATE_KEY as Hex),
+ });
+
+ const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
chain,
transport,
- feeEstimator: alchemyFeeEstimator(transport),
+ userOperation: { estimateFeesPerGas },
});
try {
- let uo = await client.buildUserOperation({
- uo: {
- data: "0x",
- target: "0x0000000000000000000000000000000000000000",
- },
+ const hash = await bundlerClient.sendUserOperation({
+ calls: [
+ { to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" },
+ ],
});
- const uoWithSig = await client.signUserOperation({ uoStruct: uo });
- const sendResult = await client.sendRawUserOperation(
- uoWithSig,
- entryPoint07Address,
- );
- await client.waitForUserOperationTransaction({
- hash: sendResult,
- retries: {
- intervalMs: 100,
- maxRetries: 600,
- multiplier: 0,
- },
- });
- const receipt = await client.getUserOperationReceipt(sendResult);
+
+ const receipt = await bundlerClient.waitForUserOperationReceipt({ hash });
console.log("UserOperation receipt:", receipt);
} catch (error) {
console.error("Error processing UserOperation:", error);
@@ -71,20 +60,26 @@ export async function estimateAndSendUserOperation() {
```
```ts title="getUserOperationByHash.ts"
-import { createAlchemyPublicRpcClient } from "@account-kit/infra";
-import { chain, transport } from "./config.ts";
-import type { Hash } from "viem";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, type Hash } from "viem";
+import { alchemyTransport } from "@alchemy/common";
+import { chain, ALCHEMY_API_KEY } from "./config";
+
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
export async function getUserOperationByHash(uoHash: Hash) {
- const client = createAlchemyPublicRpcClient({
+ const rpcClient = createClient({ chain, transport });
+ const bundlerClient = createBundlerClient({
+ client: rpcClient,
chain,
transport,
});
+
try {
- let userOp = await client.getUserOperationByHash(uoHash);
- console.log("User Operation: ", userOp);
+ const userOp = await bundlerClient.getUserOperation({ hash: uoHash });
+ console.log("User Operation:", userOp);
} catch (error) {
- console.error("Error processing UserOperation:", error);
+ console.error("Error retrieving UserOperation:", error);
}
}
```
@@ -92,7 +87,5 @@ export async function getUserOperationByHash(uoHash: Hash) {
- Make sure that your environment is set with the correct `ALCHEMY_API_KEY` and
- `PRIVATE_KEY`. These examples assume familiarity with ERC-4337 and proper
- configuration of the EntryPoint contract (`entryPoint07Address`).
+ Make sure your environment has the correct `ALCHEMY_API_KEY` and `PRIVATE_KEY`. These examples target EntryPoint v0.7 by default via Modular Account V2.
diff --git a/content/wallets/pages/low-level-infra/overview.mdx b/content/wallets/pages/low-level-infra/overview.mdx
index 7665788dc..16c6aeaf8 100644
--- a/content/wallets/pages/low-level-infra/overview.mdx
+++ b/content/wallets/pages/low-level-infra/overview.mdx
@@ -6,10 +6,15 @@ slug: wallets/transactions/low-level-infra/overview
# Lower level infra overview
-Wallet APIs is composed of some lower-level libraries if you're looking for further customization of your stack or want more control over how you build your application and use third party providers.
-One of these libraries is `@account-kit/infra` which allows you to interact with the infrastructure directly, while bringing your own smart contracts or authentication provider.
+Wallet APIs is composed of some lower-level libraries if you want full control over how you build and send user operations, or want to combine Alchemy's bundler/paymaster infrastructure with your own smart contracts or third-party providers.
-If you do not need customization, use the Wallet APIs to simplify development.
+The advanced building blocks are:
+
+* **[`@alchemy/smart-accounts`](/docs/wallets/reference/smart-accounts)** — viem-compatible smart account implementations (Light Account, Modular Account V2, validation/permission builders).
+* **[`@alchemy/aa-infra`](/docs/wallets/reference/aa-infra)** — Alchemy bundler RPC types + the `estimateFeesPerGas` helper that wraps `rundler_getUserOperationGasPrice`.
+* **viem's [`account-abstraction`](https://viem.sh/account-abstraction)** module — `createBundlerClient`, `createPaymasterClient`, etc.
+
+If you do not need this level of customization, use [`@alchemy/wallet-apis`](/docs/wallets/quickstart) — it bundles all of the above behind a single `createSmartWalletClient` call.
diff --git a/content/wallets/pages/low-level-infra/quickstart.mdx b/content/wallets/pages/low-level-infra/quickstart.mdx
index ae9b625b0..d0e2922a4 100644
--- a/content/wallets/pages/low-level-infra/quickstart.mdx
+++ b/content/wallets/pages/low-level-infra/quickstart.mdx
@@ -4,46 +4,40 @@ description: Get started with Wallet APIs infrastructure
slug: wallets/low-level-infra/quickstart
---
-In this guide, learn how to get started with `@account-kit/infra` and send a transaction. For smart contracts, the guide leverages `@account-kit/smart-contracts` to use `ModularAccountV2`,
-but you're free to use any smart contract you'd like.
+In this guide, you'll wire up viem's bundler client to Alchemy's bundler with a Modular Account V2 from `@alchemy/smart-accounts` and send a user operation. This is the advanced path; if you don't need this level of control, use [`@alchemy/wallet-apis`](/docs/wallets/quickstart) instead.
## Prerequisites
* API key from your [dashboard](https://dashboard.alchemy.com/apps)
-* minimum Typescript version of 5 and the packages below from aa-sdk
+* TypeScript 5+ and the packages below
**Installation**
```bash yarn
- yarn add @account-kit/infra @account-kit/smart-contracts @aa-sdk/core viem
+ yarn add @alchemy/common @alchemy/aa-infra @alchemy/smart-accounts viem
```
```bash npm
- npm i -s @account-kit/infra @account-kit/smart-contracts @aa-sdk/core viem
+ npm i @alchemy/common @alchemy/aa-infra @alchemy/smart-accounts viem
```
## Create a Gas Manager policy
-If you want to sponsor gas, then you'll also want to create a gas policy in the Gas Manager dashboard.
+If you want to sponsor gas, create a policy in the Gas Manager dashboard.
## Create a client
-Now that you have an API key and a Policy ID, you can create a [Smart Account Client](/docs/wallets/concepts/smart-account-client) to interact with the infrastructure.
-
- Replace `YOUR_API_KEY` and `POLICY_ID` with the key and Policy ID created
- above.
+ Replace `YOUR_API_KEY` and `POLICY_ID` with the values from the steps above.
-## Send a transaction
-
-The last step is to send a transaction using the client you created.
+## Send a user operation
```ts example.ts
@@ -51,12 +45,17 @@ The last step is to send a transaction using the client you created.
const client = await getClient();
- const { hash } = await client.sendUserOperation({
- uo: {
- target: "0xTARGET_ADDRESS",
- data: "0x",
- value: 0n,
- },
+ const hash = await client.sendUserOperation({
+ calls: [
+ {
+ to: "0x0000000000000000000000000000000000000000",
+ value: 0n,
+ data: "0x",
+ },
+ ],
});
+
+ const receipt = await client.waitForUserOperationReceipt({ hash });
+ console.log("Mined:", receipt.receipt.transactionHash);
```
diff --git a/content/wallets/pages/low-level-infra/third-party-infrastructure/bundlers.mdx b/content/wallets/pages/low-level-infra/third-party-infrastructure/bundlers.mdx
index fa8deadce..d67dd1383 100644
--- a/content/wallets/pages/low-level-infra/third-party-infrastructure/bundlers.mdx
+++ b/content/wallets/pages/low-level-infra/third-party-infrastructure/bundlers.mdx
@@ -1,77 +1,50 @@
---
title: Third Party Bundlers
-description: Learn how to use a different RPC provider with Wallet APIs
+description: Learn how to use a non-Alchemy bundler with the v5 stack
slug: wallets/low-level-infra/third-party-infrastructure/bundlers
---
-The `SmartAccountClient` within `@aa-sdk/core` is unopinionated about which bundler you use, so you can connect to any RPC provider.
+You can use any ERC-4337 bundler with the v5 stack by combining viem's bundler client with a smart account from `@alchemy/smart-accounts`. The bundler RPC URL is just a viem transport — point it wherever you like.
## Usage
-If we look at the example for creating a `SmartAccountClient`:
-
```ts twoslash
-import { createSmartAccountClient } from "@aa-sdk/core";
-import { http } from "viem";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, http } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { sepolia } from "viem/chains";
+import { toLightAccount } from "@alchemy/smart-accounts";
-const client = createSmartAccountClient({
- transport: http("https://polygon-mumbai.g.alchemy.com/v2/demo"),
+// 1. RPC client for chain reads — uses any RPC provider.
+const rpcClient = createClient({
chain: sepolia,
+ transport: http("https://your-rpc-provider.example.com"),
});
-```
-
-You can see that we set the `transport` to `http("https://polygon-mumbai.g.alchemy.com/v2/demo")`. You can swap out that the url in the `http` function to
-any other provider's URL.
-
- Depending on your provider, you may have to pass in custom logic for the
- `gasEstimator` and `feeEstimator` properties when calling
- `createSmartAccountClient`. Consult with your provider on what the correct
- logic is.
-
-
-## Splitting bundler traffic and node RPC traffic
-
-You may want to use a different RPC provider for your bundler traffic and your node traffic. This is a common use case, and you can do this by leveraging the [`split`](/docs/wallets/reference/aa-sdk/core/functions/split) transport and passing it to your `createSmartAccountClient` call. For example:
-
-### Bundler and Gas Manager with third-party RPCs
-
-If you want to split your node traffic from the bundler traffic, do this with the `alchemyTransport`
+// 2. Smart account implementation from @alchemy/smart-accounts.
+const owner = privateKeyToAccount(generatePrivateKey());
+const account = await toLightAccount({
+ client: rpcClient,
+ owner,
+ version: "v2.0.0",
+});
-```ts twoslash
-import { alchemy } from "@account-kit/infra";
+// 3. Bundler client pointed at any third-party bundler URL.
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport: http("https://your-third-party-bundler.example.com"),
+});
-const alchemyTransport = alchemy({
- alchemyConnection: { apiKey: "your-api-key" },
- nodeRpcUrl: "YOUR_NODE_RPC_URL",
+// 4. Send a user operation through the third-party bundler.
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
});
-// now use this transport in a client
```
-### Using two different 3rd party bundler and node RPCs
-
-```ts twoslash
-import { split } from "@aa-sdk/core";
-import { createPublicClient, http } from "viem";
+## Splitting bundler traffic and node RPC traffic
-const bundlerMethods = [
- "eth_sendUserOperation",
- "eth_estimateUserOperationGas",
- "eth_getUserOperationReceipt",
- "eth_getUserOperationByHash",
- "eth_supportedEntryPoints",
-];
+The `rpcClient` (chain reads) and `bundlerClient` (user-operation RPCs) use independent transports, so you can already point them at different providers. Use one provider's URL for `rpcClient` and another for `bundlerClient` and you're done — no special `split` transport needed.
-const clientWithSplit = createPublicClient({
- transport: split({
- overrides: [
- {
- methods: bundlerMethods,
- transport: http("BUNDLER_RPC_URL"),
- },
- ],
- fallback: http("OTHER_RPC_URL"),
- }),
-});
-```
+If you'd rather route a single client across multiple providers (e.g. fall back to a different RPC on certain methods), use viem's [`fallback`](https://viem.sh/docs/clients/transports/fallback) transport.
diff --git a/content/wallets/pages/low-level-infra/third-party-infrastructure/chains.mdx b/content/wallets/pages/low-level-infra/third-party-infrastructure/chains.mdx
index 7a1b7a53b..03d3af7b3 100644
--- a/content/wallets/pages/low-level-infra/third-party-infrastructure/chains.mdx
+++ b/content/wallets/pages/low-level-infra/third-party-infrastructure/chains.mdx
@@ -1,35 +1,49 @@
---
-title: Third party chains
-description: Learn how to use a 3rd party chain
+title: Non-Alchemy chains
+description: Learn how to run the v5 stack on a chain Alchemy doesn't support
slug: wallets/low-level-infra/third-party-infrastructure/chains
---
-If you're using a chain that isn't supported or only smart account methods are available for those chains, use `createSmartAccountClient` or one of the non-alchemy `create*Client` methods from `@account-kit/smart-contracts`
-with a `split` transport to route your traffic accordingly.
+To use the v5 stack on a chain that Alchemy doesn't support, define the chain with viem's `defineChain` and point your public client and bundler client at your own RPC and bundler URLs.
-## Smart account only chains example
-
-For smart account only chains, the `alchemyTransport` allows you to specify both the connection object as well as pass in a Node RPC URL, allowing you to split traffic between the Bundler and Paymaster RPC and your Node RPC provider.
+## Usage
```ts twoslash
-import { zora, alchemy } from "@account-kit/infra";
-import { createLightAccountAlchemyClient } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-
-const smartAccountClient = createLightAccountAlchemyClient({
- transport: alchemy({
- alchemyConnection: {
- apiKey: "ALCHEMY_API_KEY",
- },
- nodeRpcUrl: "ZORA_NODE_RPC_URL",
- }),
- chain: zora,
- signer: LocalAccountSigner.generatePrivateKeySigner(),
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, defineChain, http } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { toLightAccount } from "@alchemy/smart-accounts";
+
+// 1. Define your chain.
+const myChain = defineChain({
+ id: 12345,
+ name: "MyChain",
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
+ rpcUrls: {
+ default: { http: ["https://rpc.mychain.example.com"] },
+ },
});
-```
-## Non-Alchemy chains example
+// 2. RPC client + smart account against that chain.
+const rpcClient = createClient({ chain: myChain, transport: http() });
+const owner = privateKeyToAccount(generatePrivateKey());
+const account = await toLightAccount({
+ client: rpcClient,
+ owner,
+ version: "v2.0.0",
+});
-To use non-Alchemy supported chains, use the `createSmartAccountClient` method from `@aa-sdk/core` or any of the non-Alchemy `create*Client` methods exported from `@account-kit/smart-contracts` with a `chain` definition for your chain and a `transport` pointing to your RPC provider.
+// 3. Bundler client at the third-party bundler RPC.
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: myChain,
+ transport: http("https://bundler.mychain.example.com"),
+});
+
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
+});
+```
-See [`createSmartAccountClient`](/docs/wallets/reference/aa-sdk/core/functions/createSmartAccountClient) for more information.
+To add gas sponsorship via a third-party paymaster on this chain, see [Third Party Paymasters](/docs/wallets/low-level-infra/third-party-infrastructure/paymasters).
diff --git a/content/wallets/pages/low-level-infra/third-party-infrastructure/paymasters.mdx b/content/wallets/pages/low-level-infra/third-party-infrastructure/paymasters.mdx
index be04a8a31..a6fc048df 100644
--- a/content/wallets/pages/low-level-infra/third-party-infrastructure/paymasters.mdx
+++ b/content/wallets/pages/low-level-infra/third-party-infrastructure/paymasters.mdx
@@ -1,35 +1,93 @@
---
title: Third Party Paymasters
-description: Learn how to use a 3rd party Paymaster with Wallet APIs
+description: Learn how to use a non-Alchemy paymaster with the v5 stack
slug: wallets/low-level-infra/third-party-infrastructure/paymasters
---
-The `SmartAccountClient` within `@aa-sdk/core` is unopinionated about which paymaster you use, so you can connect to any paymaster. Configure it using the `paymasterAndData` config option when you call `createSmartAccountClient`.
+You can sponsor user operations with any paymaster by passing a `paymaster` option to viem's bundler client. Two approaches:
-## Usage
+* **Custom paymaster object** — supply your own `getPaymasterData` / `getPaymasterStubData` callbacks that call your provider's API.
+* **ERC-7677 paymaster** — if your provider speaks the [ERC-7677](https://eips.ethereum.org/EIPS/eip-7677) RPC, use viem's `createPaymasterClient` directly.
+
+## Custom paymaster
```ts twoslash
-import { createSmartAccountClient } from "@aa-sdk/core";
-import { http } from "viem";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, http } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { sepolia } from "viem/chains";
+import { toLightAccount } from "@alchemy/smart-accounts";
+
+const rpcClient = createClient({ chain: sepolia, transport: http() });
+const owner = privateKeyToAccount(generatePrivateKey());
+const account = await toLightAccount({
+ client: rpcClient,
+ owner,
+ version: "v2.0.0",
+});
-const chain = sepolia;
-const client = createSmartAccountClient({
- chain,
- transport: http("RPC_URL"),
- // sets the dummy paymasterAndData with paymaster address appended with some dummy paymasterData
- // that looks like a valid paymasterData
- dummyPaymasterAndData: async (userop) => ({
- ...userop,
- paymasterAndData: `0x`,
- }),
- paymasterAndData: async (userop, opts) => {
- // call your paymaster here to sponsor the userop
- // leverage the `opts` field to apply any overrides
- return {
- ...userop,
- paymasterAndData: "0xresponsefromprovider",
- };
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport: http("https://your-bundler.example.com"),
+ paymaster: {
+ async getPaymasterData(_parameters) {
+ // Call your third-party paymaster's API here.
+ return {
+ paymaster: "0xabc123...",
+ paymasterData: "0x",
+ paymasterVerificationGasLimit: 100000n,
+ paymasterPostOpGasLimit: 50000n,
+ };
+ },
+ async getPaymasterStubData(_parameters) {
+ // Stub values used during gas estimation; same shape as above.
+ return {
+ paymaster: "0xabc123...",
+ paymasterData: "0x",
+ paymasterVerificationGasLimit: 100000n,
+ paymasterPostOpGasLimit: 50000n,
+ };
+ },
},
});
+
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
+});
+```
+
+## ERC-7677 paymaster
+
+```ts twoslash
+import { createBundlerClient, createPaymasterClient } from "viem/account-abstraction";
+import { createClient, http } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { sepolia } from "viem/chains";
+import { toLightAccount } from "@alchemy/smart-accounts";
+
+const rpcClient = createClient({ chain: sepolia, transport: http() });
+const owner = privateKeyToAccount(generatePrivateKey());
+const account = await toLightAccount({
+ client: rpcClient,
+ owner,
+ version: "v2.0.0",
+});
+
+const paymasterClient = createPaymasterClient({
+ transport: http("https://your-erc7677-paymaster.example.com"),
+});
+
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport: http("https://your-bundler.example.com"),
+ paymaster: paymasterClient,
+});
+
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
+});
```
diff --git a/content/wallets/pages/overview/supported-chains.mdx b/content/wallets/pages/overview/supported-chains.mdx
index f09573669..62ed50478 100644
--- a/content/wallets/pages/overview/supported-chains.mdx
+++ b/content/wallets/pages/overview/supported-chains.mdx
@@ -7,7 +7,7 @@ Alchemy [**Wallet APIs**](/docs/wallets/api-reference/smart-wallets/wallet-api-e
To send transactions and sponsor gas, we provide the [**Bundler API**](/docs/wallets/transactions/low-level-infra/bundler/overview/api-endpoints), [**Gas Manager API**](/docs/wallets/low-level-infra/gas-manager/gas-sponsorship/api-endpoints), and [**Wallet APIs**](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-request-account) on the networks in the table below. You can use [multiple chains at once](/docs/wallets/recipes/multi-chain-setup) in the same app.
-Import chains from `@account-kit/infra`.
+Import chains from `viem/chains`. For chains viem doesn't ship (e.g. Gensyn Testnet, Rise Testnet, Robinhood Testnet), import from `@alchemy/common/chains` instead.
## Need another chain?
diff --git a/content/wallets/pages/react-native/getting-started/app-integration.mdx b/content/wallets/pages/react-native/getting-started/app-integration.mdx
index ae424e9e5..ae5cc5f14 100644
--- a/content/wallets/pages/react-native/getting-started/app-integration.mdx
+++ b/content/wallets/pages/react-native/getting-started/app-integration.mdx
@@ -4,6 +4,9 @@ description: Setup authentication to smart wallets on React Native
slug: wallets/react-native/getting-started/app-integration
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
In this step you will get authentication working so users can log in to their smart wallets.
## Set up your application
diff --git a/content/wallets/pages/react-native/getting-started/getting-started-expo.mdx b/content/wallets/pages/react-native/getting-started/getting-started-expo.mdx
index e9ec71fd6..b7261d809 100644
--- a/content/wallets/pages/react-native/getting-started/getting-started-expo.mdx
+++ b/content/wallets/pages/react-native/getting-started/getting-started-expo.mdx
@@ -4,6 +4,8 @@ description: A guide on integrating Wallet APIs within a React Native Expo appli
slug: wallets/react-native/getting-started/getting-started-expo
---
+
+
Follow these steps to get your environment set up for using Wallet APIs within a React Native application on Expo.
diff --git a/content/wallets/pages/react-native/getting-started/getting-started-rn-bare.mdx b/content/wallets/pages/react-native/getting-started/getting-started-rn-bare.mdx
index 512942f50..9241bf9be 100644
--- a/content/wallets/pages/react-native/getting-started/getting-started-rn-bare.mdx
+++ b/content/wallets/pages/react-native/getting-started/getting-started-rn-bare.mdx
@@ -4,6 +4,8 @@ description: A guide on integrating Wallet APIs within a Bare React Native appli
slug: wallets/react-native/getting-started/getting-started-rn-bare
---
+
+
This guide assumes you already have a Bare React Native app and want to
integrate Wallet APIs. If you are starting a project from scratch,
diff --git a/content/wallets/pages/react/connectors/customization.mdx b/content/wallets/pages/react/connectors/customization.mdx
index 7309a0690..bfb80728c 100644
--- a/content/wallets/pages/react/connectors/customization.mdx
+++ b/content/wallets/pages/react/connectors/customization.mdx
@@ -4,6 +4,8 @@ description: Customize external wallet connectors including ordering and feature
slug: wallets/react/connectors/customization
---
+
+
### External wallets (EVM + Solana)
Use `configForExternalWallets()` to specify external wallets you want to support, then add to your UI configuration. This allows you to feature wallets, merge EVM/Solana variants by name, and enable WalletConnect.
diff --git a/content/wallets/pages/react/customization/tailwind-setup.mdx b/content/wallets/pages/react/customization/tailwind-setup.mdx
index 680938359..0a5796adb 100644
--- a/content/wallets/pages/react/customization/tailwind-setup.mdx
+++ b/content/wallets/pages/react/customization/tailwind-setup.mdx
@@ -4,6 +4,8 @@ description: Complete guide to setting up Tailwind CSS with UI components
slug: wallets/react/tailwind-setup
---
+
+
To use pre-built UI components for user login Tailwind CSS must be configured in your project. This guide walks you through the complete setup process for both new and existing Tailwind installations.
## Prerequisites
diff --git a/content/wallets/pages/react/customization/theme.mdx b/content/wallets/pages/react/customization/theme.mdx
index 3acaebdf5..c3259dc1d 100644
--- a/content/wallets/pages/react/customization/theme.mdx
+++ b/content/wallets/pages/react/customization/theme.mdx
@@ -4,6 +4,8 @@ description: Customize the theme of your Wallet APIs app
slug: wallets/react/customization/theme
---
+
+
UI components are fully customizable so you can match your app's branding.
What you can customize in the pre-built authentication component:
diff --git a/content/wallets/pages/react/getting-started/existing-project.mdx b/content/wallets/pages/react/getting-started/existing-project.mdx
index 0d2c3dd5a..c15fbc574 100644
--- a/content/wallets/pages/react/getting-started/existing-project.mdx
+++ b/content/wallets/pages/react/getting-started/existing-project.mdx
@@ -6,6 +6,8 @@ link: /react/quickstart/existing-project
slug: wallets/react/quickstart/existing-project
---
+
+
## Initializing the provider
Wrap your application with the provider to enable embedded wallet functionality.
diff --git a/content/wallets/pages/react/getting-started/initialization.mdx b/content/wallets/pages/react/getting-started/initialization.mdx
index 65e020542..d4c8455d8 100644
--- a/content/wallets/pages/react/getting-started/initialization.mdx
+++ b/content/wallets/pages/react/getting-started/initialization.mdx
@@ -4,6 +4,8 @@ description: Build Wallet APIs in a new app
slug: wallets/react/installation
---
+
+
In this doc and the three following, you will build a Next.js application with Wallet APIs from scratch. If you are using any other tech stack, follow along with the key points of integration and adjust as needed!
## Create a new Next.js app
diff --git a/content/wallets/pages/react/getting-started/ui-customization.mdx b/content/wallets/pages/react/getting-started/ui-customization.mdx
index 516edeb97..2b348a1de 100644
--- a/content/wallets/pages/react/getting-started/ui-customization.mdx
+++ b/content/wallets/pages/react/getting-started/ui-customization.mdx
@@ -6,6 +6,8 @@ link: /react/quickstart/ui-customization
slug: wallets/react/quickstart/ui-customization
---
+
+
This guide assumes you're using Next.js. If you're using a different framework
or need more info, check out the [full tailwind setup
diff --git a/content/wallets/pages/react/login-methods/adding-and-removing-login-methods.mdx b/content/wallets/pages/react/login-methods/adding-and-removing-login-methods.mdx
index 50baf79b9..138948f39 100644
--- a/content/wallets/pages/react/login-methods/adding-and-removing-login-methods.mdx
+++ b/content/wallets/pages/react/login-methods/adding-and-removing-login-methods.mdx
@@ -4,6 +4,8 @@ description: Learn how to add and remove login methods to an account
slug: wallets/signer/authentication/adding-and-removing-login-methods
---
+
+
If your user has already authenticated with email, social auth, or a passkey, you can add additional login methods to their account or remove currently enabled methods from their account. This is useful in the case that you want to give your users the ability to customize their login methods after their account is created.
## Viewing the currently enabled auth methods
diff --git a/content/wallets/pages/react/login-methods/eoa-login.mdx b/content/wallets/pages/react/login-methods/eoa-login.mdx
index d2b042cd9..a10effdd7 100644
--- a/content/wallets/pages/react/login-methods/eoa-login.mdx
+++ b/content/wallets/pages/react/login-methods/eoa-login.mdx
@@ -4,6 +4,8 @@ description: How to connect external wallets on EVM and Solana
slug: wallets/react/login-methods/eoa-login
---
+
+
# Overview
Connectors let users authenticate with existing **external wallets**. Both **EVM** (e.g., MetaMask, Coinbase, WalletConnect) and **Solana** (e.g., Phantom) wallets are supported via UI components or custom UI, and can be surfaced together in your auth modal.
diff --git a/content/wallets/pages/react/login-methods/onchain-passkeys.mdx b/content/wallets/pages/react/login-methods/onchain-passkeys.mdx
index 31e8baeda..a2e5e5012 100644
--- a/content/wallets/pages/react/login-methods/onchain-passkeys.mdx
+++ b/content/wallets/pages/react/login-methods/onchain-passkeys.mdx
@@ -4,6 +4,8 @@ description: How to use onchain passkeys to authenticate users and send transact
slug: wallets/react/login-methods/onchain-passkeys
---
+
+
**This feature is in early access.**
The WebAuthn Modular Account enables password-less authentication onchain using **passkeys** (via WebAuthn), and is compatible with [Wallet APIs](/docs/wallets). This guide demonstrates how to register credentials, authenticate users, and send transactions using the `@account-kit/smart-contracts` package.
diff --git a/content/wallets/pages/react/login-methods/passkey-login.mdx b/content/wallets/pages/react/login-methods/passkey-login.mdx
index a8014a27f..379b4e456 100644
--- a/content/wallets/pages/react/login-methods/passkey-login.mdx
+++ b/content/wallets/pages/react/login-methods/passkey-login.mdx
@@ -4,6 +4,8 @@ description: How to implement Passkey Login authentication in your React app
slug: wallets/react/login-methods/passkey-login
---
+
+
If a user has added a passkey to their account, or they initially signed up with a passkey, authenticate them using that passkey. This provides a secure, passwordless authentication experience.
You can implement Passkey Login authentication in two ways:
diff --git a/content/wallets/pages/react/login-methods/social-providers.mdx b/content/wallets/pages/react/login-methods/social-providers.mdx
index d2c574b64..934f645d4 100644
--- a/content/wallets/pages/react/login-methods/social-providers.mdx
+++ b/content/wallets/pages/react/login-methods/social-providers.mdx
@@ -4,6 +4,8 @@ description: How to implement custom social providers using Auth0 in your React
slug: wallets/react/login-methods/social-providers
---
+
+
In addition to the standard social login providers (Google, Facebook, Apple), Wallet APIs allows you to integrate custom OAuth providers through Auth0. This gives you flexibility to add authentication methods like GitHub, Twitter, LinkedIn, and more.
You can implement custom social providers in two ways:
diff --git a/content/wallets/pages/react/mfa/email-magic-link.mdx b/content/wallets/pages/react/mfa/email-magic-link.mdx
index 3e80ef50e..0b0b1561a 100644
--- a/content/wallets/pages/react/mfa/email-magic-link.mdx
+++ b/content/wallets/pages/react/mfa/email-magic-link.mdx
@@ -4,6 +4,8 @@ description: How to authenticate users with Email Magic Link and MFA in your Rea
slug: wallets/react/mfa/email-magic-link
---
+
+
This guide shows you how to implement authentication with Email Magic Link and TOTP-based multi-factor authentication in your React application.
## Overview
diff --git a/content/wallets/pages/react/mfa/email-otp.mdx b/content/wallets/pages/react/mfa/email-otp.mdx
index b819ec34f..cd5f957ea 100644
--- a/content/wallets/pages/react/mfa/email-otp.mdx
+++ b/content/wallets/pages/react/mfa/email-otp.mdx
@@ -4,6 +4,8 @@ description: How to authenticate using Email OTP when MFA is enabled
slug: wallets/react/mfa/email-otp
---
+
+
This guide shows you how to implement Email OTP authentication when a user has multi-factor authentication (MFA) enabled.
## Overview
diff --git a/content/wallets/pages/react/mfa/setup-mfa.mdx b/content/wallets/pages/react/mfa/setup-mfa.mdx
index 623995eef..1ce9360f1 100644
--- a/content/wallets/pages/react/mfa/setup-mfa.mdx
+++ b/content/wallets/pages/react/mfa/setup-mfa.mdx
@@ -4,6 +4,8 @@ description: How to set up additional security with authenticator apps in your R
slug: wallets/react/mfa/setup-mfa
---
+
+
With Wallet APIs, multi-factor authentication (MFA) uses authenticator apps—like Google Authenticator, Authy, or Microsoft Authenticator—to generate a Time-based One-Time Password (TOTP).
diff --git a/content/wallets/pages/react/mfa/social-login.mdx b/content/wallets/pages/react/mfa/social-login.mdx
index 9912e6f79..c60281654 100644
--- a/content/wallets/pages/react/mfa/social-login.mdx
+++ b/content/wallets/pages/react/mfa/social-login.mdx
@@ -4,6 +4,8 @@ description: How to authenticate users with Social Login when MFA is enabled
slug: wallets/react/mfa/social-login
---
+
+
This guide shows you how to implement social login authentication when a user has multi-factor authentication (MFA) enabled.
## Overview
diff --git a/content/wallets/pages/react/react-hooks.mdx b/content/wallets/pages/react/react-hooks.mdx
index e935f3dfd..f74f3c7d9 100644
--- a/content/wallets/pages/react/react-hooks.mdx
+++ b/content/wallets/pages/react/react-hooks.mdx
@@ -4,6 +4,8 @@ description: Overview of implementing custom authentication UI in your React app
slug: wallets/react/react-hooks
---
+
+
Pre-built UI components are available for authentication, but you may want to create your own custom UI to match your application's design system. This section covers how to implement custom authentication flows using Wallet APIs hooks.
diff --git a/content/wallets/pages/react/solana-wallets/get-started.mdx b/content/wallets/pages/react/solana-wallets/get-started.mdx
index d1d072fce..5ccf53071 100644
--- a/content/wallets/pages/react/solana-wallets/get-started.mdx
+++ b/content/wallets/pages/react/solana-wallets/get-started.mdx
@@ -5,6 +5,8 @@ text: Getting started with Solana Wallet APIs
slug: wallets/react/solana-wallets/get-started
---
+
+
Solana Wallet support is in beta. Please [reach
out](mailto:support@alchemy.com) if you run into any issues or need support
diff --git a/content/wallets/pages/react/ui-components.mdx b/content/wallets/pages/react/ui-components.mdx
index 95865949b..76a6858eb 100644
--- a/content/wallets/pages/react/ui-components.mdx
+++ b/content/wallets/pages/react/ui-components.mdx
@@ -4,6 +4,8 @@ description: How to use our pre-built authentication component in your React app
slug: wallets/react/ui-components
---
+
+
Wallet APIs allows you to use pre-built, highly customizable UI components to handle authenticating your users. These components provide flexibility to:
* Use the pre-built [modal](#modal-auth) or [embed](#embedded-auth) the auth card directly in your app
diff --git a/content/wallets/pages/recipes/hyperliquid-wallets.mdx b/content/wallets/pages/recipes/hyperliquid-wallets.mdx
index 990ca2d83..727506af1 100644
--- a/content/wallets/pages/recipes/hyperliquid-wallets.mdx
+++ b/content/wallets/pages/recipes/hyperliquid-wallets.mdx
@@ -4,6 +4,8 @@ description: Step-by-step guide to let users send transactions on hyperliquid.
slug: wallets/recipes/hyperliquid-wallets
---
+
+
By the end of this tutorial, you’ll have an application integrated with Wallet APIs, enabling email and social login for user authentication, and the ability to sign and send transactions seamlessly.
***
diff --git a/content/wallets/pages/recipes/multi-chain-setup.mdx b/content/wallets/pages/recipes/multi-chain-setup.mdx
index b4e4efe1d..66e454ff1 100644
--- a/content/wallets/pages/recipes/multi-chain-setup.mdx
+++ b/content/wallets/pages/recipes/multi-chain-setup.mdx
@@ -4,6 +4,8 @@ description: Learn how to build multi-chain apps with Wallet APIs.
slug: wallets/recipes/multi-chain-setup
---
+
+
Multi-chain apps are supported, allowing you to build applications that interact with multiple blockchains. This guide shows you how to set up your app to work with multiple chains.
## Update your config
diff --git a/content/wallets/pages/recipes/onramp-funds.mdx b/content/wallets/pages/recipes/onramp-funds.mdx
index a5bf148e7..ea3d78f9c 100644
--- a/content/wallets/pages/recipes/onramp-funds.mdx
+++ b/content/wallets/pages/recipes/onramp-funds.mdx
@@ -4,6 +4,8 @@ description: Step-by-step guide to let users buy crypto with Coinbase Onramp and
slug: wallets/recipes/onramp-funds
---
+
+
This recipe demonstrates how to integrate the Coinbase Onramp into an app that uses **embedded smart wallets**. It uses the [Coinbase Developer Platform](https://docs.cdp.coinbase.com/onramp/introduction/welcome) and assumes you've configured your Wallet APIs integration using `@account-kit/react`.
> **Goal**: Let users seamlessly buy crypto (e.g. ETH) via card and fund their smart wallet directly.
diff --git a/content/wallets/pages/recipes/programmatic-wallet-creation.mdx b/content/wallets/pages/recipes/programmatic-wallet-creation.mdx
index 0d3fe6e4e..227c65c61 100644
--- a/content/wallets/pages/recipes/programmatic-wallet-creation.mdx
+++ b/content/wallets/pages/recipes/programmatic-wallet-creation.mdx
@@ -4,6 +4,8 @@ description: Generate a signer, initialize a Smart Wallet Client, sponsor gas wi
slug: wallets/recipes/programmatic-wallet-creation
---
+
+
This recipe shows how to programmatically create a smart wallet: you’ll generate a signer, spin up a Smart Wallet Client, read the counterfactual address before deployment and deploy the account by sending your first gas sponsored `UserOperation` (prepared → signed → sent).
diff --git a/content/wallets/pages/recipes/send-usdc.mdx b/content/wallets/pages/recipes/send-usdc.mdx
index f6cb5fa4e..08c0ea63c 100644
--- a/content/wallets/pages/recipes/send-usdc.mdx
+++ b/content/wallets/pages/recipes/send-usdc.mdx
@@ -4,6 +4,8 @@ description: Learn how to build and send a transaction that transfers USDC from
slug: wallets/recipes/send-usdc
---
+
+
In this recipe you'll construct an ERC-20 `transfer` call and submit it through a Smart Account Client. The same pattern works for **any** ERC-20 token; just swap the token address and number of decimals.
> Prefer code? Jump straight to the **React** or **Core** tabs below.
diff --git a/content/wallets/pages/recipes/smart-wallets-aave.mdx b/content/wallets/pages/recipes/smart-wallets-aave.mdx
index ebb4bb75a..60cd2e35a 100644
--- a/content/wallets/pages/recipes/smart-wallets-aave.mdx
+++ b/content/wallets/pages/recipes/smart-wallets-aave.mdx
@@ -7,6 +7,8 @@ description: >-
slug: wallets/recipes/smart-wallets-aave
---
+
+
Learn how to build DeFi applications that interact with Aave using Wallet APIs. This recipe covers supplying and withdrawing assets with both the [Core library](/docs/wallets/reference/aa-sdk/core) and the [Wallet APIs](/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-request-account) for seamless user experiences.
## Prerequisites
diff --git a/content/wallets/pages/recipes/wallet-session-keys-app.mdx b/content/wallets/pages/recipes/wallet-session-keys-app.mdx
index 32e96b008..37ad39e43 100644
--- a/content/wallets/pages/recipes/wallet-session-keys-app.mdx
+++ b/content/wallets/pages/recipes/wallet-session-keys-app.mdx
@@ -4,6 +4,9 @@ description: Demo app showcasing session keys on Wallet APIs, with examples for
slug: wallets/recipes/wallet-session-keys-app
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
Below is a demo application that showcases session keys on Wallet APIs.

diff --git a/content/wallets/pages/resources/types.mdx b/content/wallets/pages/resources/types.mdx
index 6591d199d..fae4e995e 100644
--- a/content/wallets/pages/resources/types.mdx
+++ b/content/wallets/pages/resources/types.mdx
@@ -4,6 +4,8 @@ description: Glossary of types in aa-sdk
slug: wallets/resources/types
---
+
+
## `BatchUserOperationCallData`
An array of `UserOperationCallData`, representing a sequence of `UserOperations` to be executed in batch by calling the `executeBatch` function on the `SmartContractAccount` contract. Check out the [batch transactions guide](/docs/wallets/transactions/send-batch-transactions) to learn more about batching multiple transactions into a single `UserOperation`.
diff --git a/content/wallets/pages/signer/authentication/email-magic-link.mdx b/content/wallets/pages/signer/authentication/email-magic-link.mdx
index 12796c35a..75b3e6282 100644
--- a/content/wallets/pages/signer/authentication/email-magic-link.mdx
+++ b/content/wallets/pages/signer/authentication/email-magic-link.mdx
@@ -4,6 +4,8 @@ description: Authenticate a user using an email magic link
slug: wallets/signer/authentication/email-magic-link
---
+
+
Email magic link authentication allows you to log in and sign up users using an email address. Your users will receive a link in their inbox which will redirect them to your site (configured in the dashboard) to complete login.
diff --git a/content/wallets/pages/signer/authentication/email-otp.mdx b/content/wallets/pages/signer/authentication/email-otp.mdx
index 3a1540364..fec908b5b 100644
--- a/content/wallets/pages/signer/authentication/email-otp.mdx
+++ b/content/wallets/pages/signer/authentication/email-otp.mdx
@@ -4,6 +4,8 @@ description: Authenticate a user using an email OTP code
slug: wallets/authentication/login-methods/email-otp
---
+
+
Email OTP authentication allows you to log in and sign up users using an email address. Your users will receive six-digit code in their inbox which they can enter in your site to complete login.
diff --git a/content/wallets/pages/signer/authentication/server-wallets.mdx b/content/wallets/pages/signer/authentication/server-wallets.mdx
index 7b4129789..432f83004 100644
--- a/content/wallets/pages/signer/authentication/server-wallets.mdx
+++ b/content/wallets/pages/signer/authentication/server-wallets.mdx
@@ -4,6 +4,8 @@ description: Control wallets programmatically using access keys
slug: wallets/authentication/login-methods/server-wallets
---
+
+
Server wallets are in early access. Contact wallets@alchemy.com for help
getting up and running.
diff --git a/content/wallets/pages/signer/export-private-key.mdx b/content/wallets/pages/signer/export-private-key.mdx
index 0709f600b..4f1de8e27 100644
--- a/content/wallets/pages/signer/export-private-key.mdx
+++ b/content/wallets/pages/signer/export-private-key.mdx
@@ -5,6 +5,8 @@ description: Learn how to enable a user to export their private key with Wallet
slug: wallets/signer/export-private-key
---
+
+
You can export a user's private key, giving them the right to exit at any time. Allowing your users to export their private key is a best practice, as it gives them full control over their account. The private key export method does not rely on external infrastructure, so a user can always export their private key.
## Using [useExportAccount](/docs/wallets/reference/account-kit/react/hooks/useExportAccount)
diff --git a/content/wallets/pages/signer/overview.mdx b/content/wallets/pages/signer/overview.mdx
index 9368b5673..cefc3fea9 100644
--- a/content/wallets/pages/signer/overview.mdx
+++ b/content/wallets/pages/signer/overview.mdx
@@ -4,6 +4,8 @@ description: Overview of the authentication provider
slug: wallets/signer/overview
---
+
+
The AlchemySigner is a `SmartAccountSigner` that is protected by secure infrastructure. Using the AlchemySigner, you can get started building embedded accounts with an API key.
When using Wallet APIs with React via `@account-kit/react` or `@account-kit/core`, the assumption is that you're using the AlchemySigner with the smart contract implementations and transaction sending and gas sponsorship infrastructure.
diff --git a/content/wallets/pages/signer/quickstart.mdx b/content/wallets/pages/signer/quickstart.mdx
index 809029c7e..40d5e5170 100644
--- a/content/wallets/pages/signer/quickstart.mdx
+++ b/content/wallets/pages/signer/quickstart.mdx
@@ -4,6 +4,8 @@ description: Get started with the authentication provider
slug: wallets/signer/quickstart
---
+
+
Using React? Follow [this](/docs/wallets/react/quickstart) quickstart guide instead
for easy-to-use React hooks and integration with Wallet APIs.
diff --git a/content/wallets/pages/signer/solana-wallets/solana-signer-package.mdx b/content/wallets/pages/signer/solana-wallets/solana-signer-package.mdx
index 22410eeef..52ece2f3c 100644
--- a/content/wallets/pages/signer/solana-wallets/solana-signer-package.mdx
+++ b/content/wallets/pages/signer/solana-wallets/solana-signer-package.mdx
@@ -6,6 +6,8 @@ link: /signer/solana-wallets/get-started
slug: wallets/signer/solana-wallets/get-started
---
+
+
If you're not using React, use the `@account-kit/signer` package to create smart wallets on Solana, sign messages, sign transactions, and sponsor transaction fees.
If you're using React, get started with Solana wallets [here](/docs/wallets/react/solana-wallets/get-started).
diff --git a/content/wallets/pages/signer/user-sessions.mdx b/content/wallets/pages/signer/user-sessions.mdx
index 608001ebe..49d46ce06 100644
--- a/content/wallets/pages/signer/user-sessions.mdx
+++ b/content/wallets/pages/signer/user-sessions.mdx
@@ -4,6 +4,8 @@ description: Learn how to configure and leverage sessions for your users with Wa
slug: wallets/signer/user-sessions
---
+
+
By default, `AlchemyWebSigner` user sessions are cached in `localStorage` for 15 minutes.
You can customize session length by passing a [`sessionConfig`](/docs/wallets/reference/account-kit/signer/classes/AlchemyWebSigner) to your `AlchemyWebSigner` constructor.
diff --git a/content/wallets/pages/smart-contracts/modular-account-v2/getting-started.mdx b/content/wallets/pages/smart-contracts/modular-account-v2/getting-started.mdx
index 5f4a8352b..2ddff5279 100644
--- a/content/wallets/pages/smart-contracts/modular-account-v2/getting-started.mdx
+++ b/content/wallets/pages/smart-contracts/modular-account-v2/getting-started.mdx
@@ -4,7 +4,11 @@ description: Getting started with Modular Account V2 in Wallet APIs
slug: wallets/smart-contracts/modular-account-v2/getting-started
---
-Getting started with Modular Account v2 is straightforward. Below, you create a new Modular Account v2 client to send transactions. Your MAv2 smart account deploys onchain when you send the first transaction from a unique owner.
+Getting started with Modular Account V2 is straightforward. Below, you'll create a Modular Account V2 from a signer and send a transaction through viem's bundler client. Your MAv2 deploys onchain on the first user operation.
+
+
+ [`@alchemy/wallet-apis`](/docs/wallets/quickstart) already uses Modular Account V2 as its default smart account — `createSmartWalletClient` gives you MAv2 plus account creation, gas estimation, and the bundler client behind a single call. This page covers the lower-level path: instantiating MAv2 directly from `@alchemy/smart-accounts` and pairing it with viem's bundler client. Use it when you need fine-grained control over the wiring.
+
## Install packages
@@ -14,84 +18,86 @@ Getting started with Modular Account v2 is straightforward. Below, you create a
**Installation**
-First, install the `@account-kit/smart-contracts` package.
-
```bash yarn
- yarn add @account-kit/smart-contracts
- yarn add @account-kit/infra
+ yarn add @alchemy/smart-accounts @alchemy/aa-infra viem
```
```bash npm
- npm install @account-kit/smart-contracts
- npm install @account-kit/infra
+ npm install @alchemy/smart-accounts @alchemy/aa-infra viem
```
- For Modular Account V2, the address of the smart account will be calculated as a combination of [the owner and the salt](https://github.com/alchemyplatform/modular-account/blob/v2.0.x/src/factory/AccountFactory.sol#L98-L104). You will get the same smart account address each time you supply the same `owner`, the signer(s) used to create the account for the first time. You can also optionally supply `salt` if you want a different address for the same `owner` param (the default salt is `0n`).
-
- If you want to use a signer to connect to an account whose address does not map to the contract-generated address, you can supply the `accountAddress` to connect with the account of interest. In that case, the `signer` address is not used for address calculation, but only for signing the operation.
+ For Modular Account V2, the address is derived from the owner and an optional `salt` (default `0n`). Same owner + same salt → same address. Pass `accountAddress` to connect to an existing account whose contract-derived address doesn't match the signer (e.g. the original owner is no longer in your control).
-## Creating a Modular Account V2 client
+## Create a Modular Account V2
```ts twoslash modular-account-v2.ts
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { sepolia, alchemy } from "@account-kit/infra";
-import { generatePrivateKey } from "viem/accounts";
+import { createClient } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { sepolia } from "viem/chains";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
-const accountClient = await createModularAccountV2Client({
- mode: "default", // optional param to specify the MAv2 variant (either "default" or "7702")
- chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }), // Get your API key at https://dashboard.alchemy.com/apps or http("RPC_URL") for non-alchemy infra
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+const ALCHEMY_API_KEY = "your-api-key"; // https://dashboard.alchemy.com/apps
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+
+const rpcClient = createClient({ chain: sepolia, transport });
+
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
+ // mode: "7702", // optional — defaults to "default"
});
```
**Choosing which mode to use**
-Two variants of Modular Account v2 are available: `default` and `7702`.
-
-* (Recommended) `default` provides you with the cheapest, most flexible and advanced smart wallet
-* `7702` if you are looking for 7702 support, learn about how to set up and take advantage of the EIP-7702 compliant account [here](/docs/wallets/transactions/using-eip-7702)
+Two variants of Modular Account V2 are available: `default` and `7702`.
-Want to enable social login methods? Set up your [authentication provider](/docs/wallets/signer/quickstart).
+* (Recommended) `default` provides the cheapest, most flexible smart wallet.
+* `7702` if you want EIP-7702 support — see the [EIP-7702 guide](/docs/wallets/transactions/using-eip-7702).
-Alternatively, you can [bring a 3rd party authentication provider](/docs/wallets/third-party/signers/privy) as the owner of your new account.
-
-Not sure what authentication provider to use? [Learn more](/docs/wallets/signer/what-is-a-signer).
+Want to enable social login methods? [Use Privy as your signer](/docs/wallets/wallet-integrations/privy).
## Sending a transaction
-Now that you have a client, you can send a transaction. The first transaction also deploys the new Modular Account v2.
+Pair the account with viem's bundler client (and `@alchemy/aa-infra`'s fee estimator) to submit user operations.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { sepolia, alchemy } from "@account-kit/infra";
-import { generatePrivateKey } from "viem/accounts";
-import { parseEther } from "viem";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, parseEther } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { sepolia } from "viem/chains";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+const ALCHEMY_API_KEY = "your-api-key";
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+const rpcClient = createClient({ chain: sepolia, transport });
+
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
+});
-const accountClient = await createModularAccountV2Client({
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }),
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+ transport,
+ userOperation: { estimateFeesPerGas },
});
-const operation = await accountClient.sendUserOperation({
- // simple UO sending no data or value to vitalik's address
- uo: {
- target: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // The address to call in the UO
- data: "0x", // The calldata to send in the UO
- value: parseEther("0"), // The value to send in the UO
- },
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{
+ to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // vitalik
+ data: "0x",
+ value: parseEther("0"),
+ }],
});
-console.log(
- "User operation sent! \nUO hash: ",
- operation.hash,
- "\nModular Account v2 Address: ",
- operation.request.sender,
-);
+console.log("UO hash:", hash, "\nMAv2 address:", account.address);
```
diff --git a/content/wallets/pages/smart-contracts/modular-account-v2/managing-ownership.mdx b/content/wallets/pages/smart-contracts/modular-account-v2/managing-ownership.mdx
index 9f3ede070..44d75d166 100644
--- a/content/wallets/pages/smart-contracts/modular-account-v2/managing-ownership.mdx
+++ b/content/wallets/pages/smart-contracts/modular-account-v2/managing-ownership.mdx
@@ -6,36 +6,50 @@ slug: wallets/smart-contracts/modular-account-v2/managing-ownership
You can add an owner to your account, or transfer ownership of your account with Modular Account V2.
-To transfer ownership, call the `updateFallbackSignerData` function. Modular Account V2s achieve huge savings on creation because the owner address is cached in immutable bytecode on account creation. When transferring ownership, the fallback is set to the new owner address and this will be used during validation. The boolean is set to false for the account to check this value in storage instead of the immutable cached owner address.
+To transfer ownership, call the `updateFallbackSignerData` function. Modular Account V2s achieve huge savings on creation because the owner address is cached in immutable bytecode on account creation. When transferring ownership, the fallback is set to the new owner address and this will be used during validation. The boolean is set to `false` for the account to check this value in storage instead of the immutable cached owner address.
Note that `updateFallbackSignerData` is an ownership transfer operation, and the previous owner would lose access to the account. To add an owner, you should add a session key with root permissions instead.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { semiModularAccountBytecodeAbi } from "@account-kit/smart-contracts/experimental";
-import { type SmartAccountSigner, LocalAccountSigner } from "@aa-sdk/core";
-import { generatePrivateKey } from "viem/accounts";
-import { encodeFunctionData } from "viem";
-import { sepolia, alchemy } from "@account-kit/infra";
-
-const client = await createModularAccountV2Client({
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, encodeFunctionData, type Address } from "viem";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { sepolia } from "viem/chains";
+import { alchemyTransport } from "@alchemy/common";
+import {
+ toModularAccountV2,
+ semiModularAccountV2StaticImpl,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+const ALCHEMY_API_KEY = "your-api-key";
+const transport = alchemyTransport({ apiKey: ALCHEMY_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: alchemy({ apiKey: "your-api-key" }),
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+ transport,
+ userOperation: { estimateFeesPerGas },
+});
+
+const newOwner: Address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
+
+// `isFallbackSignerDisabled = false` keeps the fallback storage slot active for validation.
+const data = encodeFunctionData({
+ abi: semiModularAccountV2StaticImpl.accountAbi,
+ functionName: "updateFallbackSignerData",
+ args: [newOwner, false],
});
-const newOwner = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
-
-// The boolean parameter in updateFallbackSignerData is `isFallbackSignerDisabled`, and false indicates that we are using the value of the fallback signer
-await client.sendUserOperation({
- uo: {
- target: client.account.address,
- value: 0n,
- data: encodeFunctionData({
- abi: semiModularAccountBytecodeAbi,
- functionName: "updateFallbackSignerData",
- args: [newOwner, false],
- }),
- },
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: account.address, data }],
});
+await bundlerClient.waitForUserOperationReceipt({ hash });
```
diff --git a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/adding-session-keys.mdx b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/adding-session-keys.mdx
index a4eb5742f..23fd74761 100644
--- a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/adding-session-keys.mdx
+++ b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/adding-session-keys.mdx
@@ -4,49 +4,55 @@ description: Adding session keys to your Modular Account V2
slug: wallets/smart-contracts/modular-account-v2/session-keys/adding-session-keys
---
-To add a session key, 1) decide what permissions you want to grant the new key and 2) call the [`installValidation`](#install-validation-method) method on the account. This method sends a single transaction that adds the session key with scoped permission to your smart account onchain. You can then use that session key to sign transactions for your account within the defined permissions.
+To add a session key: 1) decide what permissions you want to grant the new key, and 2) call the [`installValidation`](#install-validation-parameters) function on the account. In v5 you do this by extending the bundler client with `installValidationActions`, encoding the install call via `encodeInstallValidation`, then sending it as a user operation. The bundler client returns a calldata hex; `sendUserOperation` wraps it in an `execute` call from the account.
-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).
+Scoped permissions are layered on as **hooks** that you pass into `installValidation`. Hooks can be combined — e.g. a session key that can only spend 10 USDC within the next 24 hours from the account.
-## Adding a global session key (i.e. additional owner)
+## Adding a global session key (i.e. an additional owner)
-This example shows:
+This example shows two variants:
-1. 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.
-2. 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.
+1. A **global** session key with full control of the account. Functionally, this is how you add another owner.
+2. A session key restricted to calling `execute` / `executeBatch` only — full control of assets but unable to change the account implementation.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, toFunctionSelector, getAbiItem, type Hex } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey, mnemonicToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
import {
+ toModularAccountV2,
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);
+ DefaultModuleAddress,
+ semiModularAccountV2StaticImpl,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-let sessionKeyEntityId = 1;
-const ecdsaValidationModuleAddress =
- getDefaultSingleSignerValidationModuleAddress(client.chain);
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
+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);
-// 1. Adding a session key with full permissions
-await client.installValidation({
+const sessionKeySigner = mnemonicToAccount("SESSION_KEY_MNEMONIC");
+
+// 1. Global session key (full permissions)
+let sessionKeyEntityId = 1;
+const installGlobalCallData: Hex = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
@@ -55,30 +61,24 @@ await client.installValidation({
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [],
});
+await bundlerClient.sendUserOperation({ callData: installGlobalCallData });
-// 2. Adding a session key that can only call `execute` or `executeBatch` on the account
+// 2. Session key restricted to execute / executeBatch
sessionKeyEntityId = 2;
const executeSelector = toFunctionSelector(
- getAbiItem({
- abi: semiModularAccountBytecodeAbi,
- name: "execute",
- }),
+ getAbiItem({ abi: semiModularAccountV2StaticImpl.accountAbi, name: "execute" }),
);
-
const executeBatchSelector = toFunctionSelector(
- getAbiItem({
- abi: semiModularAccountBytecodeAbi,
- name: "executeBatch",
- }),
+ getAbiItem({ abi: semiModularAccountV2StaticImpl.accountAbi, name: "executeBatch" }),
);
-await client.installValidation({
+const installExecuteOnlyCallData: Hex = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: false,
isSignatureValidation: false,
@@ -87,61 +87,68 @@ await client.installValidation({
selectors: [executeSelector, executeBatchSelector],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [],
});
+await bundlerClient.sendUserOperation({ callData: installExecuteOnlyCallData });
```
## 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 your account for the next day). The Time Range Module is used to enforce time-based validation for transactions. This example shows how to add a session key that starts in a day and expires in two days.
+The Time Range Module enforces time-based validation. The example below adds a session key that's valid for the next two days.
-Additional Notes
+Notes:
-* the interval is inclusive i.e. `[beginningOfInterval, endOfInterval]`
-* the values `beginningOfInterval` and `endOfInterval` 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
+* the interval is inclusive: `[validAfter, validUntil]`
+* `validAfter` / `validUntil` are unix timestamps with a maximum size of uint32
+* `validUntil` must be **strictly greater than** `validAfter`
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey, mnemonicToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
import {
- HookType,
+ toModularAccountV2,
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);
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-let sessionKeyEntityId = 1;
-const ecdsaValidationModuleAddress =
- getDefaultSingleSignerValidationModuleAddress(client.chain);
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
+// Inline until `HookType` is value-exported from `@alchemy/smart-accounts`.
+const HookType = { EXECUTION: "0x00", VALIDATION: "0x01" } as const;
-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
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
+
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
+});
-// Adding a session key that starts in a day and expires in two days
-await client.installValidation({
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
+}).extend(installValidationActions);
+
+const sessionKeySigner = mnemonicToAccount("SESSION_KEY_MNEMONIC");
+const sessionKeyEntityId = 1;
+const hookEntityId = 0; // must not collide with an existing hook entity id on the module
+const validAfter = 0; // valid immediately
+const validUntil = validAfter + 2 * 86400; // expires in 2 days
+
+const callData = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
@@ -150,16 +157,16 @@ await client.installValidation({
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [
{
hookConfig: {
- address: getDefaultTimeRangeModuleAddress(client.chain),
+ address: DefaultModuleAddress.TIME_RANGE,
entityId: hookEntityId,
- hookType: HookType.VALIDATION, // fixed value
- hasPreHooks: true, // fixed value
- hasPostHooks: false, // fixed value
+ hookType: HookType.VALIDATION, // fixed
+ hasPreHooks: true, // fixed
+ hasPostHooks: false, // fixed
},
initData: TimeRangeModule.encodeOnInstallData({
entityId: hookEntityId,
@@ -169,55 +176,60 @@ await client.installValidation({
},
],
});
+await bundlerClient.sendUserOperation({ callData });
```
### Paymaster guard
-#### Purpose
+Restricts a session key to using a single paymaster contract.
-This module provides the ability to limit a session key to only be able to use a specific single paymaster.
+Notes:
-#### Additional Notes
-
-* you MUST specify a paymaster contract when using this module
-* if the paymaster contract that is registered with this module decides to no longer sponsor your transactions, the entity associated with this hook would no longer be able to send transactions.
+* you **must** specify a paymaster contract when using this module
+* if the registered paymaster stops sponsoring, the session key can no longer send transactions
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey, mnemonicToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
import {
- HookType,
+ toModularAccountV2,
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);
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-let sessionKeyEntityId = 1;
-const ecdsaValidationModuleAddress =
- getDefaultSingleSignerValidationModuleAddress(client.chain);
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
+// Inline until `HookType` is value-exported from `@alchemy/smart-accounts`.
+const HookType = { EXECUTION: "0x00", VALIDATION: "0x01" } as const;
-const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
+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 sessionKeySigner = mnemonicToAccount("SESSION_KEY_MNEMONIC");
+const sessionKeyEntityId = 1;
+const hookEntityId = 0;
const paymasterAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
-// Adding a session key that can only use the above paymaster for user operations
-await client.installValidation({
+const callData = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
@@ -226,16 +238,16 @@ await client.installValidation({
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [
{
hookConfig: {
- address: getDefaultPaymasterGuardModuleAddress(client.chain),
+ address: DefaultModuleAddress.PAYMASTER_GUARD,
entityId: hookEntityId,
- hookType: HookType.VALIDATION, // fixed value
- hasPreHooks: true, // fixed value
- hasPostHooks: false, // fixed value
+ hookType: HookType.VALIDATION,
+ hasPreHooks: true,
+ hasPostHooks: false,
},
initData: PaymasterGuardModule.encodeOnInstallData({
entityId: hookEntityId,
@@ -244,79 +256,69 @@ await client.installValidation({
},
],
});
+await bundlerClient.sendUserOperation({ callData });
```
### Native token and/or gas limit
-This module provides native token spending limits for modular accounts. The example below shows how to add a session key with 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)
-* 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
+Caps native-token spend (and optionally gas costs) for the session key. The example below installs a 1 ETH spend limit.
-#### Gas Cost Tracking
+What this module covers:
-For UserOperations, the module tracks:
+* Tracks native-token spending across `execute`, `executeBatch`, `performCreate`, and UserOperation gas costs.
+* Paymaster handling:
+ * Standard paymasters — gas costs **don't** count against the limit.
+ * "Special" paymasters — gas costs **do** count against the limit.
+* Limits are per entity ID.
-* Pre-verification gas
-* Verification gas
-* Call gas
-* Paymaster verification gas (for special paymasters)
-* Paymaster post-op gas (for special paymasters)
+UserOperation gas tracked: pre-verification gas, verification gas, call gas, and (for special paymasters) paymaster verification + post-op gas.
-#### Additional Notes
+Install 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
+* The module must be installed with both **validation** and **execution** hooks — the validation hook tracks gas, the execution hook tracks value.
+* The module maintains global singleton state across all accounts.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, parseEther } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey, mnemonicToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
import {
- HookType,
+ toModularAccountV2,
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);
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-let sessionKeyEntityId = 1;
-const ecdsaValidationModuleAddress =
- getDefaultSingleSignerValidationModuleAddress(client.chain);
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
+// Inline until `HookType` is value-exported from `@alchemy/smart-accounts`.
+const HookType = { EXECUTION: "0x00", VALIDATION: "0x01" } as const;
+
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
+
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
+});
-const hookEntityId = 0; // Make sure that the account does not have a hook with this entity id on the module yet
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
+}).extend(installValidationActions);
-// Adding a session key that has a 1 eth native token spend limit
-await client.installValidation({
+const sessionKeySigner = mnemonicToAccount("SESSION_KEY_MNEMONIC");
+const sessionKeyEntityId = 1;
+const hookEntityId = 0;
+
+const callData = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
@@ -325,16 +327,16 @@ await client.installValidation({
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [
{
hookConfig: {
- address: getDefaultNativeTokenLimitModuleAddress(client.chain),
+ address: DefaultModuleAddress.NATIVE_TOKEN_LIMIT,
entityId: hookEntityId,
- hookType: HookType.VALIDATION, // fixed value
- hasPreHooks: true, // fixed value
- hasPostHooks: false, // fixed value
+ hookType: HookType.VALIDATION,
+ hasPreHooks: true,
+ hasPostHooks: false,
},
initData: NativeTokenLimitModule.encodeOnInstallData({
entityId: hookEntityId,
@@ -343,103 +345,96 @@ await client.installValidation({
},
{
hookConfig: {
- address: getDefaultNativeTokenLimitModuleAddress(client.chain),
+ address: DefaultModuleAddress.NATIVE_TOKEN_LIMIT,
entityId: hookEntityId,
- hookType: HookType.EXECUTION, // fixed value
- hasPreHooks: true, // fixed value
- hasPostHooks: false, // fixed value
+ hookType: HookType.EXECUTION,
+ hasPreHooks: true,
+ hasPostHooks: false,
},
- initData: "0x", // no initdata required as the limit was set up in the above installation call
+ initData: "0x", // no init data — the limit was set above on the validation hook
},
],
});
+await bundlerClient.sendUserOperation({ callData });
```
-### Allowlist or an ERC20 token limit
+### Allowlist or ERC-20 token limit
-This module provides two key security features for modular accounts:
+The allowlist module provides two features:
-* **Allowlisting** - Controls which addresses and functions can be called
-* **ERC-20 Spend Limits** - Manages spending limits for ERC-20 tokens
+* **Address/function allowlisting** — controls which targets and selectors a session key can call.
+* **ERC-20 spend limits** — caps ERC-20 spending per token.
-#### Allowlist Features
+Allowlist details:
-* 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
+* Permissions are matched per `(address, selector)`. Wildcards are supported on either side.
+* Only applies to `execute` and `executeBatch`.
+* Check order: wildcard address → wildcard function → exact address+function match → revert.
-#### ERC-20 Spend Limit Features
+ERC-20 spend-limit details:
-* Only allows transfer and approve functions for tracked tokens
-* Works with standard execution functions:
- * execute
- * executeWithRuntimeValidation
- * executeUserOp
- * executeBatch
+* Only `transfer` and `approve` are allowed for tracked tokens. (The module is intentionally restrictive to avoid edge cases like DAI's non-standard methods.)
+* Works with `execute`, `executeBatch`, `executeWithRuntimeValidation`, and `executeUserOp`.
-#### Additional Notes
+Other 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)
+* The module is installed/uninstalled per entity ID — uninstalling one entity doesn't affect others.
+* Settings are stored in a global singleton contract and can be updated dynamically.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, parseEther } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey, mnemonicToAccount } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
import {
- HookType,
+ toModularAccountV2,
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);
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-let sessionKeyEntityId = 1;
-const ecdsaValidationModuleAddress =
- getDefaultSingleSignerValidationModuleAddress(client.chain);
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.mnemonicToAccountSigner("SESSION_KEY_MNEMONIC");
+// Inline until `HookType` is value-exported from `@alchemy/smart-accounts`.
+const HookType = { EXECUTION: "0x00", VALIDATION: "0x01" } as const;
+
+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 sessionKeySigner = mnemonicToAccount("SESSION_KEY_MNEMONIC");
+const sessionKeyEntityId = 1;
+const hookEntityId = 0;
-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
+ hasSelectorAllowlist: false, // whether to limit callable functions on this target
+ hasERC20SpendLimit: false, // whether `target` is an ERC-20 with a spend cap
+ erc20SpendLimit: parseEther("100"), // spend cap if applicable
+ selectors: [], // function selectors to allow if applicable
},
],
});
-// Adding a session key that has a 100 ERC token spend limit
-await client.installValidation({
+const callData = await bundlerClient.encodeInstallValidation({
validationConfig: {
- moduleAddress: ecdsaValidationModuleAddress,
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
isGlobal: true,
isSignatureValidation: true,
@@ -448,45 +443,46 @@ await client.installValidation({
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: sessionKeyEntityId,
- signer: await sessionKeySigner.getAddress(), // Address of the session key
+ signer: sessionKeySigner.address,
}),
hooks: [
{
hookConfig: {
- address: getDefaultAllowlistModuleAddress(client.chain),
+ address: DefaultModuleAddress.ALLOWLIST,
entityId: hookEntityId,
- hookType: HookType.VALIDATION, // fixed value
- hasPreHooks: true, // fixed value
- hasPostHooks: false, // fixed value
+ hookType: HookType.VALIDATION,
+ hasPreHooks: true,
+ hasPostHooks: false,
},
initData: allowlistInstallData,
},
],
});
+await bundlerClient.sendUserOperation({ callData });
```
-## Install validation method
+## Install validation parameters
-Use this method to add session keys to your account, with the following configurable parameters.
+Configurable parameters for `encodeInstallValidation`:
-`validationConfig`: The validation configuration for the session key, containing the following fields:
+`validationConfig`:
-* `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 the `selectors` array. It's recommended to leave this as `false` 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 as `false` 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 to `true`.
+* `moduleAddress` — address of the validation module. `SingleSignerValidationModule` provides ECDSA validation; `WebauthnModule` provides WebAuthn. For custom validation (multisig, etc.), use that module's address.
+* `entityId` — uint32 identifier for the validation. Must not collide with an existing entity on the account. The owner is entity `0`, so start session keys at `1`.
+* `isGlobal` — if `true`, the validation can call any function on the account. **Recommended `false`** plus a `selectors` array. A global key has authority to upgrade the account implementation, which can change ownership in the same transaction.
+* `isSignatureValidation` — if `true`, the key can produce ERC-1271 signatures (used for things like Permit2). **Recommended `false`** unless you need it.
+* `isUserOpValidation` — if `true`, the key can sign user operations. For most use cases this should be `true`.
-`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.
+`selectors` — function selectors the key may call. Ignored if `isGlobal` is `true`. If `isGlobal` is `false`, the key can **only** call selectors listed here. Recommended: `ModularAccount.execute.selector` and `ModularAccount.executeBatch.selector`.
-`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.
+`installData` — module-specific install data. Each module ships an `encodeOnInstallData` helper.
-`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.
+`hooks` — array of `{ hookConfig, initData }` to install alongside the validation.
-`hookConfig`: This is the hook configuration to be applied to the session key.
+`hookConfig`:
-* `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.
+* `address` — hook module address.
+* `entityId` — hook-module entity id (distinct from the validation entity id). Decoupled so multiple hooks from the same module can apply to one key. Must not collide with an existing hook entity on the account.
+* `hookType` — `HookType.VALIDATION` (runs in validation phase) or `HookType.EXECUTION` (runs pre- and/or post-execution).
+* `hasPreHooks` — runs before validation/execution.
+* `hasPostHooks` — runs after validation/execution.
diff --git a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/removing-session-keys.mdx b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/removing-session-keys.mdx
index b733663d8..c6450c4b0 100644
--- a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/removing-session-keys.mdx
+++ b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/removing-session-keys.mdx
@@ -4,92 +4,91 @@ description: Removing session keys from your Modular Account V2
slug: wallets/smart-contracts/modular-account-v2/session-keys/removing-session-keys
---
-Removing session keys is done with the `uninstallValidation` method.
+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.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+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,
- getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
- modularAccountAbi,
-} from "@account-kit/smart-contracts/experimental";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { sepolia, alchemy } from "@account-kit/infra";
-import { generatePrivateKey } from "viem/accounts";
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-const client = (
- await createModularAccountV2Client({
- chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }),
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
- })
-).extend(installValidationActions);
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
-let sessionKeyEntityId = 1;
-
-// Removing a basic session key
-await client.uninstallValidation({
- moduleAddress: getDefaultSingleSignerValidationModuleAddress(client.chain),
- entityId: sessionKeyEntityId,
- uninstallData: SingleSignerValidationModule.encodeOnUninstallData({
- entityId: sessionKeyEntityId,
- }),
- hookUninstallDatas: [],
+const account = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
});
-// Removing a session key with hooks
-await client.uninstallValidation({
- moduleAddress: getDefaultSingleSignerValidationModuleAddress(client.chain),
+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 there are hooks on the validation, you have to provide the hook uninstallation data to uninstall them too. Each module provides an `encodeOnUninstallData` helper function to generate the uninstallation hook for that module.
+If the validation has hooks installed, you have to provide uninstall data for each one. Every module exports an `encodeOnUninstallData` helper.
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
+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,
- getDefaultSingleSignerValidationModuleAddress,
SingleSignerValidationModule,
- modularAccountAbi,
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";
+ DefaultModuleAddress,
+} from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-const client = (
- await createModularAccountV2Client({
- chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }),
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
- })
-).extend(installValidationActions);
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
-let sessionKeyEntityId = 1;
-
-// Removing a basic session key
-await client.uninstallValidation({
- moduleAddress: getDefaultSingleSignerValidationModuleAddress(client.chain),
- entityId: sessionKeyEntityId,
- uninstallData: SingleSignerValidationModule.encodeOnUninstallData({
- entityId: sessionKeyEntityId,
- }),
- hookUninstallDatas: [],
+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
-await client.uninstallValidation({
- moduleAddress: getDefaultSingleSignerValidationModuleAddress(client.chain),
+const callData = await bundlerClient.encodeUninstallValidation({
+ moduleAddress: DefaultModuleAddress.SINGLE_SIGNER_VALIDATION,
entityId: sessionKeyEntityId,
uninstallData: SingleSignerValidationModule.encodeOnUninstallData({
entityId: sessionKeyEntityId,
@@ -109,4 +108,5 @@ await client.uninstallValidation({
}),
],
});
+await bundlerClient.sendUserOperation({ callData });
```
diff --git a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/using-session-keys.mdx b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/using-session-keys.mdx
index f749bf36a..b14691d63 100644
--- a/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/using-session-keys.mdx
+++ b/content/wallets/pages/smart-contracts/modular-account-v2/session-keys/using-session-keys.mdx
@@ -4,45 +4,60 @@ description: Using session keys with your Modular Account V2
slug: wallets/smart-contracts/modular-account-v2/session-keys/using-session-keys
---
-Once session keys are added, using them is straightforward -- create another client instance with the session key connected along with properties of the session key (session key entityId and global validation). These properties were set during the installValidation call.
+Once a session key is installed, you can use it by creating a second `ModularAccountV2` whose **owner** is the session key signer and whose `accountAddress` points to the original account. Pass the session key's `entityId` and global-validation flag via `signerEntity` — these were set when you called [`encodeInstallValidation`](/docs/wallets/smart-contracts/modular-account-v2/session-keys/adding-session-keys).
```ts twoslash
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { generatePrivateKey } from "viem/accounts";
-import { type SmartAccountSigner } from "@aa-sdk/core";
-import { parseEther } from "viem";
-import { sepolia, alchemy } from "@account-kit/infra";
-
-const client = await createModularAccountV2Client({
- chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }),
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, parseEther } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
+
+// Owner-side account (already created and used to install the session key).
+const ownerAccount = await toModularAccountV2({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
});
-let sessionKeyEntityId = 1;
-const sessionKeySigner: SmartAccountSigner =
- LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey());
+// Session-key signer + the validation entity id you installed it under.
+const sessionKeySigner = privateKeyToAccount(generatePrivateKey());
+const sessionKeyEntityId = 1;
-const sessionKeyClient = await createModularAccountV2Client({
- chain: sepolia,
- transport: alchemy({ apiKey: "your-api-key" }),
- signer: sessionKeySigner,
- accountAddress: client.getAddress(client.account),
- initCode: await client.account.getInitCode(),
+// Reconnect to the SAME account address, but with the session-key signer as owner
+// and the session key's signerEntity. If the account isn't deployed yet, also pass
+// factoryArgs from `ownerAccount.getFactoryArgs()` so the UO can deploy it.
+const sessionKeyAccount = await toModularAccountV2({
+ client: rpcClient,
+ owner: sessionKeySigner,
+ accountAddress: ownerAccount.address,
signerEntity: {
entityId: sessionKeyEntityId,
isGlobalValidation: true,
},
});
+const sessionKeyClient = createBundlerClient({
+ account: sessionKeyAccount,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
+});
+
await sessionKeyClient.sendUserOperation({
- uo: {
- target: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // The address to call in the UO
- data: "0x", // The calldata to send in the UO
- value: parseEther("1"), // The value to send in the UO
- },
+ calls: [
+ {
+ to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
+ data: "0x",
+ value: parseEther("1"),
+ },
+ ],
});
```
-Note that you have to pass in `accountAddress` and `initCode` to session key clients. By default, the client uses an account address counterfactual that assumes that the connected signer is the owner.
+You must pass `accountAddress` — without it, `toModularAccountV2` would derive a counterfactual address from the session-key signer instead of pointing at the original account.
diff --git a/content/wallets/pages/smart-contracts/other-accounts/light-account/getting-started.mdx b/content/wallets/pages/smart-contracts/other-accounts/light-account/getting-started.mdx
index 70268b5bf..614ab9b3f 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/light-account/getting-started.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/light-account/getting-started.mdx
@@ -4,7 +4,11 @@ description: Getting started with Light Account in Wallet APIs
slug: wallets/smart-contracts/other-accounts/light-account/getting-started
---
-Getting started with Light Account is straightforward. Below you will create and send transactions for both `LightAccount` and `MultiOwnerLightAccount` using `@alchemy/aa-alchemy`.
+Getting started with Light Account is straightforward. Below you'll create a `LightAccount` (and the multi-owner variant) from `@alchemy/smart-accounts` and use them with viem's bundler client.
+
+
+ `@alchemy/wallet-apis` defaults to Modular Account V2, but it can provision a Light Account too — pass `creationHint: { accountType: "la-v2" }` to `requestAccount`. `createSmartWalletClient` then bundles account creation, gas estimation, and the bundler client behind a single call. See the [Wallet APIs quickstart](/docs/wallets/quickstart). This page covers the lower-level path: instantiating `LightAccount` directly from `@alchemy/smart-accounts` and pairing it with viem's bundler client. Use it when you need fine-grained control over the wiring.
+
### Install packages
@@ -16,50 +20,83 @@ Getting started with Light Account is straightforward. Below you will create and
```bash npm
- npm i @account-kit/smart-contracts
+ npm i @alchemy/smart-accounts @alchemy/aa-infra viem
```
```bash yarn
- yarn add@account-kit/smart-contracts
+ yarn add @alchemy/smart-accounts @alchemy/aa-infra viem
```
-### Create a client and send a transaction
-
-The code snippets below demonstrate how to use `LightAccount` and `MultiOwnerLightAccount` with Wallet APIs. They create the account and send a `UserOperation` from it.
+### Create an account and send a transaction
```ts light-account.ts
- import { createLightAccountAlchemyClient } from "@account-kit/smart-contracts";
- import { sepolia, alchemy } from "@account-kit/infra";
- import { LocalAccountSigner } from "@aa-sdk/core";
- import { generatePrivateKey } from "viem";
+ import { createBundlerClient } from "viem/account-abstraction";
+ import { createClient } from "viem";
+ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+ import { sepolia } from "viem/chains";
+ import { alchemyTransport } from "@alchemy/common";
+ import { toLightAccount } from "@alchemy/smart-accounts";
+ import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+ const ALCHEMY_API_KEY = "your-api-key";
+ const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+ const rpcClient = createClient({ chain: sepolia, transport });
+
+ const account = await toLightAccount({
+ client: rpcClient,
+ owner: privateKeyToAccount(generatePrivateKey()),
+ version: "v2.0.0",
+ });
- const lightAccountClient = await createLightAccountAlchemyClient({
- transport: alchemy({ apiKey: "your-api-key" })
+ const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
chain: sepolia,
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+ transport,
+ userOperation: { estimateFeesPerGas },
+ });
+
+ const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
});
```
```ts multi-owner-light-account.ts
- import { createMultiOwnerLightAccountAlchemyClient } from "@account-kit/smart-contracts";
- import { sepolia, alchemy } from "@account-kit/infra";
- import { LocalAccountSigner } from "@aa-sdk/core";
- import { generatePrivateKey } from "viem";
+ import { createBundlerClient } from "viem/account-abstraction";
+ import { createClient } from "viem";
+ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+ import { sepolia } from "viem/chains";
+ import { alchemyTransport } from "@alchemy/common";
+ import { toMultiOwnerLightAccount } from "@alchemy/smart-accounts";
+ import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+ const ALCHEMY_API_KEY = "your-api-key";
+ const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+ const rpcClient = createClient({ chain: sepolia, transport });
+
+ const account = await toMultiOwnerLightAccount({
+ client: rpcClient,
+ owners: [privateKeyToAccount(generatePrivateKey())],
+ });
- const lightAccountClient = await createMultiOwnerLightAccountAlchemyClient({
- transport: alchemy({ apiKey: "your-api-key" })
+ const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
chain: sepolia,
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
+ transport,
+ userOperation: { estimateFeesPerGas },
+ });
+
+ const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
});
```
- For `LightAccount`, the address of the smart account will be calculated as a combination of the version, [the owner, and the salt](https://github.com/alchemyplatform/light-account/blob/v2.0.0/src/LightAccountFactory.sol#L24-L33). You will get the same smart account address each time you supply the same `version` and `owner`. Alternatively, you can supply `salt` if you want a different address for the same `version` and `owner` params (the default salt is `0n`). For `MultiOwnerLightAccount`, the same pattern follows, except that it takes an array of owner addresses instead of a single owner address.
-
- If you want to use a signer to connect to an account whose address does not map to the contract-generated address, you can supply the `accountAddress` to connect with the account of interest. In that case, the `signer` address is not used for address calculation, but only used for signing the operation.
+ For `LightAccount`, the address is derived from the `version`, owner, and an optional `salt` (default `0n`). Same inputs → same address. `MultiOwnerLightAccount` follows the same rule but takes an array of owners and is locked to `v2.0.0` (no `version` param). Pass `accountAddress` to connect to an existing account whose contract-derived address doesn't match the signer.
- Reference: https://eips.ethereum.org/EIPS/eip-4337#first-time-account-creation
+ Reference: [EIP-4337 — first-time account creation](https://eips.ethereum.org/EIPS/eip-4337#first-time-account-creation).
diff --git a/content/wallets/pages/smart-contracts/other-accounts/light-account/multi-owner-light-account.mdx b/content/wallets/pages/smart-contracts/other-accounts/light-account/multi-owner-light-account.mdx
index e7ec5c485..cbf01f3d0 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/light-account/multi-owner-light-account.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/light-account/multi-owner-light-account.mdx
@@ -1,61 +1,77 @@
---
title: How to manage ownership of a Multi-Owner Light Account
-description: Follow this guide to manage ownership of a Multi-Owner Light
+description: Manage owners on a Multi-Owner Light Account
slug: wallets/smart-contracts/other-accounts/light-account/multi-owner-light-account
---
-A `MultiOwnerLightAccount` has one or more ECDSA or SCA owners. This lets your account integrate with multiple owners at once, and supports recovering your account if one owner is lost.
+A `MultiOwnerLightAccount` has one or more ECDSA or smart-contract owners. This lets your account integrate with multiple owners at once, and supports recovering the account if one owner is lost.
-The `MultiOwnerLightAccount` is able to:
+A `MultiOwnerLightAccount` (returned by `toMultiOwnerLightAccount` from `@alchemy/smart-accounts`) lets you:
-* Update (add or remove) owners for an account.
-* Show all owners of an account.
-* Validate signed signatures of ERC-4337 enabled transactions as well as regular transactions.
+* Add or remove owners.
+* Read the current set of owners.
+* Validate ERC-4337 user operations and 1271 signatures from any owner.
-When you connect your `MultiOwnerLightAccount` to `SmartAccountClient` you can extend the client with `multiOwnerLightAccountClientActions`, which exposes a set of methods available to call the `MultiOwnerLightAccount` with the client connected to the account.
+### 1. Read all current owners
-
- When using `createMultiOwnerLightAccountAlchemyClient` in
- `@account-kit/smart-contracts`, the `SmartAccountClient` comes automatically
- extended with `multiOwnerLightAccountClientActions` as defaults available for
- use.
-
+Call `getOwnerAddresses()` directly on the account.
-### 1. Get all current owners of a `MultiOwnerLightAccount`
+```ts example.ts
+import { createClient } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
+import { toMultiOwnerLightAccount } from "@alchemy/smart-accounts";
-You can use the `getOwnerAddresses` method on the `MultiOwnerLightAccount` object, which can be accessed from a connected client.
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
-
- ```ts example.ts
- import { multiOwnerLightAccountClient } from "./client";
+const account = await toMultiOwnerLightAccount({
+ client: rpcClient,
+ owners: [privateKeyToAccount(generatePrivateKey())],
+});
- const owners = await multiOwnerLightAccountClient.account.getOwnerAddresses();
- ```
+const owners = await account.getOwnerAddresses();
+```
-
-
+### 2. Add or remove owners
-### 2. Add or remove owners for a `MultiOwnerLightAccount`
+Encode an `updateOwners` call with `encodeUpdateOwners`, then send it from the account through viem's bundler client.
-You can use the `updateOwners` method on the `multiOwnerLightAccountClientActions` extended smart account client to add or remove owners from the `MultiOwnerLightAccount`.
+```ts example.ts
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient, type Address } from "viem";
+import { sepolia } from "viem/chains";
+import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
+import { toMultiOwnerLightAccount } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
-
- ```ts example.ts
- import { multiOwnerLightAccountClient } from "./client";
+const ALCHEMY_API_KEY = "your-api-key";
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+const rpcClient = createClient({ chain: sepolia, transport });
- const ownersToAdd = []; // the addresses of owners to be added
- const ownersToRemove = []; // the addresses of owners to be removed
+const account = await toMultiOwnerLightAccount({
+ client: rpcClient,
+ owners: [privateKeyToAccount(generatePrivateKey())],
+});
- const opHash = await multiOwnerLightAccountClient.updateOwners({
- ownersToAdd,
- ownersToRemove,
- });
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
+});
- const txHash =
- await multiOwnerLightAccountClient.waitForUserOperationTransaction({
- hash: opHash,
- });
- ```
+const ownersToAdd: Address[] = []; // addresses to add
+const ownersToRemove: Address[] = []; // addresses to remove
-
-
+const updateData = account.encodeUpdateOwners(ownersToAdd, ownersToRemove);
+
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: account.address, data: updateData }],
+});
+
+const receipt = await bundlerClient.waitForUserOperationReceipt({ hash });
+```
diff --git a/content/wallets/pages/smart-contracts/other-accounts/light-account/transfer-ownership-light-account.mdx b/content/wallets/pages/smart-contracts/other-accounts/light-account/transfer-ownership-light-account.mdx
index d91e4ce3f..2daa5d55f 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/light-account/transfer-ownership-light-account.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/light-account/transfer-ownership-light-account.mdx
@@ -1,79 +1,81 @@
---
title: How to transfer ownership of a Light Account
-description: Follow this guide to transfer ownership of a Light Account with
+description: Transfer ownership of a LightAccount via Wallet APIs
slug: wallets/smart-contracts/other-accounts/light-account/transfer-ownership-light-account
---
-Not all smart account implementations support transferring ownership (e.g. `SimpleAccount`). However, a number of the accounts in this guide and in Wallet APIs do, including `LightAccount`. Below are a few different ways to transfer ownership of an account (using `LightAccount` as an example).
+Not all smart account implementations support transferring ownership (e.g. `SimpleAccount`), but `LightAccount` does. Below shows how to do it using `@alchemy/smart-accounts`.
## Usage
-`LightAccount` exposes the following method which allows the existing owner to transfer ownership to a new owner address:
+`LightAccount` exposes the following onchain method which allows the current owner to transfer ownership to a new owner address:
```solidity
function transferOwnership(address newOwner) public virtual onlyOwner
```
-There are a number of ways to call this method using Wallet APIs.
+### Encode and send the call
-### 1. Using `transferOwnership` client action
-
-
+`toLightAccount` returns an `encodeTransferOwnership` helper. Encode the calldata for the target new owner, send it as a user operation from the account, and (optionally) re-connect a fresh account using the new owner so you can keep sending UOs after the transfer.
```ts example.ts
-import { lightAccountClient } from "./client";
-import { createLightAccountClient } from "@account-kit/smart-contracts";
-
-// this will return the signer of the smart account you want to transfer ownerhip to
-const newOwner = LocalAccountSigner.mnemonicToAccountSigner(NEW_OWNER_MNEMONIC);
-const accountAddress = lightAccountClient.getAddress();
-
-// [!code focus:99]
-const hash = lightAccountClient.transferOwnership({
- newOwner,
- waitForTxn: true,
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient } from "viem";
+import { sepolia } from "viem/chains";
+import { mnemonicToAccount, privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+import { alchemyTransport } from "@alchemy/common";
+import { toLightAccount } from "@alchemy/smart-accounts";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+
+const ALCHEMY_API_KEY = "your-api-key";
+const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
+const rpcClient = createClient({ chain: sepolia, transport });
+
+// Original owner + account.
+const originalOwner = privateKeyToAccount(generatePrivateKey());
+const account = await toLightAccount({
+ client: rpcClient,
+ owner: originalOwner,
+ version: "v2.0.0",
});
-// after transaction is mined on the network,
-// create a new light account client for the transferred Light Account
-const transferredClient = await createLightAccountClient({
- transport: custom(smartAccountClient),
- chain: smartAccountClient.chain,
- signer: newOwner,
- accountAddress, // NOTE: you MUST specify the original smart account address to connect using the new owner/signer
- version: "v2.0.0", // NOTE: if the version of the light account is not v2.0.0, it must be specified here
+const bundlerClient = createBundlerClient({
+ account,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
});
-```
-
-
-
-
-
-Since `@alchemy/aa-accounts` exports a `LightAccount` ABI, the above approach makes it easy to transfer ownership. That said, you can also directly call `sendUserOperation` to execute the ownership transfer. As you will see below, however, it is a bit verbose:
-### 2. Using `sendUserOperation`
+// 1) Pick a new owner.
+const newOwner = mnemonicToAccount(process.env.NEW_OWNER_MNEMONIC!);
-
-
-```ts example.ts
-import { encodeFunctionData } from "viem";
-import { lightAccountClient } from "./client";
-
-// this will return the address of the smart account you want to transfer ownerhip of
-const accountAddress = lightAccountClient.getAddress();
-const newOwner = "0x..."; // the address of the new owner
+// 2) Encode + send the transfer.
+const data = account.encodeTransferOwnership(newOwner.address);
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: account.address, data }],
+});
+await bundlerClient.waitForUserOperationReceipt({ hash });
+
+// 3) After the transfer mines, reconnect to the same account address with the new owner.
+// NOTE: you MUST pass the original accountAddress — otherwise the new owner derives
+// a different counterfactual address.
+const transferredAccount = await toLightAccount({
+ client: rpcClient,
+ owner: newOwner,
+ version: "v2.0.0",
+ accountAddress: account.address,
+});
-// [!code focus:99]
-const result = await lightAccountClient.sendUserOperation({
- to: accountAddress,
- data: lightAccountClient.encodeTransferOwnership(newOwner),
+const transferredClient = createBundlerClient({
+ account: transferredAccount,
+ client: rpcClient,
+ chain: sepolia,
+ transport,
+ userOperation: { estimateFeesPerGas },
});
-// wait for txn with UO to be mined
-await lightAccountClient.waitForUserOperationTransaction(result);
```
-
-
-
+You can also read the current onchain owner via `account.getOwnerAddress()` to verify the transfer landed.
-See the [`LightAccount`](/docs/wallets/smart-contracts/other-accounts/light-account/) docs for more details about the `LightAccount` implementation.
+See the [Light Account overview](/docs/wallets/smart-contracts/other-accounts/light-account/) for more details on the contract.
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/getting-started.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/getting-started.mdx
index 1e25c30c4..7e5de6a01 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/getting-started.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/getting-started.mdx
@@ -4,6 +4,8 @@ description: Getting started with Modular Account in Wallet APIs
slug: wallets/smart-contracts/other-accounts/modular-account/getting-started
---
+
+
Getting started with Modular Account is straightforward. Below are two different approaches using the SDK infra or 3rd party infra.
## Install packages
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-ownership-mav1.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-ownership-mav1.mdx
index 290193fb1..15a807ec9 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-ownership-mav1.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-ownership-mav1.mdx
@@ -4,6 +4,9 @@ description: Follow this guide to manage ownership of a Modular Account with
slug: wallets/smart-contracts/other-accounts/modular-account/manage-ownership-mav1
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
The Multi Owner plugin lets your smart wallets have one or more ECDSA or SCA owners. This lets your account integrate with multiple owners at once, and supports recovering your account if one owner is lost.
The Multi-Owner Plugin is able to:
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/get-installed-plugins.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/get-installed-plugins.mdx
index da9206023..c4e0ef11c 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/get-installed-plugins.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/get-installed-plugins.mdx
@@ -4,6 +4,9 @@ description: Follow this guide to get installed plugins of a Modular Account
slug: wallets/smart-contracts/other-accounts/modular-account/manage-plugins/get-installed-plugins
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
[ERC-6900](https://eips.ethereum.org/EIPS/eip-6900) Modular Accounts implement the Plugin inspection interface [`IAccountLoupe.sol`](https://eips.ethereum.org/EIPS/eip-6900#iaccountloupesol) to support visibility in plugin configuration onchain. This contract interface defines the method `getInstalledPlugins()` that clients can use to fetch the currently installed plugins on a Modular Account.
```solidity
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/install-plugins.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/install-plugins.mdx
index e25e3fbd5..16a9db0f9 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/install-plugins.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/manage-plugins/install-plugins.mdx
@@ -4,6 +4,9 @@ description: Follow this guide to install and uninstall plugins on a Modular
slug: wallets/smart-contracts/other-accounts/modular-account/manage-plugins/install-plugins
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
[ERC-6900](https://eips.ethereum.org/EIPS/eip-6900) Modular Accounts implement the Plugin manager interface [`IPluginManager.sol`](https://eips.ethereum.org/EIPS/eip-6900#ipluginmanagersol) to support installing and uninstalling plugins on a Modular Account. This contract interface defines the method `installPlugin()` and `uninstallPlugin()` that clients can use to install or uninstall plugins on a Modular Account.
The `@account-kit/smart-contracts` package provides a streamlined experience for interacting with the Modular Account AccountLoupe interface through `pluginManagerActions`. When you connect your Modular Account to `SmartAccountClient` you can extend the client with `pluginManagerActions`, which exposes a set of methods available to call the account `AccountLoupe` with the client connected to the account.
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/multisig-plugin/getting-started.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/multisig-plugin/getting-started.mdx
index 790dd5f8b..ab31c5c01 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/multisig-plugin/getting-started.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/multisig-plugin/getting-started.mdx
@@ -4,6 +4,9 @@ description: Getting started with the Modular Account with Multisig Plugin in Wa
slug: wallets/smart-contracts/other-accounts/modular-account/multisig-plugin/getting-started
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
## 1. Create a multisig account client
Initialize a Multisig Modular Account client and set the `n` accounts as signers.
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/session-keys/getting-started.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/session-keys/getting-started.mdx
index 5cc6e337c..d1a342308 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/session-keys/getting-started.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/session-keys/getting-started.mdx
@@ -4,6 +4,9 @@ description: Learn how to use the Session Key Plugin.
slug: wallets/smart-contracts/other-accounts/modular-account/session-keys/getting-started
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
`@account-kit/smart-contracts` exports all of the definitions you need to use session keys with a Modular Account. It includes a `SessionKeySigner` class that generates session keys on the client and can be used as the `signer` for the Multi Owner Modular Account.
It also exports the necessary decorators which can be used to extend your `modularAccountClient` to interact with session keys.
diff --git a/content/wallets/pages/smart-contracts/other-accounts/modular-account/upgrading-to-modular-account.mdx b/content/wallets/pages/smart-contracts/other-accounts/modular-account/upgrading-to-modular-account.mdx
index 2552adb0a..6ee92d84c 100644
--- a/content/wallets/pages/smart-contracts/other-accounts/modular-account/upgrading-to-modular-account.mdx
+++ b/content/wallets/pages/smart-contracts/other-accounts/modular-account/upgrading-to-modular-account.mdx
@@ -4,6 +4,8 @@ description: Upgrading to a Modular Account using Wallet APIs
slug: wallets/smart-contracts/other-accounts/modular-account/upgrading-to-modular-account
---
+
+
Upgrade a `SmartContractAccount` using Wallet APIs by calling `upgradeAccount` on the `SmartAccountClient` with the necessary call data, `UpgradeToData`, for the account targeted for the upgrade. For upgrading to a Modular Account, use the utility function `getMSCAUpgradeToData` provided by the `@account-kit/smart-contracts` package to retrieve the call data for the upgrade. This process applies to any account with upgrade capabilities.
Using the Light Account as an example, here is an overview of how the upgrade can be executed using a Smart Account Client:
diff --git a/content/wallets/pages/smart-contracts/overview.mdx b/content/wallets/pages/smart-contracts/overview.mdx
index 4777d0dd3..a54af88a1 100644
--- a/content/wallets/pages/smart-contracts/overview.mdx
+++ b/content/wallets/pages/smart-contracts/overview.mdx
@@ -4,6 +4,8 @@ description: An overview of the smart contracts available in Wallet APIs and how
slug: wallets/smart-contracts/overview
---
+
+
One of the low level packages available in Wallet APIs is the Smart Contracts package. This package contains all of the smart contract interfaces and utilities you need to use any of the
secure, audited smart contracts.
diff --git a/content/wallets/pages/third-party/signers/custom-integration.mdx b/content/wallets/pages/third-party/signers/custom-integration.mdx
index e0c0761b5..1d4efad84 100644
--- a/content/wallets/pages/third-party/signers/custom-integration.mdx
+++ b/content/wallets/pages/third-party/signers/custom-integration.mdx
@@ -4,8 +4,6 @@ description: Use any viem-compatible signer with Wallet APIs
slug: wallets/third-party/signers/custom-integration
---
-`@alchemy/wallet-apis` (v5.x.x) is currently in beta but is the recommended replacement for `@account-kit/wallet-client` (v4.x.x). If you run into any issues, please [reach out](mailto:support@alchemy.com).
-
Any signer that provides a viem `LocalAccount` or `WalletClient` works with `createSmartWalletClient`. This guide covers the requirements and how to set up each type.
## Requirements
diff --git a/content/wallets/pages/third-party/signers/openfort.mdx b/content/wallets/pages/third-party/signers/openfort.mdx
index 422f9c263..6c799905d 100644
--- a/content/wallets/pages/third-party/signers/openfort.mdx
+++ b/content/wallets/pages/third-party/signers/openfort.mdx
@@ -4,8 +4,6 @@ description: Use Openfort with Wallet APIs for EIP-7702, sponsorship, and batchi
slug: wallets/third-party/signers/openfort
---
-`@alchemy/wallet-apis` (v5.x.x) is currently in beta but is the recommended replacement for `@account-kit/wallet-client` (v4.x.x). If you run into any issues, please [reach out](mailto:support@alchemy.com).
-
Upgrade existing Openfort embedded wallets to Wallet APIs to enable gasless transactions, batching, and more in under 10 minutes. Keep Openfort for authentication, no wallet migration needed. Add battle-tested transaction infrastructure using EIP-7702 to upgrade your wallets to Wallet APIs:
* [#1 gas abstraction infrastructure](https://www.bundlebear.com/erc4337-bundlers/all) on the market
diff --git a/content/wallets/pages/third-party/signers/privy.mdx b/content/wallets/pages/third-party/signers/privy.mdx
index b68cb6cb5..80bcc12a8 100644
--- a/content/wallets/pages/third-party/signers/privy.mdx
+++ b/content/wallets/pages/third-party/signers/privy.mdx
@@ -4,8 +4,6 @@ description: Use Privy with Wallet APIs for EIP-7702, sponsorship, and batching
slug: wallets/third-party/signers/privy
---
-`@alchemy/wallet-apis` (v5.x.x) is currently in beta but is the recommended replacement for `@account-kit/wallet-client` (v4.x.x). If you run into any issues, please [reach out](mailto:support@alchemy.com).
-
Upgrade existing Privy wallets to Wallet APIs to enable gasless transactions, batching, and more in under 10 minutes. Keep Privy for authentication, no wallet migration needed. Add battle-tested transaction infrastructure using EIP-7702 to upgrade your wallets to Wallet APIs:
* [#1 gas abstraction infrastructure](https://www.bundlebear.com/erc4337-bundlers/all) on the market
diff --git a/content/wallets/pages/third-party/signers/turnkey.mdx b/content/wallets/pages/third-party/signers/turnkey.mdx
index 60b347623..9b4360111 100644
--- a/content/wallets/pages/third-party/signers/turnkey.mdx
+++ b/content/wallets/pages/third-party/signers/turnkey.mdx
@@ -4,8 +4,6 @@ description: Use Turnkey with Wallet APIs for EIP-7702, sponsorship, and batchin
slug: wallets/third-party/signers/turnkey
---
-`@alchemy/wallet-apis` (v5.x.x) is currently in beta but is the recommended replacement for `@account-kit/wallet-client` (v4.x.x). If you run into any issues, please [reach out](mailto:support@alchemy.com).
-
Upgrade existing Turnkey wallets to Wallet APIs to enable gasless transactions, batching, and more in under 10 minutes. Keep Turnkey for key management, no wallet migration needed. Add battle-tested transaction infrastructure using EIP-7702 to upgrade EOAs to Wallet APIs:
* [#1 gas abstraction infrastructure](https://www.bundlebear.com/erc4337-bundlers/all) on the market
diff --git a/content/wallets/pages/third-party/smart-contracts.mdx b/content/wallets/pages/third-party/smart-contracts.mdx
index 655824a1f..74f819091 100644
--- a/content/wallets/pages/third-party/smart-contracts.mdx
+++ b/content/wallets/pages/third-party/smart-contracts.mdx
@@ -1,883 +1,58 @@
---
title: 3rd party smart contracts
-description: Learn how to use smart contract accounts not included in Wallet APIs
+description: Use smart account contracts not included in Wallet APIs
slug: wallets/third-party/smart-contracts
---
-You are not limited to the accounts defined in `@account-kit/smart-contracts`.
-If you'd like to bring your own smart account, you have a couple options:
+You aren't limited to the accounts defined in `@alchemy/smart-accounts`. If you'd like to bring your own smart account, you have two options:
-1. Use a 3rd party library that exports an `aa-sdk` compatible `SmartContractAccount` interface.
-2. Implement your own `SmartContractAccount` interface using `toSmartContractAccount`.
+1. Use any library that returns a viem-compatible [`SmartAccount`](https://viem.sh/account-abstraction/accounts/smart) — those plug into the same `createBundlerClient` flow we use for Light Account and Modular Account V2.
+2. Build your own `SmartAccount` implementation with viem's [`toSmartAccount`](https://viem.sh/account-abstraction/accounts/smart/toSmartAccount) primitive.
-## Third party SDKs
+## Using a third-party `SmartAccount`
-
- If you've built an SDK or guide that leverages any of the methods above to use as a smart contract within Wallet APIs, open a PR to add a link to your content in this section.
-
-
-## Using `toSmartContractAccount`
+Any object that satisfies viem's `SmartAccount` interface works:
-The `SmartAccountClient` can be used with any smart account because it only relies on the `SmartContractAccount` interface. This means you can use your own smart account implementation with Wallet APIs.
-
-```ts my-account.ts twoslash
-import { getEntryPoint, toSmartContractAccount } from "@aa-sdk/core";
-import { http, type SignableMessage, type Hash } from "viem";
+```ts
+import { createBundlerClient } from "viem/account-abstraction";
+import { createClient } from "viem";
import { sepolia } from "viem/chains";
+import { alchemyTransport } from "@alchemy/common";
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+import { myAccount } from "./my-account"; // your custom SmartAccount
+
+const transport = alchemyTransport({ apiKey: "your-api-key" });
+const rpcClient = createClient({ chain: sepolia, transport });
-const myAccount = await toSmartContractAccount({
- /// REQUIRED PARAMS ///
- source: "MyAccount",
- transport: http("RPC_URL"),
+const bundlerClient = createBundlerClient({
+ account: myAccount,
+ client: rpcClient,
chain: sepolia,
- // The EntryPointDef that your account is compatible with
- entryPoint: getEntryPoint(sepolia),
- // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method
- getAccountInitCode: async (): Promise => "0x{factoryAddress}{callData}",
- // an invalid signature that doesn't cause your account to revert during validation
- getDummySignature: async (): Promise => "0x1234...",
- // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method
- encodeExecute: async (uo): Promise => "0x....",
- signMessage: async ({ message }): Promise => "0x...",
- signTypedData: async (typedData): Promise => "0x000",
+ transport,
+ userOperation: { estimateFeesPerGas },
+});
- /// OPTIONAL PARAMS ///
- // if you already know your account's address, pass that in here to avoid generating a new counterfactual
- accountAddress: "0x...",
- // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method
- encodeBatchExecute: async (uos): Promise => "0x...",
- // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here
- signUserOperationHash: async (hash): Promise => "0x...",
- // allows you to define the calldata for upgrading your account
- encodeUpgradeToAndCall: async (params): Promise => "0x...",
+const hash = await bundlerClient.sendUserOperation({
+ calls: [{ to: "0x0000000000000000000000000000000000000000", value: 0n, data: "0x" }],
});
```
-To use your account, you will need to pass it into a `SmartAccountClient`.
-
-
- ```ts example.ts
- import { createAlchemySmartAccountClient, alchemy } from "@account-kit/infra";
- import { sepolia } from "viem/chains";
- import { myAccount } from "./my-account";
-
- const client = createAlchemySmartAccountClient({
- // created above
- account: myAccount,
- chain: sepolia,
- transport: alchemy({
- apiKey: "YOUR_API_KEY",
- }),
- });
- ```
-
- ```ts my-account.ts filename="my-account.ts"
- import { getEntryPoint, toSmartContractAccount } from "@aa-sdk/core";
- import { http, type SignableMessage, type Hash } from "viem";
- import { sepolia } from "viem/chains";
-
- export const myAccount = await toSmartContractAccount({
- /// REQUIRED PARAMS ///
- source: "MyAccount",
- transport: http("RPC_URL"),
- chain: sepolia,
- // The EntryPointDef that your account is compatible with
- entryPoint: getEntryPoint(sepolia),
- // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method
- getAccountInitCode: async (): Promise => "0x{factoryAddress}{callData}",
- // an invalid signature that doesn't cause your account to revert during validation
- getDummySignature: async (): Promise => "0x1234...",
- // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method
- encodeExecute: async (uo): Promise => "0x....",
- signMessage: async ({ message }): Promise => "0x...",
- signTypedData: async (typedData): Promise => "0x000",
-
- /// OPTIONAL PARAMS ///
- // if you already know your account's address, pass that in here to avoid generating a new counterfactual
- accountAddress: "0x...",
- // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method
- encodeBatchExecute: async (uos): Promise => "0x...",
- // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here
- signUserOperationHash: async (hash): Promise => "0x...",
- // allows you to define the calldata for upgrading your account
- encodeUpgradeToAndCall: async (params): Promise => "0x...",
- });
- ```
-
-
-### `LightSmartContractAccount` as an example
-
-The `@account-kit/smart-contracts` package includes an extension of the eth-infinitism `SimpleAccount` called [LightAccount.sol](https://github.com/alchemyplatform/light-account/blob/main/src/LightAccount.sol). Learn more about Light Account in the [Light Account documentation](/docs/wallets/smart-contracts/other-accounts/light-account/).
-
-The following implementation of `SmartContractAccount` works with `LightAccount.sol` and can be used as an example of how to implement your own smart contract account:
-
-
- ````ts
- import {
- createBundlerClient,
- getEntryPoint,
- type Address,
- type EntryPointDef,
- type SmartAccountSigner,
- } from "@aa-sdk/core";
- import {
- concatHex,
- encodeFunctionData,
- type Chain,
- type Hex,
- type Transport,
- } from "viem";
- import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js";
- import { LightAccountAbi_v2 } from "../abis/LightAccountAbi_v2.js";
- import { LightAccountFactoryAbi_v1 } from "../abis/LightAccountFactoryAbi_v1.js";
- import { LightAccountFactoryAbi_v2 } from "../abis/LightAccountFactoryAbi_v2.js";
- import type {
- LightAccountEntryPointVersion,
- LightAccountVersion,
- } from "../types.js";
- import {
- AccountVersionRegistry,
- LightAccountUnsupported1271Factories,
- defaultLightAccountVersion,
- getDefaultLightAccountFactoryAddress,
- } from "../utils.js";
- import {
- createLightAccountBase,
- type CreateLightAccountBaseParams,
- type LightAccountBase,
- } from "./base.js";
- import { predictLightAccountAddress } from "./predictAddress.js";
-
- export type LightAccount<
- TSigner extends SmartAccountSigner = SmartAccountSigner,
- TLightAccountVersion extends
- LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">,
- > = LightAccountBase & {
- encodeTransferOwnership: (newOwner: Address) => Hex;
- getOwnerAddress: () => Promise;
- };
-
- export type CreateLightAccountParams<
- TTransport extends Transport = Transport,
- TSigner extends SmartAccountSigner = SmartAccountSigner,
- TLightAccountVersion extends
- LightAccountVersion<"LightAccount"> = LightAccountVersion<"LightAccount">,
- > = Omit<
- CreateLightAccountBaseParams<
- "LightAccount",
- TLightAccountVersion,
- TTransport,
- TSigner
- >,
- | "getAccountInitCode"
- | "entryPoint"
- | "version"
- | "abi"
- | "accountAddress"
- | "type"
- > & {
- salt?: bigint;
- initCode?: Hex;
- accountAddress?: Address;
- factoryAddress?: Address;
- version?: TLightAccountVersion;
- entryPoint?: EntryPointDef<
- LightAccountEntryPointVersion<"LightAccount", TLightAccountVersion>,
- Chain
- >;
- };
-
- export async function createLightAccount<
- TTransport extends Transport = Transport,
- TSigner extends SmartAccountSigner = SmartAccountSigner,
- TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0",
- >(
- config: CreateLightAccountParams,
- ): Promise>;
-
- /**
- * Creates a light account based on the provided parameters such as transport, chain, signer, init code, and more. Ensures that an account is configured and returned with various capabilities, such as transferring ownership and retrieving the owner's address.
- *
- * @example
- * ```ts
- * import { createLightAccount } from "@account-kit/smart-contracts";
- * import { LocalAccountSigner } from "@aa-sdk/core";
- * import { sepolia } from "viem/chains";
- * import { http, generatePrivateKey } from "viem"
- *
- * const account = await createLightAccount({
- * chain: sepolia,
- * transport: http("RPC_URL"),
- * signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey())
- * });
- * ```
- *
- * @param {CreateLightAccountParams} config The parameters for creating a light account
- * @returns {Promise} A promise that resolves to a `LightAccount` object containing the created account information and methods
- */
- export async function createLightAccount({
- transport,
- chain,
- signer,
- initCode,
- version = defaultLightAccountVersion(),
- entryPoint = getEntryPoint(chain, {
- version: AccountVersionRegistry["LightAccount"][version]
- .entryPointVersion as any,
- }),
- accountAddress,
- factoryAddress = getDefaultLightAccountFactoryAddress(chain, version),
- salt: salt_ = 0n,
- }: CreateLightAccountParams): Promise {
- const client = createBundlerClient({
- transport,
- chain,
- });
-
- const accountAbi =
- version === "v2.0.0" ? LightAccountAbi_v2 : LightAccountAbi_v1;
- const factoryAbi =
- version === "v2.0.0"
- ? LightAccountFactoryAbi_v1
- : LightAccountFactoryAbi_v2;
-
- const signerAddress = await signer.getAddress();
-
- const salt = LightAccountUnsupported1271Factories.has(
- factoryAddress.toLowerCase() as Address,
- )
- ? 0n
- : salt_;
-
- const getAccountInitCode = async () => {
- if (initCode) return initCode;
-
- return concatHex([
- factoryAddress,
- encodeFunctionData({
- abi: factoryAbi,
- functionName: "createAccount",
- args: [signerAddress, salt],
- }),
- ]);
- };
-
- const address =
- accountAddress ??
- predictLightAccountAddress({
- factoryAddress,
- salt,
- ownerAddress: signerAddress,
- version,
- });
-
- const account = await createLightAccountBase<
- "LightAccount",
- LightAccountVersion<"LightAccount">,
- Transport,
- SmartAccountSigner
- >({
- transport,
- chain,
- signer,
- abi: accountAbi,
- type: "LightAccount",
- version,
- entryPoint,
- accountAddress: address,
- getAccountInitCode,
- });
-
- return {
- ...account,
-
- encodeTransferOwnership: (newOwner: Address) => {
- return encodeFunctionData({
- abi: accountAbi,
- functionName: "transferOwnership",
- args: [newOwner],
- });
- },
- async getOwnerAddress(): Promise {
- const callResult = await client.readContract({
- address,
- abi: accountAbi,
- functionName: "owner",
- });
-
- if (callResult == null) {
- throw new Error("could not get on-chain owner");
- }
-
- return callResult;
- },
- };
- }
- ````
-
-
-### The `toSmartContractAccount` method
-
-For your reference, this is the definition of the `toSmartContractAccount` interface as pulled from the source code:
-
-
- ````ts
- import {
- getContract,
- hexToBytes,
- type Address,
- type Chain,
- type CustomSource,
- type Hex,
- type LocalAccount,
- type PublicClient,
- type SignableMessage,
- type Transport,
- type TypedData,
- type TypedDataDefinition,
- } from "viem";
- import { toAccount } from "viem/accounts";
- import { createBundlerClient } from "../client/bundlerClient.js";
- import type {
- EntryPointDef,
- EntryPointRegistryBase,
- EntryPointVersion,
- } from "../entrypoint/types.js";
- import {
- BatchExecutionNotSupportedError,
- FailedToGetStorageSlotError,
- GetCounterFactualAddressError,
- SignTransactionNotSupportedError,
- UpgradesNotSupportedError,
- } from "../errors/account.js";
- import { InvalidRpcUrlError } from "../errors/client.js";
- import { InvalidEntryPointError } from "../errors/entrypoint.js";
- import { Logger } from "../logger.js";
- import type { SmartAccountSigner } from "../signer/types.js";
- import { wrapSignatureWith6492 } from "../signer/utils.js";
- import type { NullAddress } from "../types.js";
- import type { IsUndefined, Never } from "../utils/types.js";
-
- export type AccountOp = {
- target: Address;
- value?: bigint;
- data: Hex | "0x";
- };
-
- export enum DeploymentState {
- UNDEFINED = "0x0",
- NOT_DEPLOYED = "0x1",
- DEPLOYED = "0x2",
- }
-
- export type SignatureRequest =
- | {
- type: "personal_sign";
- data: SignableMessage;
- }
- | {
- type: "eth_signTypedData_v4";
- data: TypedDataDefinition;
- };
-
- export type SigningMethods = {
- prepareSign: (request: SignatureRequest) => Promise;
- formatSign: (signature: Hex) => Promise;
- };
-
- export type GetEntryPointFromAccount<
- TAccount extends SmartContractAccount | undefined,
- TAccountOverride extends SmartContractAccount = SmartContractAccount,
- > =
- GetAccountParameter extends SmartContractAccount<
- string,
- infer TEntryPointVersion
- >
- ? TEntryPointVersion
- : EntryPointVersion;
-
- export type GetAccountParameter<
- TAccount extends SmartContractAccount | undefined =
- | SmartContractAccount
- | undefined,
- TAccountOverride extends SmartContractAccount = SmartContractAccount,
- > =
- IsUndefined extends true
- ? { account: TAccountOverride }
- : { account?: TAccountOverride };
-
- export type UpgradeToAndCallParams = {
- upgradeToAddress: Address;
- upgradeToInitData: Hex;
- };
-
- export type SmartContractAccountWithSigner<
- Name extends string = string,
- TSigner extends SmartAccountSigner = SmartAccountSigner,
- TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
- > = SmartContractAccount & {
- getSigner: () => TSigner;
- };
+## Building a custom `SmartAccount`
- /**
- * Determines if the given SmartContractAccount has a signer associated with it.
- *
- * @example
- * ```ts
- * import { toSmartContractAccount } from "@aa-sdk/core";
- *
- * const account = await toSmartContractAccount(...);
- *
- * console.log(isSmartAccountWithSigner(account)); // false: the base account does not have a publicly accessible signer
- * ```
- *
- * @param {SmartContractAccount} account The account to check.
- * @returns {boolean} true if the account has a signer, otherwise false.
- */
- export const isSmartAccountWithSigner = (
- account: SmartContractAccount,
- ): account is SmartContractAccountWithSigner => {
- return "getSigner" in account;
- };
+Use [`toSmartAccount`](https://viem.sh/account-abstraction/accounts/smart/toSmartAccount) from `viem/account-abstraction`. You provide:
- export type SmartContractAccount<
- Name extends string = string,
- TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
- > = LocalAccount & {
- source: Name;
- getDummySignature: () => Hex | Promise;
- encodeExecute: (tx: AccountOp) => Promise;
- encodeBatchExecute: (txs: AccountOp[]) => Promise;
- signUserOperationHash: (uoHash: Hex) => Promise;
- signMessageWith6492: (params: { message: SignableMessage }) => Promise;
- signTypedDataWith6492: <
- const typedData extends TypedData | Record,
- primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
- >(
- typedDataDefinition: TypedDataDefinition,
- ) => Promise;
- encodeUpgradeToAndCall: (params: UpgradeToAndCallParams) => Promise;
- getAccountNonce(nonceKey?: bigint): Promise;
- getInitCode: () => Promise;
- isAccountDeployed: () => Promise;
- getFactoryAddress: () => Promise;
- getFactoryData: () => Promise;
- getEntryPoint: () => EntryPointDef;
- getImplementationAddress: () => Promise;
- } & SigningMethods;
+* The entry point version your account uses (`0.6`, `0.7`, or `0.8`).
+* `getAddress()` — counterfactual or deployed address.
+* `getFactoryArgs()` — factory address + calldata (for first-time deploys).
+* `encodeCalls()` — encode one or more calls into your account's execution calldata.
+* `signUserOperation()` / `signMessage` / `signTypedData` — signing methods bound to your account's owner.
- export interface AccountEntryPointRegistry
- extends EntryPointRegistryBase<
- SmartContractAccount
- > {
- "0.6.0": SmartContractAccount;
- "0.7.0": SmartContractAccount;
- }
+The viem docs walk through a full custom example. Once you have a `SmartAccount`, pass it to `createBundlerClient` exactly like the snippet above.
- export type ToSmartContractAccountParams<
- Name extends string = string,
- TTransport extends Transport = Transport,
- TChain extends Chain = Chain,
- TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
- > = {
- source: Name;
- transport: TTransport;
- chain: TChain;
- entryPoint: EntryPointDef;
- accountAddress?: Address;
- getAccountInitCode: () => Promise;
- getDummySignature: () => Hex | Promise;
- encodeExecute: (tx: AccountOp) => Promise;
- encodeBatchExecute?: (txs: AccountOp[]) => Promise;
- getNonce?: (nonceKey?: bigint) => Promise;
- // if not provided, will default to just using signMessage over the Hex
- signUserOperationHash?: (uoHash: Hex) => Promise;
- encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise;
- getImplementationAddress?: () => Promise;
- } & Omit &
- (SigningMethods | Never);
-
- /**
- * Parses the factory address and factory calldata from the provided account initialization code (initCode).
- *
- * @example
- * ```ts
- * import { parseFactoryAddressFromAccountInitCode } from "@aa-sdk/core";
- *
- * const [address, calldata] = parseFactoryAddressFromAccountInitCode("0xAddressCalldata");
- * ```
- *
- * @param {Hex} initCode The initialization code from which to parse the factory address and calldata
- * @returns {[Address, Hex]} A tuple containing the parsed factory address and factory calldata
- */
- export const parseFactoryAddressFromAccountInitCode = (
- initCode: Hex,
- ): [Address, Hex] => {
- const factoryAddress: Address = `0x${initCode.substring(2, 42)}`;
- const factoryCalldata: Hex = `0x${initCode.substring(42)}`;
- return [factoryAddress, factoryCalldata];
- };
-
- export type GetAccountAddressParams = {
- client: PublicClient;
- entryPoint: EntryPointDef;
- accountAddress?: Address;
- getAccountInitCode: () => Promise;
- };
-
- /**
- * Retrieves the account address. Uses a provided `accountAddress` if available; otherwise, it computes the address using the entry point contract and the initial code.
- *
- * @example
- * ```ts
- * import { getEntryPoint, getAccountAddress } from "@aa-sdk/core";
- *
- * const accountAddress = await getAccountAddress({
- * client,
- * entryPoint: getEntryPoint(chain),
- * getAccountInitCode: async () => "0x{factoryAddress}{factoryCallData}",
- * });
- * ```
- *
- * @param {GetAccountAddressParams} params The configuration object
- * @param {PublicClient} params.client A public client instance to interact with the blockchain
- * @param {EntryPointDef} params.entryPoint The entry point definition which includes the address and ABI
- * @param {Address} params.accountAddress Optional existing account address
- * @param {() => Promise} params.getAccountInitCode A function that returns a Promise resolving to a Hex string representing the initial code of the account
- * @returns {Promise} A promise that resolves to the account address
- */
- export const getAccountAddress = async ({
- client,
- entryPoint,
- accountAddress,
- getAccountInitCode,
- }: GetAccountAddressParams) => {
- if (accountAddress) return accountAddress;
-
- const entryPointContract = getContract({
- address: entryPoint.address,
- abi: entryPoint.abi,
- client,
- });
-
- const initCode = await getAccountInitCode();
- Logger.verbose("[BaseSmartContractAccount](getAddress) initCode: ", initCode);
-
- try {
- await entryPointContract.simulate.getSenderAddress([initCode]);
- } catch (err: any) {
- Logger.verbose(
- "[BaseSmartContractAccount](getAddress) getSenderAddress err: ",
- err,
- );
- if (err.cause?.data?.errorName === "SenderAddressResult") {
- Logger.verbose(
- "[BaseSmartContractAccount](getAddress) entryPoint.getSenderAddress result:",
- err.cause.data.args[0],
- );
-
- return err.cause.data.args[0] as Address;
- }
-
- if (err.details === "Invalid URL") {
- throw new InvalidRpcUrlError();
- }
- }
-
- throw new GetCounterFactualAddressError();
- };
-
- export async function toSmartContractAccount<
- Name extends string = string,
- TTransport extends Transport = Transport,
- TChain extends Chain = Chain,
- TEntryPointVersion extends EntryPointVersion = EntryPointVersion,
- >({
- transport,
- chain,
- entryPoint,
- source,
- accountAddress,
- getAccountInitCode,
- getNonce,
- signMessage,
- signTypedData,
- encodeBatchExecute,
- encodeExecute,
- getDummySignature,
- signUserOperationHash,
- encodeUpgradeToAndCall,
- }: ToSmartContractAccountParams<
- Name,
- TTransport,
- TChain,
- TEntryPointVersion
- >): Promise>;
-
- /**
- * Converts an account to a smart contract account and sets up various account-related methods using the provided parameters like transport, chain, entry point, and other utilities.
- *
- * @example
- * ```ts
- * import { http, type SignableMessage } from "viem";
- * import { sepolia } from "viem/chains";
- *
- * const myAccount = await toSmartContractAccount({
- * /// REQUIRED PARAMS ///
- * source: "MyAccount",
- * transport: http("RPC_URL"),
- * chain: sepolia,
- * // The EntryPointDef that your account is com"patible with
- * entryPoint: getEntryPoint(sepolia, { version: "0.6.0" }),
- * // This should return a concatenation of your `factoryAddress` and the `callData` for your factory's create account method
- * getAccountInitCode: async () => "0x{factoryAddress}{callData}",
- * // an invalid signature that doesn't cause your account to revert during validation
- * getDummySignature: () => "0x1234...",
- * // given a UO in the form of {target, data, value} should output the calldata for calling your contract's execution method
- * encodeExecute: async (uo) => "0xcalldata",
- * signMessage: async ({ message }: { message: SignableMessage }) => "0x...",
- * signTypedData: async (typedData) => "0x000",
- *
- * /// OPTIONAL PARAMS ///
- * // if you already know your account's address, pass that in here to avoid generating a new counterfactual
- * accountAddress: "0xaddressoverride",
- * // if your account supports batching, this should take an array of UOs and return the calldata for calling your contract's batchExecute method
- * encodeBatchExecute: async (uos) => "0x...",
- * // if your contract expects a different signing scheme than the default signMessage scheme, you can override that here
- * signUserOperationHash: async (hash) => "0x...",
- * // allows you to define the calldata for upgrading your account
- * encodeUpgradeToAndCall: async (params) => "0x...",
- * });
- * ```
- *
- * @param {ToSmartContractAccountParams} params the parameters required for converting to a smart contract account
- * @param {Transport} params.transport the transport mechanism used for communication
- * @param {Chain} params.chain the blockchain chain used in the account
- * @param {EntryPoint} params.entryPoint the entry point of the smart contract
- * @param {string} params.source the source identifier for the account
- * @param {Address} [params.accountAddress] the address of the account
- * @param {() => Promise} params.getAccountInitCode a function to get the initial state code of the account
- * @param {(message: { message: SignableMessage }) => Promise} params.signMessage a function to sign a message
- * @param {(typedDataDefinition: TypedDataDefinition) => Promise} params.signTypedData a function to sign typed data
- * @param {(transactions: Transaction[]) => Hex} [params.encodeBatchExecute] a function to encode batch transactions
- * @param {(tx: Transaction) => Hex} params.encodeExecute a function to encode a single transaction
- * @param {() => Promise} params.getDummySignature a function to get a dummy signature
- * @param {(uoHash: Hex) => Promise} [params.signUserOperationHash] a function to sign user operations
- * @param {(implementationAddress: Address, implementationCallData: Hex) => Hex} [params.encodeUpgradeToAndCall] a function to encode upgrade call
- * @returns {Promise} a promise that resolves to a SmartContractAccount object with methods and properties for interacting with the smart contract account
- */
- export async function toSmartContractAccount(
- params: ToSmartContractAccountParams,
- ): Promise {
- const {
- transport,
- chain,
- entryPoint,
- source,
- accountAddress,
- getAccountInitCode,
- signMessage,
- signTypedData,
- encodeExecute,
- encodeBatchExecute,
- getNonce,
- getDummySignature,
- signUserOperationHash,
- encodeUpgradeToAndCall,
- getImplementationAddress,
- prepareSign: prepareSign_,
- formatSign: formatSign_,
- } = params;
-
- const client = createBundlerClient({
- // we set the retry count to 0 so that viem doesn't retry during
- // getting the address. That call always reverts and without this
- // viem will retry 3 times, making this call very slow
- transport: (opts) => transport({ ...opts, chain, retryCount: 0 }),
- chain,
- });
-
- const entryPointContract = getContract({
- address: entryPoint.address,
- abi: entryPoint.abi,
- client,
- });
-
- const accountAddress_ = await getAccountAddress({
- client,
- entryPoint: entryPoint,
- accountAddress,
- getAccountInitCode,
- });
-
- let deploymentState = DeploymentState.UNDEFINED;
-
- const getInitCode = async () => {
- if (deploymentState === DeploymentState.DEPLOYED) {
- return "0x";
- }
- const contractCode = await client.getCode({
- address: accountAddress_,
- });
-
- if ((contractCode?.length ?? 0) > 2) {
- deploymentState = DeploymentState.DEPLOYED;
- return "0x";
- } else {
- deploymentState = DeploymentState.NOT_DEPLOYED;
- }
-
- return getAccountInitCode();
- };
-
- const signUserOperationHash_ =
- signUserOperationHash ??
- (async (uoHash: Hex) => {
- return signMessage({ message: { raw: hexToBytes(uoHash) } });
- });
-
- const getFactoryAddress = async (): Promise =>
- parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[0];
-
- const getFactoryData = async (): Promise =>
- parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[1];
-
- const encodeUpgradeToAndCall_ =
- encodeUpgradeToAndCall ??
- (() => {
- throw new UpgradesNotSupportedError(source);
- });
-
- const isAccountDeployed = async () => {
- const initCode = await getInitCode();
- return initCode === "0x";
- };
-
- const getNonce_ =
- getNonce ??
- (async (nonceKey = 0n): Promise => {
- return entryPointContract.read.getNonce([
- accountAddress_,
- nonceKey,
- ]) as Promise;
- });
-
- const account = toAccount({
- address: accountAddress_,
- signMessage,
- signTypedData,
- signTransaction: () => {
- throw new SignTransactionNotSupportedError();
- },
- });
-
- const create6492Signature = async (isDeployed: boolean, signature: Hex) => {
- if (isDeployed) {
- return signature;
- }
-
- const [factoryAddress, factoryCalldata] =
- parseFactoryAddressFromAccountInitCode(await getAccountInitCode());
-
- return wrapSignatureWith6492({
- factoryAddress,
- factoryCalldata,
- signature,
- });
- };
-
- const signMessageWith6492 = async (message: { message: SignableMessage }) => {
- const [isDeployed, signature] = await Promise.all([
- isAccountDeployed(),
- account.signMessage(message),
- ]);
-
- return create6492Signature(isDeployed, signature);
- };
-
- const signTypedDataWith6492 = async <
- const typedData extends TypedData | Record,
- primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
- >(
- typedDataDefinition: TypedDataDefinition,
- ): Promise => {
- const [isDeployed, signature] = await Promise.all([
- isAccountDeployed(),
- account.signTypedData(typedDataDefinition),
- ]);
-
- return create6492Signature(isDeployed, signature);
- };
-
- const getImplementationAddress_ =
- getImplementationAddress ??
- (async () => {
- const storage = await client.getStorageAt({
- address: account.address,
- // This is the default slot for the implementation address for Proxies
- slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
- });
-
- if (storage == null) {
- throw new FailedToGetStorageSlotError(
- "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
- "Proxy Implementation Address",
- );
- }
-
- // The storage slot contains a full bytes32, but we want only the last 20 bytes.
- // So, slice off the leading `0x` and the first 12 bytes (24 characters), leaving the last 20 bytes, then prefix with `0x`.
- return `0x${storage.slice(26)}`;
- });
-
- if (entryPoint.version !== "0.6.0" && entryPoint.version !== "0.7.0") {
- throw new InvalidEntryPointError(chain, entryPoint.version);
- }
-
- if ((prepareSign_ && !formatSign_) || (!prepareSign_ && formatSign_)) {
- throw new Error(
- "Must implement both prepareSign and formatSign or neither",
- );
- }
-
- const prepareSign =
- prepareSign_ ??
- (() => {
- throw new Error("prepareSign not implemented");
- });
+
+ Built an SDK or guide that defines a custom `SmartAccount` for Wallet APIs? Open a PR to add it here.
+
- const formatSign =
- formatSign_ ??
- (() => {
- throw new Error("formatSign not implemented");
- });
+### Why use the existing implementations?
- return {
- ...account,
- source,
- // TODO: I think this should probably be signUserOperation instead
- // and allow for generating the UO hash based on the EP version
- signUserOperationHash: signUserOperationHash_,
- getFactoryAddress,
- getFactoryData,
- encodeBatchExecute:
- encodeBatchExecute ??
- (() => {
- throw new BatchExecutionNotSupportedError(source);
- }),
- encodeExecute,
- getDummySignature,
- getInitCode,
- encodeUpgradeToAndCall: encodeUpgradeToAndCall_,
- getEntryPoint: () => entryPoint,
- isAccountDeployed,
- getAccountNonce: getNonce_,
- signMessageWith6492,
- signTypedDataWith6492,
- getImplementationAddress: getImplementationAddress_,
- prepareSign,
- formatSign,
- };
- }
- ````
-
+`@alchemy/smart-accounts` ships viem-compatible implementations of [Light Account](/docs/wallets/smart-contracts/other-accounts/light-account/getting-started), [Modular Account V1](/docs/wallets/reference/smart-accounts), and [Modular Account V2](/docs/wallets/smart-contracts/modular-account-v2/getting-started), plus the validation/permission builders for MAv2. If your use case fits one of those, you'll save a lot of custom-account boilerplate.
diff --git a/content/wallets/pages/troubleshooting/server-side-rendering.mdx b/content/wallets/pages/troubleshooting/server-side-rendering.mdx
index 5f2d28290..2ed625096 100644
--- a/content/wallets/pages/troubleshooting/server-side-rendering.mdx
+++ b/content/wallets/pages/troubleshooting/server-side-rendering.mdx
@@ -4,6 +4,9 @@ description: Learn how to use Wallet APIs with server-side rendering.
slug: wallets/troubleshooting/ssr
---
+{/* No inbound links from other /content pages as of 2026-05-22 — candidate for retirement rather than v5 rewrite. */}
+
+
> ⚠️ **Common Issue**: If SSR is not set up correctly, you may see sessions resetting or users getting logged out unexpectedly. Make sure to follow this guide fully to preserve session state.
When using Wallet APIs in a server-side rendered setting, you may see inconsistencies in account state between the server and the client. This leads to flashes of content when logged in. To avoid this, load the account state optimistically on the server and pass it to the client.
diff --git a/content/wallets/shared/infra/mav2-client.mdx b/content/wallets/shared/infra/mav2-client.mdx
index 348b7a0ac..efd6ef163 100644
--- a/content/wallets/shared/infra/mav2-client.mdx
+++ b/content/wallets/shared/infra/mav2-client.mdx
@@ -1,32 +1,39 @@
-```ts title="config.ts"
-import { alchemy, sepolia } from "@account-kit/infra";
+ ```ts title="config.ts"
+ import { sepolia } from "viem/chains";
-const YOUR_API_KEY = "";
+ export const chain = sepolia;
+ export const ALCHEMY_API_KEY = "";
+ export const policyId = "";
+ ```
-export const chain = sepolia;
+ ```ts title="client.ts"
+ import { createBundlerClient, createPaymasterClient } from "viem/account-abstraction";
+ import { createClient } from "viem";
+ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
+ import { alchemyTransport } from "@alchemy/common";
+ import { toModularAccountV2 } from "@alchemy/smart-accounts";
+ import { estimateFeesPerGas } from "@alchemy/aa-infra";
+ import { chain, ALCHEMY_API_KEY, policyId } from "./config";
-export const policyId = "";
+ const transport = alchemyTransport({ apiKey: ALCHEMY_API_KEY });
-export const transport = alchemy({
- apiKey: YOUR_API_KEY,
-});
-```
+ const rpcClient = createClient({ chain, transport });
-```ts title="client.ts"
-import { createModularAccountV2Client } from "@account-kit/smart-contracts";
-import { chain, transport, policyId } from "./config";
-import { LocalAccountSigner } from "@aa-sdk/core";
-import { generatePrivateKey } from "viem/accounts";
-
-export async function createClient() {
- return createModularAccountV2Client({
- chain,
- transport,
- signer: LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey()),
- policyId,
- });
-}
-```
+ export async function getClient() {
+ const owner = privateKeyToAccount(generatePrivateKey());
+ const account = await toModularAccountV2({ client: rpcClient, owner });
+ return createBundlerClient({
+ account,
+ client: rpcClient,
+ chain,
+ transport,
+ // Drop the next two lines if you don't want gas sponsorship.
+ paymaster: createPaymasterClient({ transport }),
+ paymasterContext: { policyId },
+ userOperation: { estimateFeesPerGas },
+ });
+ }
+ ```
diff --git a/content/wallets/shared/v4-code-banner.mdx b/content/wallets/shared/v4-code-banner.mdx
new file mode 100644
index 000000000..8b39ae6db
--- /dev/null
+++ b/content/wallets/shared/v4-code-banner.mdx
@@ -0,0 +1 @@
+This page documents Account Kit (v4). New projects are recommended to start with [v5 (`@alchemy/wallet-apis`)](/docs/wallets/quickstart).
diff --git a/content/wallets/shared/v4-concept-banner.mdx b/content/wallets/shared/v4-concept-banner.mdx
new file mode 100644
index 000000000..66ae87b84
--- /dev/null
+++ b/content/wallets/shared/v4-concept-banner.mdx
@@ -0,0 +1 @@
+Note: This page describes AA-SDK v4 concepts that are largely abstracted away in v5 (`@alchemy/wallet-apis`).
diff --git a/content/wallets/shared/v4-react-banner.mdx b/content/wallets/shared/v4-react-banner.mdx
new file mode 100644
index 000000000..e818aa42d
--- /dev/null
+++ b/content/wallets/shared/v4-react-banner.mdx
@@ -0,0 +1 @@
+This page documents Account Kit (v4) React. v5 (`@alchemy/wallet-apis`) is framework-agnostic and does not currently include a dedicated React package. Drop it into any framework with the state manager of your choice. See the [Wallet APIs quickstart](/docs/wallets/quickstart) and the [transaction guides](/docs/wallets/transactions/send-transactions).
diff --git a/content/wallets/shared/v4-signer-banner.mdx b/content/wallets/shared/v4-signer-banner.mdx
new file mode 100644
index 000000000..dc75a9c6a
--- /dev/null
+++ b/content/wallets/shared/v4-signer-banner.mdx
@@ -0,0 +1 @@
+The Alchemy signer is being sunset. For new projects, we recommend using [`@alchemy/wallet-apis`](/docs/wallets/quickstart) with [Privy as your signer](/docs/wallets/third-party/signers/privy).