Privy lets users create wallets through email or social login. This guide shows you how to connect Privy wallets to Biconomy using the AbstractJS SDK for gasless, cross-chain transactions.
What We’re Building
By the end of this tutorial, your users will be able to:
- Log in with email (no seed phrase)
- Execute transactions without holding ETH for gas
- Perform cross-chain operations with one signature
How it works: When a user logs in with Privy, they get an embedded wallet. We upgrade this wallet to a smart account using EIP-7702, then route transactions through Biconomy’s MEE to handle gas payments.
Step 1: Install Dependencies
npm install @privy-io/react-auth @biconomy/abstractjs viem
What these packages do:
@privy-io/react-auth — Privy’s React SDK for wallet creation and auth
@biconomy/abstractjs — Biconomy’s SDK for smart accounts and MEE
viem — Ethereum library for building transactions
Wrap your app with Privy’s provider. The key settings are:
import { PrivyProvider } from "@privy-io/react-auth";
function App({ children }) {
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
config={{
// Allow email login
loginMethods: ["email"],
embeddedWallets: {
// Auto-create wallet on first login
createOnLogin: true,
// Don't prompt for every signature (better UX)
noPromptOnSignature: true,
},
}}
>
{children}
</PrivyProvider>
);
}
Step 3: Access the Embedded Wallet
After a user logs in, get their wallet from Privy:
import { useWallets, useSignAuthorization } from "@privy-io/react-auth";
import { createWalletClient, custom } from "viem";
import { optimism } from "viem/chains";
function usePrivyWallet() {
const { wallets } = useWallets();
const { signAuthorization } = useSignAuthorization();
// The embedded wallet is typically the first one
const embeddedWallet = wallets?.[0];
return { embeddedWallet, signAuthorization };
}
Step 4: Create a Wallet Client
The wallet client is how you’ll sign transactions:
async function getWalletClient(embeddedWallet) {
// Switch to your target chain
await embeddedWallet.switchChain(optimism.id);
// Get the Ethereum provider from Privy
const provider = await embeddedWallet.getEthereumProvider();
// Create a viem wallet client
const walletClient = createWalletClient({
account: embeddedWallet.address,
chain: optimism,
transport: custom(provider),
});
return walletClient;
}
Step 5: Sign EIP-7702 Authorization
This step upgrades the EOA to a smart account:
// The Nexus smart account implementation address
const NEXUS_IMPLEMENTATION = "0x000000004F43C49e93C970E84001853a70923B03";
// Sign the authorization to upgrade the wallet
const authorization = await signAuthorization({
contractAddress: NEXUS_IMPLEMENTATION,
chainId: 0, // 0 = works on any chain
});
What just happened? The user signed a message that says “I authorize my wallet to behave like this smart contract.” Their address stays the same, but now it has smart account capabilities.
Step 6: Create the Smart Account
Now create a Nexus account that can work across multiple chains:
import {
toMultichainNexusAccount,
getMEEVersion,
MEEVersion
} from "@biconomy/abstractjs";
import { optimism, base } from "viem/chains";
import { http } from "viem";
const nexusAccount = await toMultichainNexusAccount({
chainConfigurations: [
{
chain: optimism,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
// Use the original wallet address (EIP-7702 mode)
accountAddress: embeddedWallet.address,
},
{
chain: base,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
accountAddress: embeddedWallet.address,
},
],
signer: walletClient.account,
});
Step 7: Create the MEE Client
The MEE client handles transaction execution:
import { createMeeClient } from "@biconomy/abstractjs";
const meeClient = await createMeeClient({
account: nexusAccount
});
Step 8: Execute a Gasless Transaction
Now you can execute transactions where gas is paid in any token:
import { erc20Abi } from "viem";
const USDC_ADDRESS = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"; // USDC on Optimism
const { hash } = await meeClient.execute({
// Required for EIP-7702 flow
authorization,
delegate: true,
// Pay gas fees with USDC instead of ETH
feeToken: {
address: USDC_ADDRESS,
chainId: optimism.id,
},
// Your transaction instructions
instructions: [
{
chainId: optimism.id,
calls: [
{
to: "0xRecipientAddress",
value: 0n,
data: "0x", // or encoded function data
},
],
},
],
});
// Wait for confirmation
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
console.log("Transaction complete:", receipt.hash);
Complete Example
Here’s a full React component putting it all together:
import { useState } from "react";
import { useWallets, useSignAuthorization } from "@privy-io/react-auth";
import { createWalletClient, custom, http, erc20Abi } from "viem";
import { optimism, base } from "viem/chains";
import {
createMeeClient,
toMultichainNexusAccount,
getMEEVersion,
MEEVersion,
} from "@biconomy/abstractjs";
const NEXUS_IMPLEMENTATION = "0x000000004F43C49e93C970E84001853a70923B03";
const USDC_OPTIMISM = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85";
export function GaslessTransfer() {
const { wallets } = useWallets();
const { signAuthorization } = useSignAuthorization();
const [status, setStatus] = useState("");
async function sendGaslessTransaction() {
try {
setStatus("Setting up...");
const embeddedWallet = wallets?.[0];
if (!embeddedWallet) throw new Error("Please log in first");
// Create wallet client
await embeddedWallet.switchChain(optimism.id);
const provider = await embeddedWallet.getEthereumProvider();
const walletClient = createWalletClient({
account: embeddedWallet.address,
chain: optimism,
transport: custom(provider),
});
setStatus("Signing authorization...");
// Sign EIP-7702 authorization
const authorization = await signAuthorization({
contractAddress: NEXUS_IMPLEMENTATION,
chainId: 0,
});
setStatus("Creating smart account...");
// Create multichain account
const nexusAccount = await toMultichainNexusAccount({
chainConfigurations: [
{
chain: optimism,
transport: http(),
version: getMEEVersion(MEEVersion.V2_1_0),
accountAddress: embeddedWallet.address,
},
],
signer: walletClient.account,
});
// Create MEE client
const meeClient = await createMeeClient({ account: nexusAccount });
setStatus("Executing transaction...");
// Execute gasless transaction
const { hash } = await meeClient.execute({
authorization,
delegate: true,
feeToken: {
address: USDC_OPTIMISM,
chainId: optimism.id,
},
instructions: [
{
chainId: optimism.id,
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: 0n,
},
],
},
],
});
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
setStatus(`Success! Hash: ${receipt.hash}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
}
return (
<div>
<button onClick={sendGaslessTransaction}>
Send Gasless Transaction
</button>
<p>{status}</p>
</div>
);
}
Key Takeaways
- Privy creates the wallet — Users log in with email, Privy creates an embedded wallet
- EIP-7702 adds smart features — The wallet gets upgraded without changing address
- MEE handles gas — Transactions are executed with any ERC-20 token for gas
- Cross-chain ready — Add more chains to
chainConfigurations for multi-chain ops
Next Steps