How to Migrate from Alchemy AccountKit

Joan Alavedra, Co-Founder at Openfort•14 min read
Alchemy Account Kit to Openfort React migration guide

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/react v2 (latest 2.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-query a required peer dependency and changed every wallet and auth action to resolve with an error field instead of throwing. If you are on @openfort/react v1, 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 conceptOpenfort equivalent
Account Kit signer (TEE)Openfort iframe signer + Shield
Light Account / Modular Account v1 / MAv2 / Simple AccountwalletConfig.ethereum.accountType (SMART_ACCOUNT, EOA, DELEGATED_ACCOUNT)
EIP-7702 delegationAccountTypeEnum.DELEGATED_ACCOUNT, with use7702Authorization() to sign the authorization
Gas Manager policywalletConfig.ethereum.ethereumFeeSponsorshipId
@alchemy/wallet-apis (createSmartWalletClient, sendCalls)wagmi useSendTransaction / useSendCalls (smart account is bonded to the wagmi connector)
Session keys via Wallet APIsuseGrantPermissions() / useRevokePermissions()

Feature mapping

Alchemy featureOpenfort equivalentNotes
Email OTP (Account Kit)useEmailOtpAuthOne-time code flow
Email magic linkuseEmailAuthEmail + verification
Social login (Google, X, Apple, Discord, Facebook)useOAuthConfigure providers in dashboard
PasskeyRecoveryMethod.PASSKEY in uiConfig.walletRecoveryPasskey is a wallet recovery method, not a sign-in hook. There is no usePasskey
External wallets / SIWEuseWalletAuth from @openfort/react/wagmiLists connectors, connects and links wallets over SIWE
Embedded wallet auto-createwalletConfig.connectOnLogin (defaults to true)Configured in OpenfortProvider
Session keysuseGrantPermissionsERC-7715-aligned

1. Install Openfort dependencies

Remove the Alchemy Account Kit packages and install Openfort with its peer dependencies:


_10
yarn remove @account-kit/react @account-kit/infra @account-kit/core
_10
yarn 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:

PeerRequired range
wagmi3.x
@wagmi/core3.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):


_22
import { AlchemyAccountProvider } from "@account-kit/react";
_22
import { alchemy, sepolia } from "@account-kit/infra";
_22
import { createConfig } from "@account-kit/react";
_22
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
_22
_22
const config = createConfig({
_22
transport: alchemy({ apiKey: "YOUR_ALCHEMY_API_KEY" }),
_22
chain: sepolia,
_22
ssr: true,
_22
});
_22
_22
const queryClient = new QueryClient();
_22
_22
function 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):


_47
import { OpenfortProvider, RecoveryMethod } from "@openfort/react";
_47
import { getDefaultConfig, OpenfortWagmiBridge } from "@openfort/react/wagmi";
_47
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
_47
import { WagmiProvider, createConfig } from "wagmi";
_47
import { mainnet, sepolia } from "viem/chains";
_47
_47
const config = createConfig(
_47
getDefaultConfig({
_47
appName: "Your App Name",
_47
chains: [mainnet, sepolia],
_47
walletConnectProjectId: "YOUR_WALLETCONNECT_PROJECT_ID",
_47
})
_47
);
_47
_47
const queryClient = new QueryClient();
_47
_47
const 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
_47
function 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. OpenfortWagmiBridge reads wagmi's context and hands it to OpenfortProvider, so it has to sit between them. Put OpenfortProvider outside WagmiProvider and the bridge finds nothing.
  • walletRecovery belongs to uiConfig, not to OpenfortProvider directly. Passing it as a top-level prop is a type error.
  • getDefaultConfig already sets ssr: true and builds http() transports for every chain you list, so you only pass ssr or transports when 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):


_28
import { useAuthenticate, useSignerStatus, useUser, useLogout } from "@account-kit/react";
_28
import { useState } from "react";
_28
_28
function 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):


_52
import { useUser, useSignOut, useEmailOtpAuth } from "@openfort/react";
_52
import { useState } from "react";
_52
_52
function 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):


_10
import { useSmartAccountClient } from "@account-kit/react";
_10
_10
function 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):


_10
import { useAccount } from "wagmi";
_10
_10
function 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:


_10
import { useEthereumEmbeddedWallet } from "@openfort/react/ethereum";
_10
_10
function 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):


_22
import { useSendUserOperation, useSmartAccountClient } from "@account-kit/react";
_22
import { zeroAddress } from "viem";
_22
_22
function 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):


_20
import { useSendTransaction, useAccount } from "wagmi";
_20
import { parseEther } from "viem";
_20
_20
function 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):


_17
import { useSignMessage, useSmartAccountClient } from "@account-kit/react";
_17
_17
function 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):


_15
import { useSignMessage } from "wagmi";
_15
_15
function 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:


_10
yarn remove @account-kit/react @account-kit/infra @account-kit/core @alchemy/wallet-apis

Hook mapping reference

Alchemy hookOpenfort / wagmi equivalent
useSignerStatus().isConnecteduseUser().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 APIsuseGrantPermissions() / 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:

  1. Export any assets from their Alchemy smart account.
  2. 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_ACCOUNT is 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_ACCOUNT is the EIP-7702 path, where the signer EOA delegates to a smart account at the same address. Use use7702Authorization() 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:


_10
import { getDefaultConfig } from "@openfort/react/wagmi";
_10
import { mainnet, polygon, arbitrum, base } from "viem/chains";
_10
import { createConfig } from "wagmi";
_10
_10
const 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 methodRecoveryMethod valueBackend required
AutomaticRecoveryMethod.AUTOMATICYes, an encryption session endpoint
PasswordRecoveryMethod.PASSWORDNo
PasskeyRecoveryMethod.PASSKEYNo

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, EOA and DELEGATED_ACCOUNT rather 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-apis client.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-apis imports in your codebase.
  • Grep for await on Openfort auth and wallet actions that never check result.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.

Share this article

Related Articles

  1. How to Migrate from RainbowKit

    This guide provides a step-by-step process to migrate your React app from RainbowKit to Openfort React

  2. How to Migrate from Web3Modal

    This guide provides a step-by-step process to migrate your React app from Web3Modal to Openfort React

  3. Introducing React SDK

    The new React SDK integrates Openfort Kit auth UI elements with dedicated React hooks and TypeScript types for embedded wallets.

  4. How to create a USDC wallet

    In this tutorial, you'll learn how to integrate Openfort smart wallets into a mobile app to provide a usdc compatible wallet for your users.

  5. How to Build a Telegram Bot with Built-In Crypto Wallets

    Build a Telegram bot where /start creates a wallet and /send moves USDC gas-free, using Openfort backend wallets, plus the Mini App path for React.

  6. Introducing Universal Deposit Address

    Universal deposit address: one address per user that auto-swaps any token on any chain into the destination wallet's native asset.

  7. From spikes to stable: eRPC in production at Openfort

    What broke, what we fixed, and what we gained moving our multi-chain RPC from an in-house Elastic stack to eRPC—with hedging, batching, and clean metrics.

  8. Prediction markets for builders: Make “belief” tradable

    Prediction markets for builders: how they work, key design choices (liquidity + resolution), and how embedded wallets make trading and settlement usable.

Ship your first wallet in minutes