Pregenerate Wallets

Objective Generate a wallet owner (signer) and the smart-account address before a user ever logs in, using only their email.

Prerequisites

  • Alchemy API Key with Smart Wallets enabled
  • Node ≥ 18 (or cURL / any HTTP client)
  • The @account-kit/smart-contracts package (for step 4)

You can generate a wallet address on both EVM and Solana before a user ever authenticates or logs in using only their email. This could enable use cases such as:

  • airdropping assets or seed balances to users who signed up for a waitlist with their email before they ever open your app
  • allowing users to send assets to another user with just their email even if the recipient has not logged in before. When that recipient logs in, they’ll be able to claim their balance.
  • migrating assets to smart wallets to enable gas sponsorship, batching, etc.
  • and more!

With Alchemy you can do this entirely from your server using the Signer API – no UI, hooks, or user interaction required.

Note: Smart contract addresses are deterministic. The deployment address is a function of the address of owner/signer address, the account implementation (e.g. latest version of Modular Account), and the salt (optionally specified). If all three of those remain the same, then the smart account will be consistent across chains and for users. We will handle generating a consistent signer address for email.

1. Create the owner signer

First, you need to generate the signer address. This will be generated using the Create Wallet API endpoint. The signer address will be used as the owner of the new smart account and enable the user to sign transactions after authenticating with their email.

const 
const res: Response
res
= await
function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
fetch
("https://api.g.alchemy.com/signer/v1/signup", {
RequestInit.method?: string | undefined
method
: "POST",
RequestInit.headers?: HeadersInit | undefined
headers
: {
"Content-Type": "application/json", // The API key is sent in the Authorization header
type Authorization: string
Authorization
: `Bearer ${
var process: NodeJS.Process
process
.
NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. See environ(7).

An example of this object looks like:

jsTERM: 'xterm-256color', SHELL: '/usr/local/bin/bash', USER: 'maciej', PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', PWD: '/Users/maciej', EDITOR: 'vim', SHLVL: '1', HOME: '/Users/maciej', LOGNAME: 'maciej', _: '/usr/local/bin/node'

It is possible to modify this object, but such modifications will not be reflected outside the Node.js process, or (unless explicitly requested) to other Worker threads. In other words, the following example would not work:

bash node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo

While the following will:


env.foo = 'bar'; console.log(env.foo); ```

Assigning a property on `process.env` will implicitly convert the value to a string. **This behavior is deprecated.** Future versions of Node.js may throw an error when the value is not a string, number, or boolean.

```js import env from 'node:process';

env.test = null; console.log(env.test); // => 'null' env.test = undefined; console.log(env.test); // => 'undefined' ```

Use `delete` to delete a property from `process.env`.

```js import env from 'node:process';

env.TEST = 1; delete env.TEST; console.log(env.TEST); // => undefined ```

On Windows operating systems, environment variables are case-insensitive.

```js import env from 'node:process';

env.TEST = 1; console.log(env.test); // => 1 ```

Unless explicitly specified when creating a `Worker` instance, each `Worker` thread has its own copy of `process.env`, based on its parent thread's `process.env`, or whatever was specified as the `env` option to the `Worker` constructor. Changes to `process.env` will not be visible across `Worker` threads, and only the main thread can make changes that are visible to the operating system or to native add-ons. On Windows, a copy of `process.env` on a `Worker` instance operates in a case-sensitive manner unlike the main thread.
env
.
string | undefined
ALCHEMY_API_KEY
}`,
},
RequestInit.body?: BodyInit | undefined
body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

stringify
({
email: string
email
: "[email protected]",
}), }); if (!
const res: Response
res
.
Response.ok: boolean
ok
) throw new
var Error: ErrorConstructor new (message?: string) => Error
Error
(await
const res: Response
res
.
BodyMixin.text: () => Promise<string>
text
());
const {
const address: any
address
,
const solanaAddress: any
solanaAddress
,
const userId: any
userId
} = await
const res: Response
res
.
BodyMixin.json: () => Promise<unknown>
json
();
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

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
({
address: any
address
,
solanaAddress: any
solanaAddress
,
userId: any
userId
});
FieldDescription
addressThe signer (EOA) address that will own your user’s smart account (this is not the smart account itself).
solanaAddressThe Solana wallet address associated with the same user.
orgIdYour organisation ID – useful if you operate multiple orgs.

The address you receive here is the owner key. Your user’s Smart Contract Account (ModularAccountV2) will be deployed at a different address – but we can deterministically calculate it ahead of time using predictModularAccountV2Address.

2. Predict the smart-account address

Using the signer address you generated in step 2, you can now generate the counter-factual (future) address of the smart account before it is even deployed.

The predictModularAccountV2Address method will generate the address for a new Modular Account v2 and mirrors the CREATE2 logic used by the MAv2 factory.

import { 
import predictModularAccountV2Address
predictModularAccountV2Address
} from "@account-kit/smart-contracts";
// Factory & implementation addresses vary by chain – you can grab them from // the Deployed addresses page. // https://www.alchemy.com/docs/wallets/smart-contracts/deployed-addresses const
const mav2FactoryAddress: "0x00000000000017c61b5bEe81050EC8eFc9c6fecd"
mav2FactoryAddress
= "0x00000000000017c61b5bEe81050EC8eFc9c6fecd";
const
const implementationAddress: "0x000000000000c5A9089039570Dd36455b5C07383"
implementationAddress
= "0x000000000000c5A9089039570Dd36455b5C07383";
const
const smartAccountAddress: any
smartAccountAddress
=
import predictModularAccountV2Address
predictModularAccountV2Address
({
factoryAddress: string
factoryAddress
:
const mav2FactoryAddress: "0x00000000000017c61b5bEe81050EC8eFc9c6fecd"
mav2FactoryAddress
,
implementationAddress: string
implementationAddress
,
salt: bigint
salt
: 0n, // use 0 unless you have a custom salt scheme
type: string
type
: "SMA",
ownerAddress: any
ownerAddress
:
any
address
, // signer address from Step 2
});
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

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
({
smartAccountAddress: any
smartAccountAddress
});

Now you have both the owner key (address) and the eventual smart account address (smartAccountAddress). When the user finally authenticates, their smart account will be deployed at exactly this counter-factual address.

If your application is still using the older Light Account contracts (v1 or v2) instead of Modular Account V2, use the corresponding helpers from @account-kit/smart-contracts:

  • predictLightAccountAddress – single-owner Light Account
  • predictMultiOwnerLightAccountAddress – multi-owner Light Account

These functions take the same factory/owner/salt pattern shown above and work for all historical Light-Account factory versions. Everything else in this guide (steps 1-3) remains the same.


That’s it! You now hold the future Smart Contract Account address. You can fund it immediately or store it for later onboarding.