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
- If starting from scratch, follow the quickstart to get your API key
- Existing api keys can be found in the Alchemy Dashboard by viewing your config in the Smart wallets configuration page
- 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.
Fetch (ESM / Node ≥18)
request (CommonJS)
cURL
const const res: Responseres = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>fetch("https://api.g.alchemy.com/signer/v1/signup", {
RequestInit.method?: string | undefinedmethod: "POST",
RequestInit.headers?: HeadersInit | undefinedheaders: {
"Content-Type": "application/json",
// The API key is sent in the Authorization header
type Authorization: stringAuthorization: `Bearer ${var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvThe 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"' && 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 | undefinedALCHEMY_API_KEY}`,
},
RequestInit.body?: BodyInit | undefinedbody: var JSON: JSONAn 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: stringemail: "[email protected]",
}),
});
if (!const res: Responseres.Response.ok: booleanok) throw new var Error: ErrorConstructor
new (message?: string) => ErrorError(await const res: Responseres.BodyMixin.text: () => Promise<string>text());
const { const address: anyaddress, const solanaAddress: anysolanaAddress, const userId: anyuserId } = await const res: Responseres.BodyMixin.json: () => Promise<unknown>json();
var console: ConsoleThe 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[]): voidPrints 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: anyaddress, solanaAddress: anysolanaAddress, userId: anyuserId });
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 predictModularAccountV2AddresspredictModularAccountV2Address } 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: anysmartAccountAddress = import predictModularAccountV2AddresspredictModularAccountV2Address({
factoryAddress: stringfactoryAddress: const mav2FactoryAddress: "0x00000000000017c61b5bEe81050EC8eFc9c6fecd"mav2FactoryAddress,
implementationAddress: stringimplementationAddress,
salt: bigintsalt: 0n, // use 0 unless you have a custom salt scheme
type: stringtype: "SMA",
ownerAddress: anyownerAddress: anyaddress, // signer address from Step 2
});
var console: ConsoleThe 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[]): voidPrints 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: anysmartAccountAddress });
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 AccountpredictMultiOwnerLightAccountAddress
– 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.