Connect with EOAs

You can allow users to login by connecting their existing EOA using Smart Wallets. This can be done using our pre-built UI components or custom UI - no need for a separate wallet connector! Depending on what method you choose, you can bring in your EOA:

  • As a “pure” EOA: users connect and transact directly from their EOAs. These wallets don’t support smart account features like gas sponsorship or batching.
  • As an owner and signer of a new smart account: users will maintain self-custody and sign with their EOA for each transaction. They will not be able to use funds from the EOA directly, but their new smart wallet have AA features like sponsorship and batching.

Pre-built Components

Easily log in into your existing EOAs using popular wallet providers like MetaMask, Rabby, Phantom, and more. Note: Since this is not a smart account, users will not get sponsorship, batching, or other features, and instead they will use their EOA as normal.

Our SDK supports external wallet connection out of the box, so you can seamlessly support both Web2 users with social login and Web3 users who want to bring their own EOAs.We support enabling users to bring their existing EOA in 2 ways:

  1. Automatically detect and display all wallet extensions available in the user’s browser. (Note: if the user has no wallet extensions installed, nothing will be displayed)
  2. WalletConnect for all other EOAs

With Smart Wallets, users can still connect to their existing wallets without sacrificing compatibility or convenience.

Our hooks will handle checking if the connected wallet is a smart wallet or an existing EOA. Transactions can be signed and sent in the same way, but external EOA wallets don’t have access to Account Abstraction benefits such as gas abstraction, batching, etc. These EOAs will simply function as EOAs.

In order to access these external wallet providers, just make sure to include configure type: "external_wallets" in your config:

import { 
const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
,
const cookieStorage: (config?: { sessionLength?: number; domain?: string; }) => Storage

Function to create cookie based Storage

cookieStorage
} from "@account-kit/react";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const sepolia: Chain
sepolia
} from "@account-kit/infra";
export const
const config: AlchemyAccountsConfigWithUI
config
=
function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
(
{ // alchemy config
transport: AlchemyTransport
transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "your_api_key" }), // TODO: add your Alchemy API key - setup your app and embedded account config in the alchemy dashboard (https://dashboard.alchemy.com/accounts)
chain: Chain
chain
:
const sepolia: Chain
sepolia
, // TODO: specify your preferred chain here and update imports from @account-kit/infra
ssr?: boolean | undefined

Enable this parameter if you are using the config in an SSR setting (eg. NextJS) Turing this setting on will disable automatic hydration of the client store

ssr
: true, // Defers hydration of the account state to the client after the initial mount solving any inconsistencies between server and client state (read more here: https://accountkit.alchemy.com/react/ssr)
storage?: CreateStorageFn | undefined
storage
:
const cookieStorage: (config?: { sessionLength?: number; domain?: string; }) => Storage

Function to create cookie based Storage

cookieStorage
, // persist the account state using cookies (read more here: https://accountkit.alchemy.com/react/ssr#persisting-the-account-state)
enablePopupOauth: true
enablePopupOauth
: true, // must be set to "true" if you plan on using popup rather than redirect in the social login flow
// optional config to override default session manager config
sessionConfig?: ({ storage?: Storage | "localStorage" | "sessionStorage" | undefined; sessionKey?: string | undefined; expirationTimeMs?: number | undefined; } & { domain?: string; }) | undefined
sessionConfig
: {
expirationTimeMs?: number | undefined
expirationTimeMs
: 1000 * 60 * 60, // 60 minutes (default is 15 min)
}, }, { // authentication ui config - your customizations here
auth?: { addPasskeyOnSignup?: boolean; header?: React.ReactNode; hideError?: boolean; onAuthSuccess?: () => void; sections: AuthType[][]; hideSignInText?: boolean; } | undefined
auth
: {
sections: AuthType[][]

Each section can contain multiple auth types which will be grouped together and separated by an OR divider

sections
: [
[{
type: "email"
type
: "email" }],
[ {
type: "passkey"
type
: "passkey" },
{
type: "social"
type
: "social",
authProviderId: KnownAuthProvider
authProviderId
: "google",
mode: "popup"
mode
: "popup" },
{
type: "social"
type
: "social",
authProviderId: KnownAuthProvider
authProviderId
: "facebook",
mode: "popup"
mode
: "popup" },
], [ {
type: "external_wallets"
type
: "external_wallets",
walletConnect?: { isNewChainsStale?: boolean | undefined; client?: SignClient | undefined; storage?: IKeyValueStorage | undefined; projectId: string; metadata?: Metadata | undefined; ... 13 more ...; showQrModal?: boolean | undefined; } | undefined
walletConnect
: {
projectId: string
projectId
: "your-project-id" },
}, ], ],
addPasskeyOnSignup?: boolean | undefined

If this is true, then auth components will prompt users to add a passkey after signing in for the first time

addPasskeyOnSignup
: true,
showSignInText: boolean
showSignInText
: true,
}, }, );

In order to access other providers via WalletConnect, also add walletConnect: { projectId: "your-project-id" } to your config. You create a WalletConnect id here.

[
  {
    
type: string
type
: "external_wallets",
walletConnect: { projectId: string; }
walletConnect
: {
projectId: string
projectId
: "your-project-id" },
}, ];

Custom UI

Bring in an EOA via Connectors

If you need complete control over the user experience, you can customize your EOA connection. For example, use the useConnect hook to allow users to connect their EOA via available connectors:

import { 
const useConnect: (params?: UseConnectParameters<Config>["mutation"]) => UseConnectReturnType<Config>

Re-exported wagmi hook for connecting an EOA. This hook uses the internal wagmi config though so that the state is in sync with the rest of the Alchemy Account hook state. Useful if you wnat to connect to an EOA.

useConnect
} from "@account-kit/react";
const {
const connectors: readonly Connector<CreateConnectorFn>[]
connectors
,
const connect: ConnectMutate<Config, unknown>
connect
} =
function useConnect(params?: UseConnectParameters<Config>["mutation"]): UseConnectReturnType<Config>

Re-exported wagmi hook for connecting an EOA. This hook uses the internal wagmi config though so that the state is in sync with the rest of the Alchemy Account hook state. Useful if you wnat to connect to an EOA.

useConnect
({
onSuccess?: ((data: ConnectData<Config>, variables: { chainId?: number | undefined; connector: CreateConnectorFn | Connector<CreateConnectorFn>; }, context: unknown) => Promise<unknown> | unknown) | undefined
onSuccess
: (
data: ConnectData<Config>
data
) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout

See util.format() for more information.

log
("Connected!",
data: ConnectData<Config>
data
);
},
onError?: ((error: ConnectErrorType, variables: { chainId?: number | undefined; connector: CreateConnectorFn | Connector<CreateConnectorFn>; }, context: unknown) => Promise<unknown> | unknown) | undefined
onError
: (
err: ConnectErrorType
err
) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const code = 5; console.error('error #%d', code); // Prints: error #5, to stderr console.error('error', code); // Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

error
("Connection failed",
err: ConnectErrorType
err
);
}, }); return ( <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
{
const connectors: readonly Connector<CreateConnectorFn>[]
connectors
.
ReadonlyArray<Connector<CreateConnectorFn>>.map<JSX.Element>(callbackfn: (value: Connector<CreateConnectorFn>, index: number, array: readonly Connector<CreateConnectorFn>[]) => JSX.Element, thisArg?: any): JSX.Element[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

map
((
connector: Connector<CreateConnectorFn>
connector
) => (
<
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
React.Attributes.key?: React.Key | null | undefined
key
={
connector: Connector<CreateConnectorFn>
connector
.
id: string
id
}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick
={() =>
const connect: <Connector<CreateConnectorFn>>(variables: ConnectVariables<Config, Connector<CreateConnectorFn>>, options?: { ...; } | undefined) => void
connect
({
connector: CreateConnectorFn | Connector<CreateConnectorFn>
connector
})}>
Connect with {
connector: Connector<CreateConnectorFn>
connector
.
name: string
name
}
</
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>
))} </
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
);

Bring in an EOA as a Smart Wallet Owner

For local wallets or JSON-RPC wallets that support the EIP-1193 standard (like MetaMask, Coinbase Wallet, etc.), you can use WalletClientSigner from @aa-sdk/core to bring in these EOAs as your smart wallet owner. More info here. By making your EOA an owner of a smart account, you will have access to AA feature through your new smart wallet.

import { 
class WalletClientSigner

Represents a wallet client signer for smart accounts, providing methods to get the address, sign messages, and sign typed data.

WalletClientSigner
, type
interface SmartAccountSigner<Inner = any>

A signer that can sign messages and typed data.

SmartAccountSigner
} from "@aa-sdk/core";
import {
function createWalletClient<transport extends Transport, chain extends Chain | undefined = undefined, accountOrAddress extends Account | Address | undefined = undefined, rpcSchema extends RpcSchema | undefined = undefined>(parameters: WalletClientConfig<transport, chain, accountOrAddress, rpcSchema>): WalletClient<transport, chain, ParseAccount<accountOrAddress>, rpcSchema>

Creates a Wallet Client with a given Transport configured for a Chain.

Docs: https://viem.sh/docs/clients/wallet

A Wallet Client is an interface to interact with Ethereum Account(s) and provides the ability to retrieve accounts, execute transactions, sign messages, etc. through Wallet Actions.

The Wallet Client supports signing over: JSON-RPC Accounts (e.g. Browser Extension Wallets, WalletConnect, etc). Local Accounts (e.g. private key/mnemonic wallets).

createWalletClient
,
function custom<provider extends EthereumProvider>(provider: provider, config?: CustomTransportConfig): CustomTransport
custom
} from "viem";
import {
const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; ... 11 more ...; serializers?: ChainSerializers<...> | undefined; }
sepolia
} from "viem/chains";
import {
function createModularAccountV2<TTransport extends Transport = Transport, TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(config: CreateModularAccountV2Params<TTransport, TSigner>): Promise<ModularAccountV2<TSigner>>
createModularAccountV2
} from "@account-kit/smart-contracts";
const
const externalProvider: any
externalProvider
=
any
window
.
any
ethereum
; // or another EIP-1193 provider
const
const walletClient: { account: undefined; batch?: { multicall?: boolean | Prettify<MulticallBatchOptions> | undefined; } | undefined; ... 33 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
walletClient
=
createWalletClient<CustomTransport, { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; ... 11 more ...; serializers?: ChainSerializers<...> | undefined; }, undefined, undefined>(parameters: { ...; }): { ...; }

Creates a Wallet Client with a given Transport configured for a Chain.

Docs: https://viem.sh/docs/clients/wallet

A Wallet Client is an interface to interact with Ethereum Account(s) and provides the ability to retrieve accounts, execute transactions, sign messages, etc. through Wallet Actions.

The Wallet Client supports signing over: JSON-RPC Accounts (e.g. Browser Extension Wallets, WalletConnect, etc). Local Accounts (e.g. private key/mnemonic wallets).

createWalletClient
({
chain?: Chain | { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; ... 11 more ...; serializers?: ChainSerializers<...> | undefined; } | undefined

Chain for the client.

chain
:
const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; ... 11 more ...; serializers?: ChainSerializers<...> | undefined; }
sepolia
,
transport: CustomTransport

The RPC transport

transport
:
custom<any>(provider: any, config?: CustomTransportConfig): CustomTransport
custom
(
const externalProvider: any
externalProvider
),
}); export const
const signer: SmartAccountSigner<any>
signer
:
interface SmartAccountSigner<Inner = any>

A signer that can sign messages and typed data.

SmartAccountSigner
= new
new WalletClientSigner(client: WalletClient, signerType: string): WalletClientSigner

Initializes a signer with a given wallet client and signer type.

WalletClientSigner
(
const walletClient: { account: undefined; batch?: { multicall?: boolean | Prettify<MulticallBatchOptions> | undefined; } | undefined; ... 33 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
walletClient
,
"json-rpc", ); // Connect your signer to your smart account const
const account: ModularAccountV2<SmartAccountSigner<any>>
account
= await
createModularAccountV2<any, SmartAccountSigner<any>>(config: CreateModularAccountV2Params<any, SmartAccountSigner<any>>): Promise<...>
createModularAccountV2
({
chain: Chain
chain
:
const sepolia: { blockExplorers: { readonly default: { readonly name: "Etherscan"; readonly url: "https://sepolia.etherscan.io"; readonly apiUrl: "https://api-sepolia.etherscan.io/api"; }; }; ... 11 more ...; serializers?: ChainSerializers<...> | undefined; }
sepolia
,
transport: any
transport
:
any
alchemyTransport
,
signer: SmartAccountSigner<any>
signer
:
const signer: SmartAccountSigner<any>
signer
, // your EOA that you've brought in as an owner
});