You're going to need @alchemy/wallet-apis and viem.
npm install @alchemy/wallet-apis viem
This quickstart uses a local private key signer as an example. You can use any viem-compatible signer, including providers like Privy and Turnkey.
import { createSmartWalletClient, alchemyWalletTransport } from "@alchemy/wallet-apis";import { arbitrumSepolia } from "viem/chains";import { privateKeyToAccount } from "viem/accounts";const client = createSmartWalletClient({ transport: alchemyWalletTransport({ apiKey: "YOUR_API_KEY", }), chain: arbitrumSepolia, signer: privateKeyToAccount("0xYOUR_PRIVATE_KEY" as const), paymaster: { policyId: "YOUR_POLICY_ID", },});
The client defaults to EIP-7702, so your EOA will be delegated to a smart account to enable gas sponsorship, batching, and more. The SDK handles delegation automatically on the first transaction.
import { zeroAddress } from "viem";const { id } = await client.sendCalls({ calls: [{ to: zeroAddress, value: BigInt(0) }],});
const status = await client.waitForCallsStatus({ id });const txHash = status.receipts?.[0]?.transactionHash;console.log(`Explorer: https://sepolia.arbiscan.io/tx/${txHash}`);
Using @account-kit/wallet-client (v4.x.x)?
The examples on this page use @alchemy/wallet-apis (v5.x.x). If you're using @account-kit/wallet-client (v4.x.x), the client setup looks like this:
client.ts (v4.x.x)
import { LocalAccountSigner } from "@aa-sdk/core";import { createSmartWalletClient } from "@account-kit/wallet-client";import { alchemy, sepolia } from "@account-kit/infra";const signer = LocalAccountSigner.privateKeyToAccountSigner("0xYOUR_PRIVATE_KEY" as const);export const client = createSmartWalletClient({ transport: alchemy({ apiKey: "YOUR_API_KEY" }), chain: sepolia, signer, account: signer.address, // can also be passed per action as `from` or `account` // Optional: sponsor gas for your users (see "Sponsor gas" guide) policyId: "YOUR_POLICY_ID",});
Key v4.x.x differences:
Account address must be specified on the client or per action (from or account). In v5.x.x, the client automatically uses the signer's address as the account address via EIP-7702.
Chain imports come directly from @account-kit/infra instead of viem/chains.
Numeric values use hex strings: value: "0x0" instead of value: BigInt(0).
In v4.x.x, the paymaster capability on prepareCalls or sendCalls is called paymasterService instead of paymaster, or you can set the policyId directly on the client.
Signers use LocalAccountSigner / WalletClientSigner from @aa-sdk/core. In v5.x.x, a viem LocalAccount or WalletClient is used directly.
Sign the returned signature request(s) using your signer key.
If the account isn't delegated yet (array response): Sign both the EIP-7702 authorization and the user operation. The authorization only needs to be signed once to delegate your account.
Once delegated: Sign only the user operation.
If using an Alchemy Signer, you can learn how to stamp the request on the frontend here. When using the Alchemy Signer, it's easiest to sign the signatureRequest.rawPayload directly.
If you are using a library such as Viem that supports the personal_sign method, you should use that to sign the user operation hash (since the signatureRequest.type is "personal_sign").
With the signature(s) from step 2 and the data from step 1, you're good to send the call!
If the account isn't delegated yet (array response):
Now you can simply call wallet_getCallsStatus to check the status of the calls. A “pending” state (1xx status codes) is expected for some time before the transition to “confirmed,” so this endpoint should be polled while the status is pending. If the call does not progress, refer to the retry guide.