Documentation Index
Fetch the complete documentation index at: https://base-a060aa97-meyer9-move-azul-to-top-of-upgrades.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Integrate Base Account with CDP Embedded Wallets
Learn how to build an onchain app that seamlessly supports both existing Base Account users and new users through CDP Embedded Wallets, providing unified authentication and wallet management.
Overview
This integration enables your app to serve two distinct user types:
- Existing Base users: Connect with their Base Account for a familiar experience
- New onchain users: Create CDP Embedded Wallets via email, mobile, or social authentication
Both user types get the same app functionality while using their preferred wallet type.
What you’ll build
- Unified authentication flow: Single sign-in supporting both wallet types
- Automatic wallet detection: Smart routing based on user’s existing wallet status
- Consistent user experience: Both wallet types access the same app features
Prerequisites
- Node.js 18+ installed
- React application (Next.js recommended)
- CDP Portal account with Project ID
- Basic familiarity with Wagmi and React hooks
Installation
Install the required packages for both CDP Embedded Wallets and Base Account support:
npm install @coinbase/cdp-core @coinbase/cdp-hooks @base-org/account @tanstack/react-query viem wagmi
Step-by-step implementation
Since native CDP + Base Account integration is under development, this guide uses a dual connector approach where both wallet types are supported through separate, coordinated connectors.
You can use the Base Account Wagmi connector alongside CDP’s React provider system to create a unified experience that properly handles wallet persistence for both wallet types.
Step 1: Environment configuration
Create environment variables for your CDP project:
# .env.local
NEXT_PUBLIC_CDP_PROJECT_ID=your_cdp_project_id
NEXT_PUBLIC_APP_NAME="Your App Name"
Get your CDP Project ID from the CDP Portal.
⚠️ Critical: Without a valid NEXT_PUBLIC_CDP_PROJECT_ID, the app will fail to load with “Project ID is required” errors. Also configure your domain in CDP Portal → Wallets → Embedded Wallet settings for CORS.
Set up Wagmi with the Base Account connector (embedded wallets will be handled separately via CDP React providers):
// config/wagmi.ts
import { createConfig, http } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { baseAccount } from 'wagmi/connectors';
// Base Account connector
const baseAccountConnector = baseAccount({
appName: process.env.NEXT_PUBLIC_APP_NAME || 'Your App',
});
// Wagmi config (only for Base Account - embedded wallets handled by CDP React providers)
export const wagmiConfig = createConfig({
connectors: [baseAccountConnector],
chains: [baseSepolia, base], // Put baseSepolia first for testing
transports: {
[base.id]: http(),
[baseSepolia.id]: http(),
},
});
Step 3: Set up application providers
Wrap your application with the necessary providers. Important: Use CDPHooksProvider to properly manage embedded wallet authentication state:
// app/layout.tsx
'use client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CDPHooksProvider } from '@coinbase/cdp-hooks';
import { wagmiConfig } from '../config/wagmi';
const queryClient = new QueryClient();
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<CDPHooksProvider
config={{
projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID!,
}}
>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
</CDPHooksProvider>
</body>
</html>
);
}
Step 4: Create unified authentication hook
Build a custom hook to manage both wallet types. Using CDPHooksProvider ensures users get their existing embedded wallets when they sign in again, rather than creating new ones each time.
// hooks/useUnifiedAuth.ts
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { useSignInWithEmail, useVerifyEmailOTP, useIsSignedIn, useEvmAddress, useSignOut } from '@coinbase/cdp-hooks';
import { useState, useEffect } from 'react';
export type WalletType = 'base_account' | 'embedded' | 'none';
export function useUnifiedAuth() {
// Wagmi hooks for Base Account
const { address: wagmiAddress, isConnected: wagmiConnected, connector } = useAccount();
const { connect, connectors } = useConnect();
const { disconnect: wagmiDisconnect } = useDisconnect();
// CDP hooks for embedded wallet - these work with CDPHooksProvider
const { signInWithEmail, isLoading: isSigningIn } = useSignInWithEmail();
const { verifyEmailOTP, isLoading: isVerifying } = useVerifyEmailOTP();
const { isSignedIn: cdpSignedIn } = useIsSignedIn();
const { evmAddress: cdpAddress } = useEvmAddress();
const { signOut } = useSignOut();
const [walletType, setWalletType] = useState<WalletType>('none');
const [flowId, setFlowId] = useState<string>('');
// Determine which wallet is active and prioritize the active one
const address = wagmiConnected ? wagmiAddress : cdpAddress;
const isConnected = wagmiConnected || cdpSignedIn;
useEffect(() => {
if (wagmiConnected && connector?.name === 'Base Account') {
setWalletType('base_account');
} else if (cdpSignedIn && cdpAddress) {
setWalletType('embedded');
} else {
setWalletType('none');
}
}, [wagmiConnected, cdpSignedIn, connector, cdpAddress]);
const connectBaseAccount = () => {
const baseConnector = connectors.find(c => c.name === 'Base Account');
if (baseConnector) {
connect({ connector: baseConnector });
}
};
const signInWithEmbeddedWallet = async (email: string) => {
try {
const response = await signInWithEmail({ email });
// Capture flowId for OTP verification
if (response && typeof response === 'object' && 'flowId' in response) {
setFlowId(response.flowId as string);
}
return true;
} catch (error) {
console.error('Failed to sign in with email:', error);
return false;
}
};
const verifyOtpAndConnect = async (otp: string) => {
try {
// With CDPReactProvider, verifyEmailOTP automatically signs the user in
await verifyEmailOTP({ flowId, otp });
return true;
} catch (error) {
console.error('Failed to verify OTP:', error);
return false;
}
};
const disconnect = async () => {
if (wagmiConnected) {
wagmiDisconnect();
}
if (cdpSignedIn || walletType === 'embedded') {
try {
await signOut();
} catch (error) {
console.error('CDP sign out failed:', error);
}
}
};
return {
address,
isConnected,
walletType,
connectBaseAccount,
signInWithEmbeddedWallet,
verifyOtpAndConnect,
disconnect,
isSigningIn,
isVerifying,
};
}
Step 5: Build authentication component
Create a component that presents both authentication options:
// components/WalletAuthButton.tsx
'use client';
import { useState } from 'react';
import { useUnifiedAuth } from '../hooks/useUnifiedAuth';
export function WalletAuthButton() {
const {
address,
isConnected,
walletType,
connectBaseAccount,
signInWithEmbeddedWallet,
verifyOtpAndConnect,
disconnect,
isSigningIn,
isVerifying,
} = useUnifiedAuth();
const [authStep, setAuthStep] = useState<'select' | 'email' | 'otp'>('select');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState('');
// Connected state
if (isConnected && address) {
const walletDisplay = {
base_account: { name: 'Base Account', icon: '🟦' },
embedded: { name: 'Embedded Wallet', icon: '📱' },
}[walletType] || { name: 'Connected', icon: '✅' };
return (
<div className="flex items-center space-x-3 px-4 py-2 bg-green-50 border border-green-200 rounded-lg">
<span>{walletDisplay.icon}</span>
<div>
<div className="font-medium text-green-800">{walletDisplay.name}</div>
<div className="text-xs text-green-600 font-mono">
{address.slice(0, 6)}...{address.slice(-4)}
</div>
</div>
<button onClick={() => disconnect()} className="text-sm text-red-600">
Disconnect
</button>
</div>
);
}
// OTP verification
if (authStep === 'otp') {
return (
<div className="space-y-4 p-4 border rounded-lg">
<div className="text-center">
<h3 className="font-semibold">Check your email</h3>
<p className="text-sm text-gray-600">Enter the code sent to {email}</p>
</div>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
placeholder="000000"
maxLength={6}
className="w-full px-3 py-2 border rounded text-center font-mono"
/>
<div className="space-y-2">
<button
onClick={() => verifyOtpAndConnect(otp)}
disabled={otp.length !== 6 || isVerifying}
className="w-full px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isVerifying ? 'Creating account...' : 'Verify & create account'}
</button>
<button
onClick={() => setAuthStep('email')}
className="w-full px-4 py-2 text-gray-600 hover:text-gray-800"
>
Back
</button>
</div>
</div>
);
}
// Email input
if (authStep === 'email') {
return (
<div className="space-y-4 p-4 border rounded-lg">
<h3 className="font-semibold text-center">Create account</h3>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
className="w-full px-3 py-2 border rounded"
/>
<div className="space-y-2">
<button
onClick={async () => {
const success = await signInWithEmbeddedWallet(email);
if (success) setAuthStep('otp');
}}
disabled={!email || isSigningIn}
className="w-full px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isSigningIn ? 'Sending Code...' : 'Send Verification Code'}
</button>
<button
onClick={() => setAuthStep('select')}
className="w-full px-4 py-2 text-gray-600 hover:text-gray-800"
>
Back
</button>
</div>
</div>
);
}
// Initial selection
return (
<div className="space-y-3">
<h2 className="text-xl font-bold text-center mb-4">Connect Your Wallet</h2>
<button
onClick={connectBaseAccount}
className="w-full p-4 border-2 border-blue-200 rounded-lg hover:bg-blue-50"
>
<div className="flex items-center space-x-3">
<span className="text-2xl">🟦</span>
<div className="text-left">
<div className="font-semibold">Sign in with Base</div>
<div className="text-sm text-gray-600">I have a Base Account</div>
</div>
</div>
</button>
<button
onClick={() => setAuthStep('email')}
className="w-full p-4 border-2 border-gray-200 rounded-lg hover:bg-gray-50"
>
<div className="flex items-center space-x-3">
<span className="text-2xl">📱</span>
<div className="text-left">
<div className="font-semibold">Create new account</div>
<div className="text-sm text-gray-600">Use email to get started</div>
</div>
</div>
</button>
</div>
);
}
Step 6: Handle transactions for each wallet type
Create a transaction component that adapts to each wallet type:
// components/SendTransaction.tsx
import { useState } from 'react';
import { parseEther } from 'viem';
import { useSendTransaction, useWaitForTransactionReceipt, useAccount, useSwitchChain } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { useUnifiedAuth } from '../hooks/useUnifiedAuth';
export function SendTransaction() {
const { address, walletType } = useUnifiedAuth();
const { chain } = useAccount();
const { switchChain } = useSwitchChain();
const [amount, setAmount] = useState('');
const [recipient, setRecipient] = useState('');
const { data: hash, sendTransaction, isPending, error } = useSendTransaction();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
const handleTransaction = async () => {
if (!address || !amount || !recipient) return;
try {
sendTransaction({
to: recipient as `0x${string}`,
value: parseEther(amount),
});
} catch (error) {
console.error('Transaction failed:', error);
}
};
// Show different guidance based on wallet type
const getTransactionGuidance = () => {
switch (walletType) {
case 'base_account':
return {
title: 'Base Account Transaction',
description: 'You\'ll be prompted to confirm with your passkey',
icon: '🔐'
};
case 'embedded':
return {
title: 'Embedded Wallet Transaction',
description: 'Transaction will be signed automatically',
icon: '⚡'
};
default:
return { title: 'Send Transaction', description: '', icon: '💸' };
}
};
const guidance = getTransactionGuidance();
if (!address) return null;
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<div className="text-center mb-6">
<div className="text-3xl mb-2">{guidance.icon}</div>
<h3 className="text-lg font-bold">{guidance.title}</h3>
<p className="text-sm text-gray-600">{guidance.description}</p>
{/* Network indicator and switch */}
<div className="mt-3 p-2 bg-gray-50 rounded border">
<div className="flex items-center justify-between">
<span className="text-sm">
Network: <strong>{chain?.name || 'Unknown'}</strong>
</span>
<div className="space-x-1">
{chain?.id !== baseSepolia.id && (
<button
onClick={() => switchChain({ chainId: baseSepolia.id })}
className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
→ Sepolia
</button>
)}
{chain?.id !== base.id && (
<button
onClick={() => switchChain({ chainId: base.id })}
className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200"
>
→ Mainnet
</button>
)}
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Amount (ETH)</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.001"
step="0.001"
className="w-full px-3 py-2 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">To Address</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
className="w-full px-3 py-2 border border-gray-300 rounded font-mono text-sm"
/>
</div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded">
<p className="text-sm text-red-800">Error: {error.message}</p>
</div>
)}
<button
onClick={handleTransaction}
disabled={!amount || !recipient || isPending || isConfirming}
className="w-full px-4 py-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isPending || isConfirming ? 'Processing...' : 'Send Transaction'}
</button>
{isSuccess && hash && (
<div className="p-3 bg-green-50 border border-green-200 rounded text-center">
<p className="text-green-800 font-medium mb-2">✅ Transaction Confirmed!</p>
<a
href={`https://${chain?.id === baseSepolia.id ? 'sepolia.' : ''}basescan.org/tx/${hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm underline"
>
View on {chain?.id === baseSepolia.id ? 'Sepolia ' : ''}Basescan →
</a>
</div>
)}
</div>
</div>
);
}
Step 7: Complete your app
Put everything together in your main application:
// app/page.tsx
'use client';
import { WalletAuthButton } from '../components/WalletAuthButton';
import { SendTransaction } from '../components/SendTransaction';
import { useAccount } from 'wagmi';
export default function HomePage() {
const { isConnected } = useAccount();
return (
<div className="min-h-screen bg-gray-50 py-12 px-4">
<div className="max-w-2xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold mb-2">CDP + Base Account Demo</h1>
<p className="text-gray-600">
One app supporting both Base Account and embedded wallet users
</p>
</div>
<div className="space-y-6">
<WalletAuthButton />
{isConnected && <SendTransaction />}
</div>
</div>
</div>
);
}
Troubleshooting
Common Issues
Base Account connector not appearing
- Verify the Base Account SDK,
@base-org/account, is installed and up-to-date
- Check wagmi configuration includes Base Account connector
- Ensure app is running on Base or Base Sepolia network
CDP Embedded Wallet authentication failing
- Verify CDP Project ID is correct in environment variables
- Critical: Add your domains (e.g.,
http://localhost:3000, http://localhost:3001) to CDP Portal → Wallets → Embedded Wallet settings → Allowed domains
- Ensure all required CDP packages (see above) are installed
New wallet created each time instead of signing into existing wallet
- Ensure you’re using
CDPHooksProvider with proper config in your layout
- Verify CDP Project ID is correctly configured
- Check that hooks are imported from
@coinbase/cdp-hooks consistently
Users can’t switch between wallet types
- Implement proper disconnect flow before connecting different type
- Clear any cached authentication state when switching
- Provide clear UI guidance for wallet type selection
Enhanced integration coming soon
We are actively working on native Base Account integration with CDP Embedded Wallets that will enable:
- Unified connector: Single CDP connector to handle both wallet types seamlessly
- Spend permissions: Sub Accounts will be able to access parent Base Account balance with limits
- Sub Account creation: Base Account users will be able to create app-specific Sub Accounts
Resources
Monitor the CDP documentation for updates on enhanced Embedded Wallet Base Account integration features.