Alchemy is sunsetting its in-house AccountKit signer. This guide walks you through replacing @account-kit/react with Openfort embedded wallets while keeping a familiar React + wagmi developer experience.
Note: This migration generates new wallet addresses for your users. Ask users to transfer any assets from their Alchemy smart accounts to their new Openfort wallets after migration.
SDK version: The code here targets
@openfort/reactv2 (latest2.1.3). Version 2 requires wagmi 3, viem 2.52.2 or newer, React 18.3.1 or newer, and Node 20 or newer. It also made@tanstack/react-querya required peer dependency and changed every wallet and auth action to resolve with anerrorfield instead of throwing. If you are on@openfort/reactv1, read the v2 changelog before copying these snippets.
How to Migrate from Alchemy to Openfort?
To migrate from Alchemy Account Kit to Openfort, install the @openfort/react package and its peer dependencies, replace AlchemyAccountProvider with the layered WagmiProvider + OpenfortWagmiBridge + OpenfortProvider stack, and switch your authentication and transaction code from Account Kit hooks (useAuthenticate, useSendUserOperation) to the Openfort headless hooks plus standard wagmi hooks. The Openfort smart account is bonded to the wagmi connector, so once the provider is in place your existing useSendTransaction / useSignMessage / useAccount calls keep working.
Concept mapping
Alchemy Account Kit and Openfort solve the same problem with slightly different vocabulary:
| Alchemy concept | Openfort equivalent |
|---|---|
| Account Kit signer (TEE) | Openfort iframe signer + Shield |
| Light Account / Modular Account v1 / MAv2 / Simple Account | walletConfig.ethereum.accountType (SMART_ACCOUNT, EOA, DELEGATED_ACCOUNT) |
| EIP-7702 delegation | AccountTypeEnum.DELEGATED_ACCOUNT, with use7702Authorization() to sign the authorization |
| Gas Manager policy | walletConfig.ethereum.ethereumFeeSponsorshipId |
@alchemy/wallet-apis (createSmartWalletClient, sendCalls) | wagmi useSendTransaction / useSendCalls (smart account is bonded to the wagmi connector) |
| Session keys via Wallet APIs | useGrantPermissions() / useRevokePermissions() |
Feature mapping
| Alchemy feature | Openfort equivalent | Notes |
|---|---|---|
| Email OTP (Account Kit) | useEmailOtpAuth | One-time code flow |
| Email magic link | useEmailAuth | Email + verification |
| Social login (Google, X, Apple, Discord, Facebook) | useOAuth | Configure providers in dashboard |
| Passkey | RecoveryMethod.PASSKEY in uiConfig.walletRecovery | Passkey is a wallet recovery method, not a sign-in hook. There is no usePasskey |
| External wallets / SIWE | useWalletAuth from @openfort/react/wagmi | Lists connectors, connects and links wallets over SIWE |
| Embedded wallet auto-create | walletConfig.connectOnLogin (defaults to true) | Configured in OpenfortProvider |
| Session keys | useGrantPermissions | ERC-7715-aligned |
1. Install Openfort dependencies
Remove the Alchemy Account Kit packages and install Openfort with its peer dependencies:
_10yarn remove @account-kit/react @account-kit/infra @account-kit/core_10yarn add @openfort/react wagmi@^3 viem@^2.52.2 @tanstack/react-query@^5.99.2
@openfort/react v2 declares these peer ranges, and npm and pnpm will fail the install if you are outside them:
| Peer | Required range |
|---|---|
wagmi | 3.x |
@wagmi/core | 3.x |
viem | >=2.52.2 <3 |
react / react-dom | >=18.3.1 <20 |
@tanstack/react-query | >=5.99.2 <6 |
| Node.js | >=20 |
If you are still on wagmi 2, upgrade wagmi first and get your app compiling against it before you add Openfort. Mixing wagmi 2 with @openfort/react v2 will not work, and debugging both migrations at once is miserable.
If you also use @alchemy/wallet-apis for transactions, you can remove it after migrating to wagmi's useSendTransaction (Openfort smart accounts are bonded to the wagmi connector, so user operations are sent through the same hook).
2. Update provider configuration
Replace the Alchemy provider with Openfort's layered provider structure.
Before (Alchemy):
_22import { AlchemyAccountProvider } from "@account-kit/react";_22import { alchemy, sepolia } from "@account-kit/infra";_22import { createConfig } from "@account-kit/react";_22import { QueryClient, QueryClientProvider } from "@tanstack/react-query";_22_22const config = createConfig({_22 transport: alchemy({ apiKey: "YOUR_ALCHEMY_API_KEY" }),_22 chain: sepolia,_22 ssr: true,_22});_22_22const queryClient = new QueryClient();_22_22function Providers({ children }: { children: React.ReactNode }) {_22 return (_22 <QueryClientProvider client={queryClient}>_22 <AlchemyAccountProvider config={config} queryClient={queryClient}>_22 {children}_22 </AlchemyAccountProvider>_22 </QueryClientProvider>_22 );_22}
After (Openfort):
_47import { OpenfortProvider, RecoveryMethod } from "@openfort/react";_47import { getDefaultConfig, OpenfortWagmiBridge } from "@openfort/react/wagmi";_47import { QueryClient, QueryClientProvider } from "@tanstack/react-query";_47import { WagmiProvider, createConfig } from "wagmi";_47import { mainnet, sepolia } from "viem/chains";_47_47const config = createConfig(_47 getDefaultConfig({_47 appName: "Your App Name",_47 chains: [mainnet, sepolia],_47 walletConnectProjectId: "YOUR_WALLETCONNECT_PROJECT_ID",_47 })_47);_47_47const queryClient = new QueryClient();_47_47const walletConfig = {_47 shieldPublishableKey: "YOUR_SHIELD_PUBLISHABLE_KEY",_47 // Required for AUTOMATIC recovery. Your backend mints the encryption session._47 createEncryptedSessionEndpoint: "YOUR_RECOVERY_ENDPOINT",_47 // Recover or create the embedded wallet right after auth. Default: true._47 connectOnLogin: true,_47 ethereum: {_47 // Your Openfort gas sponsorship policy, the Gas Manager equivalent._47 ethereumFeeSponsorshipId: "YOUR_FEE_SPONSORSHIP_ID",_47 },_47};_47_47function Providers({ children }: { children: React.ReactNode }) {_47 return (_47 <QueryClientProvider client={queryClient}>_47 <WagmiProvider config={config}>_47 <OpenfortWagmiBridge>_47 <OpenfortProvider_47 publishableKey="YOUR_OPENFORT_PUBLISHABLE_KEY"_47 walletConfig={walletConfig}_47 uiConfig={{_47 walletRecovery: { defaultMethod: RecoveryMethod.AUTOMATIC },_47 }}_47 >_47 {children}_47 </OpenfortProvider>_47 </OpenfortWagmiBridge>_47 </WagmiProvider>_47 </QueryClientProvider>_47 );_47}
Three details that trip people up:
- The nesting order is fixed.
OpenfortWagmiBridgereads wagmi's context and hands it toOpenfortProvider, so it has to sit between them. PutOpenfortProvideroutsideWagmiProviderand the bridge finds nothing. walletRecoverybelongs touiConfig, not toOpenfortProviderdirectly. Passing it as a top-level prop is a type error.getDefaultConfigalready setsssr: trueand buildshttp()transports for every chain you list, so you only passssrortransportswhen you want to override them.
In Next.js App Router, this file needs "use client" at the top. OpenfortProvider and every Openfort hook are client-only.
3. Update authentication code
Replace Alchemy authentication hooks with Openfort's headless equivalents. Auth state is read with useUser, sign-out with useSignOut, and individual auth methods are exposed as dedicated hooks (useEmailOtpAuth, useEmailAuth, useOAuth, usePhoneOtpAuth, useGuestAuth).
Before (Alchemy):
_28import { useAuthenticate, useSignerStatus, useUser, useLogout } from "@account-kit/react";_28import { useState } from "react";_28_28function EmailLogin() {_28 const { authenticate, isPending } = useAuthenticate();_28 const { isConnected } = useSignerStatus();_28 const { logout } = useLogout();_28 const user = useUser();_28 const [email, setEmail] = useState("");_28_28 if (isConnected) {_28 return (_28 <div>_28 <p>Welcome, {user?.email}</p>_28 <button onClick={() => logout()}>Logout</button>_28 </div>_28 );_28 }_28_28 return (_28 <div>_28 <input value={email} onChange={(e) => setEmail(e.target.value)} />_28 <button onClick={() => authenticate({ type: "email", email })} disabled={isPending}>_28 {isPending ? "Sending..." : "Send magic link"}_28 </button>_28 </div>_28 );_28}
After (Openfort):
_52import { useUser, useSignOut, useEmailOtpAuth } from "@openfort/react";_52import { useState } from "react";_52_52function EmailLogin() {_52 const { user, isConnected } = useUser();_52 const { signOut } = useSignOut();_52 const { requestEmailOtp, signInEmailOtp, isRequesting, isLoading } = useEmailOtpAuth();_52 const [email, setEmail] = useState("");_52 const [otp, setOtp] = useState("");_52 const [otpSent, setOtpSent] = useState(false);_52 const [error, setError] = useState<string>();_52_52 if (isConnected) {_52 return (_52 <div>_52 <p>Welcome, {user?.email}</p>_52 <button onClick={signOut}>Sign Out</button>_52 </div>_52 );_52 }_52_52 const handleRequestOtp = async () => {_52 const result = await requestEmailOtp({ email });_52 if (result.error) {_52 setError(result.error.shortMessage);_52 return;_52 }_52 setOtpSent(true);_52 };_52_52 const handleSignIn = async () => {_52 const result = await signInEmailOtp({ email, otp });_52 if (result.error) setError(result.error.shortMessage);_52 };_52_52 return (_52 <div>_52 <input value={email} onChange={(e) => setEmail(e.target.value)} />_52 {otpSent && <input value={otp} onChange={(e) => setOtp(e.target.value)} />}_52 {!otpSent ? (_52 <button onClick={handleRequestOtp} disabled={isRequesting}>_52 {isRequesting ? "Sending..." : "Send OTP"}_52 </button>_52 ) : (_52 <button onClick={handleSignIn} disabled={isLoading}>_52 {isLoading ? "Verifying..." : "Verify"}_52 </button>_52 )}_52 {error && <p role="alert">{error}</p>}_52 </div>_52 );_52}
Two behaviours changed in v2 and they bite quietly.
Auth and wallet actions resolve instead of rejecting. requestEmailOtp returns { error?, user?, wallet? }, so a try/catch around it catches nothing and an await followed by a success path advances on failure. Check result.error before moving on.
useUser() gives you both isAuthenticated and isConnected. isAuthenticated only means the user signed in. isConnected means signed in and the embedded wallet is ready to sign, which is what Alchemy's useSignerStatus().isConnected meant. Gate anything that touches the wallet on isConnected.
Alchemy's email auth defaults to magic links; Openfort's useEmailOtpAuth uses one-time codes. If you want a magic-link flow instead, use useEmailAuth.
For OAuth (Google, X, Apple, Discord, Facebook) use useOAuth. For phone-number OTP use usePhoneOtpAuth. For guest sign-in use useGuestAuth. For external wallet sign-in over SIWE use useWalletAuth from @openfort/react/wagmi, which returns availableWallets, connectWallet and linkWallet.
4. Update wallet access code
Alchemy exposes the smart account through useSmartAccountClient. With Openfort, the smart account is bonded to the wagmi connector, so wagmi's useAccount returns its address.
Before (Alchemy):
_10import { useSmartAccountClient } from "@account-kit/react";_10_10function WalletInfo() {_10 const { client, address, isLoadingClient } = useSmartAccountClient({});_10_10 if (isLoadingClient) return <p>Loading wallet...</p>;_10 if (!client) return <p>No wallet</p>;_10_10 return <p>Wallet: {address}</p>;_10}
After (Openfort):
_10import { useAccount } from "wagmi";_10_10function WalletInfo() {_10 const { address, isConnected } = useAccount();_10_10 if (!isConnected) return <p>No wallet</p>;_10 return <p>Wallet: {address}</p>;_10}
useAccount covers reads. When you need embedded-wallet specifics that wagmi does not model, use useEthereumEmbeddedWallet from @openfort/react/ethereum. It exposes the wallet list, the detailed status (connecting, needs-recovery, connected, error), and the create, import, setActive, setRecovery and exportPrivateKey actions:
_10import { useEthereumEmbeddedWallet } from "@openfort/react/ethereum";_10_10function WalletStatus() {_10 const wallet = useEthereumEmbeddedWallet();_10_10 if (wallet.status === "needs-recovery") return <p>Recover your wallet to continue</p>;_10 if (wallet.status !== "connected") return <p>Loading wallet...</p>;_10_10 return <p>Wallet: {wallet.displayAddress}</p>;_10}
5. Update transaction code
Alchemy uses useSendUserOperation (or the lower-level client.sendCalls from @alchemy/wallet-apis) to send user operations. With Openfort the smart account is exposed through the wagmi connector, so you use the standard useSendTransaction.
Before (Alchemy):
_22import { useSendUserOperation, useSmartAccountClient } from "@account-kit/react";_22import { zeroAddress } from "viem";_22_22function SendTransaction() {_22 const { client } = useSmartAccountClient({});_22 const { sendUserOperation, isSendingUserOperation } = useSendUserOperation({_22 client,_22 waitForTxn: true,_22 });_22_22 const send = () => {_22 sendUserOperation({_22 uo: { target: zeroAddress, data: "0x", value: BigInt(0) },_22 });_22 };_22_22 return (_22 <button onClick={send} disabled={isSendingUserOperation || !client}>_22 {isSendingUserOperation ? "Sending..." : "Send Transaction"}_22 </button>_22 );_22}
After (Openfort):
_20import { useSendTransaction, useAccount } from "wagmi";_20import { parseEther } from "viem";_20_20function SendTransaction() {_20 const { address } = useAccount();_20 const { sendTransaction, isPending } = useSendTransaction();_20_20 const send = () => {_20 sendTransaction({_20 to: "0xRecipientAddress",_20 value: parseEther("0.001"),_20 });_20 };_20_20 return (_20 <button onClick={send} disabled={isPending || !address}>_20 {isPending ? "Sending..." : "Send Transaction"}_20 </button>_20 );_20}
Contract writes and reads work the same way, through wagmi's useWriteContract and useReadContract. For batched calls, use wagmi's EIP-5792 useSendCalls hook in place of Alchemy's client.sendCalls.
6. Update message signing
Alchemy's useSignMessage is replaced with wagmi's useSignMessage. The connector signs with the Openfort embedded wallet under the hood.
Before (Alchemy):
_17import { useSignMessage, useSmartAccountClient } from "@account-kit/react";_17_17function SignMessage() {_17 const { client } = useSmartAccountClient({});_17 const { signMessage, isSigningMessage } = useSignMessage({ client });_17_17 const sign = async () => {_17 const signature = await signMessage({ message: "Hello World" });_17 console.log("Signature:", signature);_17 };_17_17 return (_17 <button onClick={sign} disabled={isSigningMessage || !client}>_17 {isSigningMessage ? "Signing..." : "Sign Message"}_17 </button>_17 );_17}
After (Openfort):
_15import { useSignMessage } from "wagmi";_15_15function SignMessage() {_15 const { signMessage, isPending } = useSignMessage();_15_15 const sign = () => {_15 signMessage({ message: "Hello World" });_15 };_15_15 return (_15 <button onClick={sign} disabled={isPending}>_15 {isPending ? "Signing..." : "Sign Message"}_15 </button>_15 );_15}
Watch the import path here. @openfort/react exports its own useSignMessage, a different hook that opens the Openfort confirmation modal, takes the message as a plain string, and resolves to { signature } or { error }. Use the wagmi one for a headless flow and the Openfort one when you want the modal. Importing the wrong one compiles and then fails at the call site.
EIP-712 typed data goes through wagmi's useSignTypedData.
7. Remove Alchemy dependencies
Once everything compiles and your sign-in/sign-out, signing, and transaction flows pass smoke tests, clean up the Alchemy packages:
_10yarn remove @account-kit/react @account-kit/infra @account-kit/core @alchemy/wallet-apis
Hook mapping reference
| Alchemy hook | Openfort / wagmi equivalent |
|---|---|
useSignerStatus().isConnected | useUser().isConnected |
useAuthenticate() | useEmailOtpAuth() / useEmailAuth() / useOAuth() / usePhoneOtpAuth() / useGuestAuth() |
useUser() | useUser().user |
useLogout() | useSignOut().signOut() |
useSmartAccountClient() | useAccount() (wagmi), or useEthereumEmbeddedWallet() from @openfort/react/ethereum |
useSendUserOperation() | useSendTransaction() (wagmi) |
useSignMessage() (Account Kit) | useSignMessage() (wagmi) |
useExportAccount() | useEthereumEmbeddedWallet().exportPrivateKey() |
| Account Kit signer (TEE) | Openfort iframe signer + Shield |
| Session keys via Wallet APIs | useGrantPermissions() / useRevokePermissions() |
Considerations
Embedded wallet creation
Alchemy creates a smart account on first authentication. Openfort does the same when walletConfig.connectOnLogin is true, which is the default. Set it to false if you want to call create() or setActive() yourself, for example to show your own wallet-creation screen.
Gas sponsorship
An Alchemy Gas Manager policy has a direct counterpart. Create a gas sponsorship policy in the Openfort dashboard and pass its ID as walletConfig.ethereum.ethereumFeeSponsorshipId. Sponsored transactions then go through the same wagmi hooks with no call-site changes. Policy rules (which contracts, which limits) are configured in the dashboard, not in code, so plan to port them by hand.
Session handling
Alchemy sessions are not transferable. Users will need to re-authenticate with Openfort after the migration is deployed.
Wallet addresses
Users receive new smart wallet addresses. Alchemy accounts (Light Account, Modular Account v1/v2) are not redeployed at the same address by Openfort. Communicate this change to users before migration so they can:
- Export any assets from their Alchemy smart account.
- Transfer assets to their new Openfort wallet after authentication.
If your application relies on a specific Alchemy account type (e.g. MAv2-only features) that has no clean Openfort equivalent, scope the migration to the flows that map cleanly first and book a call with the Openfort team for guidance on the rest.
EIP-7702 vs ERC-4337
Openfort supports both, and you pick per project with walletConfig.ethereum.accountType:
AccountTypeEnum.SMART_ACCOUNTis the ERC-4337 path, where assets live in a smart account separate from the signer. The wagmi connector builds the user operation, so existing wagmi calls keep working.AccountTypeEnum.DELEGATED_ACCOUNTis the EIP-7702 path, where the signer EOA delegates to a smart account at the same address. Useuse7702Authorization()to sign the delegation. The hook resolves to{ status: "success", authorization }or{ status: "error", error }.
Chain configuration
Both Alchemy and Openfort support multiple chains. Update your chain configuration in the wagmi config:
_10import { getDefaultConfig } from "@openfort/react/wagmi";_10import { mainnet, polygon, arbitrum, base } from "viem/chains";_10import { createConfig } from "wagmi";_10_10const config = createConfig(_10 getDefaultConfig({_10 appName: "Your App",_10 chains: [mainnet, polygon, arbitrum, base],_10 })_10);
With OpenfortWagmiBridge in place, wagmi owns the active chain, so you do not set walletConfig.ethereum.chainId as well. That field is for SDK-only setups with no wagmi.
Recovery methods
Alchemy stores keys in a TEE behind email/social/passkey auth. Openfort uses an iframe-based signer plus Shield and offers three recovery options, set through uiConfig.walletRecovery:
| Recovery method | RecoveryMethod value | Backend required |
|---|---|---|
| Automatic | RecoveryMethod.AUTOMATIC | Yes, an encryption session endpoint |
| Password | RecoveryMethod.PASSWORD | No |
| Passkey | RecoveryMethod.PASSKEY | No |
uiConfig.walletRecovery takes defaultMethod and allowedMethods. Automatic recovery is the closest match to the Alchemy experience, and it needs a backend route that mints the encryption session. The backend quickstart has a working one.
Concepts that don't map 1:1
A few Alchemy concepts have no direct Openfort equivalent. Call these out explicitly to your team during planning:
- Per-account-type behaviour (Light Account, Modular Account v1, MAv2, Simple Account). Openfort exposes account types as
SMART_ACCOUNT,EOAandDELEGATED_ACCOUNTrather than mirroring Alchemy's implementations. If you depend on MAv1- or Light-Account-specific module behaviour, you will need to adapt your contracts or business logic. @alchemy/wallet-apisclient.requestAccount({ creationHint }). Openfort provisions the smart account during authentication; there is no separate "request account" step before sending calls.- In-place key migration via TEE-to-TEE export. Openfort does not import private keys from Alchemy's TEE. Plan for new wallet addresses and an asset transfer step rather than an end-to-end key migration.
Test your application
- Run your app and verify sign-in, sign-out, wallet access, message signing, and transaction flows.
- Confirm there are no remaining
@account-kit/*or@alchemy/wallet-apisimports in your codebase. - Grep for
awaiton Openfort auth and wallet actions that never checkresult.error. Those are the v2 failures that pass silently. - Walk through the migration with a real test user before promoting the change to production.
Next steps
The full Openfort embedded-wallet React reference, including session keys and recovery methods, is in the Openfort docs. A runnable headless example matching this guide is in the openfort-react repo. If you depend on Alchemy features that do not have a clean Openfort equivalent, book a migration call.









