Core Quickstart

If you’re not using React, but still want a number of the abstractions that make the React package so easy to use, you can leverage the @account-kit/core package directly.

In this guide, we’ll walk you through how to use this package to send a user operation, while using the reactive utilities exported by this package.

Install packages

Prerequisites

  • minimum Typescript version of 5

Installation

To get started, you’ll need to install the required packages. We also install the infra package because it contains the necessary Chain definitions that makes it easier to setup your a Bundler client.

$yarn add @account-kit/core @account-kit/infra

Get your API keys

  1. Get your API key by creating a new app in your Alchemy Dashboard

    Make sure your desired network is enabled for your app under the Networks tab.

  2. Create a new account config in your Account Kit Dashboard

    1. Apply the config to your app from step 1

      apply your the config to the app from the first step
    2. Enable authentication methods you want to support.


      Email auth

      If you want to use email auth, toggle on email.

      • For testing, use http://localhost:3000 as the Redirect URL (Note http not https)
      • The user will be redirected to the Redirect URL if you use the magic link email flow
      • Optionally stylize ✨ the email with your brand color and logo!
      configure email auth
      Social auth

      If you want to enable social login, toggle which auth providers you want to support.

      • For testing, add http://localhost:3000 as a whitelisted origin

      • Add the link that your dapp will be running on to the whitelisted origin list

      • Optionally enter your own OAuth credentials or use our defaults

        configure social auth
  3. Create the config and copy the API Key

    how to copy the api key

Create a config

Now, you’re ready to create a config. The config we create should be a static object that you can import anywhere into your application. It contains all of the state that the functions within this package use.

import { 
const createConfig: (params: CreateConfigProps) => AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
} from "@account-kit/core";
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: AlchemyAccountsConfig
config
=
function createConfig(params: CreateConfigProps): AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
({
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" }),
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
// optional if you want to sponsor gas
policyId?: string | string[] | undefined
policyId
: "YOUR_POLICY_ID",
});

Authenticate the user

Before you can create a Smart Account instance for your users, you need to authenticate them with the user. Depending on what framework you’re using this will look different, but using email based auth as an example you would:

  1. collect the user’s email
  2. call the authenticate method on the signer
  3. handle the redirect from the user’s email and pass the bundle to the signer to complete login
import { 
import config
config
} from "./config";
import {
const getSigner: <T extends AlchemySigner>(config: AlchemyAccountsConfig) => T | null

If there is a signer attached to the client state, it will return it. The signer should always be null on the server, and will be set on the client if the store was properly hydrated.

getSigner
} from "@account-kit/core";
const
const signer: AlchemySigner | null
signer
=
getSigner<AlchemySigner>(config: AlchemyAccountsConfig): AlchemySigner | null

If there is a signer attached to the client state, it will return it. The signer should always be null on the server, and will be set on the client if the store was properly hydrated.

getSigner
(
import config
config
);
if (!
const signer: AlchemySigner | null
signer
) {
// this can happen if your rendering this on the server // the signer instance is only available on the client throw new
var Error: ErrorConstructor new (message?: string) => Error
Error
("Signer not found");
} // authenticate the user with email await
const signer: AlchemySigner
signer
.
BaseAlchemySigner<TClient extends BaseSignerClient>.authenticate: (params: AuthParams) => Promise<User>

Authenticate a user with either an email or a passkey and create a session for that user

authenticate
({
type: "email"
type
: "email",
email: string
email
: "[email protected]",
}); // once the user has clicked on the email and been redirected back to your site const
const bundle: string | null
bundle
= new
var URLSearchParams: new (init?: string[][] | Record<string, string> | string | URLSearchParams) => URLSearchParams

MDN Reference

URLSearchParams class is a global reference for import URLSearchParams from 'node:url' https://nodejs.org/api/url.html#class-urlsearchparams

URLSearchParams
(
var window: Window & typeof globalThis
window
.
location: Location
location
.
Location.search: string

Returns the Location object's URL's query (includes leading "?" if non-empty).

Can be set, to navigate to the same URL with a changed query (ignores leading "?").

MDN Reference

search
).
URLSearchParams.get(name: string): string | null

Returns the first value associated to the given search parameter.

MDN Reference

get
("bundle");
if (!
const bundle: string | null
bundle
) {
throw new
var Error: ErrorConstructor new (message?: string) => Error
Error
("No bundle found in URL");
} await
const signer: AlchemySigner
signer
.
BaseAlchemySigner<TClient extends BaseSignerClient>.authenticate: (params: AuthParams) => Promise<User>

Authenticate a user with either an email or a passkey and create a session for that user

authenticate
({
type: "email"
type
: "email",
bundle: string
bundle
});
import { 
const createConfig: (params: CreateConfigProps) => AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
} from "@account-kit/core";
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: AlchemyAccountsConfig
config
=
function createConfig(params: CreateConfigProps): AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
({
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" }),
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
// optional if you want to sponsor gas
policyId?: string | string[] | undefined
policyId
: "YOUR_POLICY_ID",
});

Send a user operation

Now that you have your config, you can send user operations by leveraging the underlying smart account client.

import { 
function watchSmartAccountClient<TAccount extends SupportedAccountTypes, TChain extends Chain | undefined = Chain | undefined>(params: GetSmartAccountClientParams<TChain, TAccount>, config: AlchemyAccountsConfig): (onChange: (client: GetSmartAccountClientResult<TChain, SupportedAccount<TAccount>>) => void) => () => void

Watches for changes to the smart account client and triggers the provided callback when a change is detected.

watchSmartAccountClient
} from "@account-kit/core";
import {
import config
config
} from "./config";
let
let clientState: any
clientState
;
// The watch smart account client will handle all of the possible state changes // that can impact this client: // - Signer status // - Account instantiation // - Chain changes const
const clientSubscription: () => void
clientSubscription
=
watchSmartAccountClient<"LightAccount", Chain | undefined>(params: GetSmartAccountClientParams<Chain | undefined, "LightAccount">, config: AlchemyAccountsConfig): (onChange: (client: GetSmartAccountClientResult<...>) => void) => () => void

Watches for changes to the smart account client and triggers the provided callback when a change is detected.

watchSmartAccountClient
(
{
type: "LightAccount"
type
: "LightAccount",
},
import config
config
,
)((
clientState_: GetSmartAccountClientResult<Chain | undefined, LightAccount<AlchemySigner>>
clientState_
) => {
let clientState: any
clientState
=
clientState_: GetSmartAccountClientResult<Chain | undefined, LightAccount<AlchemySigner>>
clientState_
;
}); if (
let clientState: undefined
clientState
== null ||
let clientState: never
clientState
.
any
isLoadingClient
) {
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
("Loading...");
} const
const client: any
client
=
let clientState: undefined
clientState
.
any
client
;
await
const client: any
client
.
any
sendUserOperation
({
uo: { target: string; data: string; value: bigint; }
uo
: {
target: string
target
: "0xtarget",
data: string
data
: "0x",
value: bigint
value
: 0n,
}, });
import { 
const createConfig: (params: CreateConfigProps) => AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
} from "@account-kit/core";
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: AlchemyAccountsConfig
config
=
function createConfig(params: CreateConfigProps): AlchemyAccountsConfig

Creates an AlchemyAccountsConfig object that can be used in conjunction with the actions exported from @account-kit/core.

The config contains core and client stores that can be used to manage account state in your application.

createConfig
({
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" }),
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
// optional if you want to sponsor gas
policyId?: string | string[] | undefined
policyId
: "YOUR_POLICY_ID",
});

The key thing here is the watchSmartAccountClient method which allows you to subscribe to the state of signer and underlying account to give you a stable instance of the Smart Account Client. How you store the clientState variable will depend largely on your framework, but the above example should give you a good starting point.