OverviewRecipes

How to programmatically create a wallet

This recipe shows how to programmatically create a smart wallet: you’ll generate a signer, spin up a Smart Wallet Client, read the counterfactual address before deployment and deploy the wallet by sending your first gas sponsored UserOperation (prepared → signed → sent).

1

Install required dependencies

You’ll need the following packages for this recipe:

  • @account-kit/signer: Server wallet signer and access key generation
  • @account-kit/infra: Alchemy transport & chain constants (e.g., arbitrumSepolia)
  • @account-kit/wallet-client: Smart Wallet Client (prepare/sign/send flows)
  • dotenv: Read .env for your API key & policy id
  • typescript + tsx + @types/node: Run TypeScript files directly
$npm i @account-kit/signer @account-kit/infra @account-kit/wallet-client dotenv
>npm i -D typescript tsx @types/node
2

Environment variables

Create a .env in your project root:

$# Get your app API key: https://dashboard.alchemy.com/apps
>ALCHEMY_API_KEY=YOUR_API_KEY
># Get your gas sponsorship policy ID: https://dashboard.alchemy.com/services/gas-manager/configuration
>ALCHEMY_POLICY_ID=YOUR_POLICY_ID
># Generate a secure access key for server wallet authentication - see step 3 below
>ACCESS_KEY=your-secure-access-key-here
3

Generate an access key

Server wallets enable backend applications to programmatically control wallets using access keys, without requiring interactive authentication. This is perfect for automated systems, batch operations, or when you need to sign transactions from your backend.

How server wallets work:

  • You generate a secure access key that never leaves your server
  • Alchemy derives a public key from your access key for authentication
  • The access key is used to sign transactions and messages on behalf of users
  • No private keys are stored or transmitted to Alchemy’s servers
1import { generateAccessKey } from "@account-kit/signer";
2
3// Generate a secure access key (do this once and store securely)
4const accessKey = generateAccessKey();
5console.log("Access key:", accessKey);
6
7// Add it to your .env file - you'll need it to control the server signer

Critical: Save your access key securely!

This access key is required to control your server wallet and cannot be recovered if lost. Make sure to store it in a secure location.

4

Create a server signer

1import { createServerSigner } from "@account-kit/signer";
2
3const signer = await createServerSigner({
4 auth: {
5 accessKey: process.env.ACCESS_KEY!,
6 },
7 connection: {
8 apiKey: process.env.ALCHEMY_API_KEY!,
9 },
10});
11
12console.log("Signer address:", await signer.getAddress());
5

Create the Smart Wallet Client

1import { createSmartWalletClient } from "@account-kit/wallet-client";
2import { alchemy, arbitrumSepolia } from "@account-kit/infra";
3
4const transport = alchemy({ apiKey: process.env.ALCHEMY_API_KEY! });
5
6const client = createSmartWalletClient({
7 transport,
8 chain: arbitrumSepolia,
9 signer,
10});
6

Get the counterfactual address

The counterfactual address is the account address associated with the given signer but the account contract hasn’t been deployed yet.

1const account = await client.requestAccount();
2const address = account.address;
3console.log("Counterfactual address:", address);
7

Prepare → sign → send a sponsored call

Use the capabilities pipeline with paymasterService to sponsor gas via your policy and deploy the account contract by sending a gas sponsored UserOperation.

1const prepared = await client.prepareCalls({
2 from: address,
3 calls: [
4 {
5 to: "0x0000000000000000000000000000000000000000",
6 data: "0x",
7 value: "0x0",
8 },
9 ], // minimal call to deploy the account contract
10 capabilities: {
11 paymasterService: { policyId: process.env.ALCHEMY_POLICY_ID! },
12 },
13});
14
15const signed = await client.signPreparedCalls(prepared);
16const sent = await client.sendPreparedCalls(signed);
17
18const txHash = await client.waitForCallsStatus({ id: sent.preparedCallIds[0] });
19console.log("Prepared call IDs:", sent.preparedCallIds);
20console.log("Tx hash:", txHash);

For non-sponsored path, remove the paymasterService capability in prepareCalls and fund the account to pay gas. The rest of the flow is unchanged.

Full script (copy-paste)

provision.ts
1import "dotenv/config";
2import { createServerSigner } from "@account-kit/signer";
3import { createSmartWalletClient } from "@account-kit/wallet-client";
4import { alchemy, arbitrumSepolia } from "@account-kit/infra";
5
6const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY!;
7const ALCHEMY_POLICY_ID = process.env.ALCHEMY_POLICY_ID!;
8const ACCESS_KEY = process.env.ACCESS_KEY!;
9
10if (!ALCHEMY_API_KEY || !ALCHEMY_POLICY_ID || !ACCESS_KEY) {
11 throw new Error("Missing ALCHEMY_API_KEY, ALCHEMY_POLICY_ID, or ACCESS_KEY in env");
12}
13
14async function main() {
15 // 1) Create server signer using access key
16 const signer = await createServerSigner({
17 auth: {
18 accessKey: ACCESS_KEY,
19 },
20 connection: {
21 apiKey: ALCHEMY_API_KEY,
22 },
23 });
24
25 // 2) Transport + Smart Wallet Client
26 const transport = alchemy({ apiKey: ALCHEMY_API_KEY });
27 const client = createSmartWalletClient({
28 transport,
29 chain: arbitrumSepolia,
30 signer,
31 });
32
33 // 3) Account & counterfactual address
34 const account = await client.requestAccount();
35 const address = account.address;
36 console.log("Counterfactual address:", address);
37
38 // 4) Prepare → sign → send (sponsored) to trigger deployment
39 const prepared = await client.prepareCalls({
40 from: address,
41 calls: [
42 {
43 to: "0x0000000000000000000000000000000000000000",
44 data: "0x",
45 value: "0x0",
46 },
47 ], // minimal call to deploy the account contract
48 capabilities: {
49 paymasterService: { policyId: ALCHEMY_POLICY_ID },
50 },
51 });
52
53 const signed = await client.signPreparedCalls(prepared);
54 const sent = await client.sendPreparedCalls(signed);
55
56 // 5) Wait for inclusion → tx hash
57 const txHash = await client.waitForCallsStatus({
58 id: sent.preparedCallIds[0],
59 });
60
61 console.log("Prepared call IDs:", sent.preparedCallIds);
62 console.log("Tx hash:", txHash);
63
64 return { address, preparedCallIds: sent.preparedCallIds, txHash };
65}
66
67main().then(
68 (res) => {
69 console.log("✅ Wallet provisioned:", res);
70 process.exit(0);
71 },
72 (err) => {
73 console.error("❌ Failed:", err);
74 process.exit(1);
75 },
76);
8

Run it

$# one-time per project
>npm i @account-kit/signer @account-kit/infra @account-kit/wallet-client dotenv
>npm i -D typescript tsx @types/node
>
># then run
>npx tsx provision.ts

You should see:

  • Counterfactual address: 0x…
  • Prepared call IDs: [...]
  • Tx hash: 0x… (open on an Arbitrum Sepolia explorer to see the deployment)

Example:

$Counterfactual address: 0x022fA5358476f0EB881138BD822E5150AFb36c5B
>Prepared call IDs: [
> '0x0000000000000000000000000000000000000000000000000000000000066eee7d4670e76c7186cf2fb9d448e3b8348e316bdb5b142c3ff3149aa703805c81cb'
>]
>Tx hash: {
> id: '0x0000000000000000000000000000000000000000000000000000000000066eee7d4670e76c7186cf2fb9d448e3b8348e316bdb5b142c3ff3149aa703805c81cb',
> status: 'success',
> atomic: true,
> chainId: 421614,
> receipts: [
> {
> status: 'success',
> blockHash: '0x180071dc55dbcf7d31dd2fbe1c1a58e3cb5564d0b59c465c4f558236340cecd3',
> blockNumber: 196845735n,
> gasUsed: 208770n,
> transactionHash: '0x2e625854abcb557bc2bc02e97d2f3deaae544a2aface000fbcf1c3573fee1526',
> logs: [Array]
> }
> ],
> statusCode: 200,
> version: '2.0.0'
>}
>✅ Wallet provisioned: {
> address: '0x022fA5358476f0EB881138BD822E5150AFb36c5B',
> preparedCallIds: [
> '0x0000000000000000000000000000000000000000000000000000000000066eee7d4670e76c7186cf2fb9d448e3b8348e316bdb5b142c3ff3149aa703805c81cb'
> ],
> txHash: {
> id: '0x0000000000000000000000000000000000000000000000000000000000066eee7d4670e76c7186cf2fb9d448e3b8348e316bdb5b142c3ff3149aa703805c81cb',
> status: 'success',
> atomic: true,
> chainId: 421614,
> receipts: [ [Object] ],
> statusCode: 200,
> version: '2.0.0'
> }
>}

And when opened in an Arbitrum Sepolia explorer, you should see the deployment, congrats you have just learned how to programmatically create a smart wallet 🎉