Wallets API Quickstart

Learn to interact with Wallet APIs using any RPC client!

This guide outlines exactly what you need to use Wallet Server with your app! We’ll go over the wallet_requestAccount, wallet_createSession, wallet_prepareCalls, and wallet_sendPreparedCalls endpoints in this tutorial.

The logical flow is to get an account for a given signer, create a session for use with that account, prepare the calls you’re looking to send, and send them! If you’re looking for a typescript SDK guide, you’ll find that here too.

Session keys are useful if you are looking to grant your app’s server partial, secure access to user’s wallets so that the server can sign and send transactions on a user’s behalf within certain permissions.

In API calls, it’s as simple as:

If instead, you don’t want to use session keys and you want the user to sign all of the transactions, you can simply skip the createSession step and use the client side signer to sign prepared calls.

This guide assumes you have an account you can sign with, like an Alchemy Signer. You will also need an Alchemy API key and a gas manager policy ID.

Don't have an API key?

Start using the Alchemy Wallets API today! Get started for free

Using The Account Kit SDK

Install Prerequisities

You’re going to need the @account-kit/wallet-client, @account-kit/infra, and optionally @aa-sdk/core if you use a LocalAccountSigner.

$npm install @account-kit/wallet-client @account-kit/infra @aa-sdk/core

Create A Smart Account Client

Given a signer (e.g. a LocalAccountSigner imported from @aa-sdk/core or an Alchemy Signer), all you need to do is follow a few simple steps to start sending user ops with Wallet APIs!

1import { createSmartWalletClient } from "@account-kit/wallet-client";
2import { alchemy, arbitrumSepolia } from "@account-kit/infra";
3import { LocalAccountSigner } from "@aa-sdk/core";
4
5const signer = LocalAccountSigner.fromPrivateKey(PRIVATE_KEY); // we use a private key signer as an example here
6
7const transport = alchemy({
8 apiKey: ALCHEMY_API_KEY, // use your Alchemy app api key here!
9});
10
11const client = createSmartWalletClient({
12 transport,
13 chain: arbitrumSepolia, // use any chain imported from @account-kit/infra here!
14 mode: "remote",
15 signer,
16});

Request The Account

A 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();
2
3// get the address
4const address = account.address;

Sign A Message

1import { createPublicClient } from "viem";
2
3const message = "we are so back"
4
5const signature = await client.signMessage({ message });
6
7const publicClient = createPublicClient({
8 chain: arbitrumSepolia,
9 transport: transport,
10});
11
12const isValid = await publicClient.verifyMessage({
13 address: account.address, // fetched from await client.requestAccount()
14 message,
15 signature,
16});

Sign Typed Data

1// assuming you have a typedData variable
2const signature = await client.signTypedData({typedData});
3
4const isValid = await publicClient.verifyTypedData({
5 address: account.address, // fetched from await client.requestAccount()
6 ...typedData,
7 signature,
8});

Send A UserOp

1import { zeroAddress } from "viem";
2
3const account = await client.requestAccount();
4
5const { signatureRequest, ...preparedUO } = await client.prepareCalls({
6 calls: [{ to: zeroAddress, value: "0x0" }], // callData is optional in a "data" parameter
7 from: account.address,
8 // "capabilities" is a data structure that hold gas manager data (as seen below) or permission data
9 capabilities: {
10 paymasterService: {
11 policyId: GAS_MANAGER_POLICY_ID, // put your gas manager policy ID here
12 },
13 },
14});
15
16// Sign the userOp
17const signature = await client.signSignatureRequest(signatureRequest);
18
19// Send the userOp
20const result = await client.sendPreparedCalls({
21 ...preparedUO,
22 signature,
23});

Create A Session With Permissions

Session keys allow another account to operate on a user’s smart account with given permissions. See here for a list of permissions!

1const account = await client.requestAccount();
2
3// This is where you would use your session key signer!
4const sessionKey = LocalAccountSigner.generatePrivateKeySigner();
5
6const permissions = await client.grantPermissions({
7 account: account.address,
8 expiry: Math.floor(Date.now() / 1000) + 60 * 60,
9 key: {
10 publicKey: await sessionKey.getAddress(),
11 type: "secp256k1",
12 },
13 permissions: [{ type: "root" }], // Here we grant root permissions as an example, but this is not advised in production!
14});
15
16const { signatureRequest, ...preparedUO } = await client.prepareCalls({
17 calls: [{ to: zeroAddress, value: "0x0" }],
18 from: account.address,
19 capabilities: {
20 paymasterService: {
21 policyId: GAS_MANAGER_POLICY_ID, // put your gas manager policy ID here
22 },
23 permissions,
24 },
25});
26
27const signature = await signSignatureRequest(
28 sessionKey, // Note that we now sign with the session key!
29 signatureRequest,
30);
31
32const result = await client.sendPreparedCalls({
33 ...preparedUO,
34 signature,
35 capabilities: {
36 permissions,
37 },
38});

Using The JSON-RPC APIs Directly

1. Request an Account for the Owner Signer

Given an owner address, call wallet_requestAccount to return the smart account address for that owner. The owner address can be any signer (or public key) that has the ability to sign transactions.

  • If you want to use social sign up / log in, you can simply use the SDK to authenticate user’s and retrieve their signer address
  • If instead, you want to generate and control wallets with a custodied owner, you can generate any public private key pair (e.g. any EOA)

This will return the account address associated with the given signer, as well as a uuid you could use to differentiate between accounts for the same signer in the future.

$curl --request POST \
> --url https://api.g.alchemy.com/v2/API_KEY \
> --header 'accept: application/json' \
> --header 'content-type: application/json' \
> --data '
>{
> "id": 1,
> "jsonrpc": "2.0",
> "method": "wallet_requestAccount",
> "params": [
> {
> "signerAddress": "0xOWNER_ADDRESS"
> }
> ]
>}
>'

This will return the smart account address associated with the given signer:

${
> "jsonrpc": "2.0",
> "id": 1,
> "result": {
> "accountAddress": "0xACCOUNT_ADDRESS",
> "id": "af638-a8..."
> }
>}

2. Create a Session With the Session Key Signer

Session keys are useful if you are looking to define on-chain policies or grant your app’s server partial, secure access to user’s wallets so that the server can sign and send transactions on a user’s behalf within certain permissions. If you don’t want to use session keys for server side signing, you can skip to step 4.

After creating a session, you will be able to sign transactions for the generated wallet within the defined permissioned using that session key.

To create a session key using onchain policies:

  • Get the public address of a key you want to use as a session key. This can be any key pair that has the ability to sign (aka a signer that is either an local signer like an EOA or signer generated with a signer provider)
  • Create a session for that key using your session key public address - here passed in publicKey call wallet_createSession.

Note that the expiry is in seconds and represents a UNIX timestamp. (E.g. 1776657600 for April 20th, 2077)

$curl --request POST \
> --url https://api.g.alchemy.com/v2/API_KEY \
> --header 'accept: application/json' \
> --header 'content-type: application/json' \
> --data '
>{
> "jsonrpc": "2.0",
> "id": 1,
> "method": "wallet_createSession",
> "params": [
> {
> "account": "0xACCOUNT_ADDRESS",
> "chainId": "0xCHAIN_ID",
> "expiry": UNIX_TIMESTAMP_EXPIRY_IN_SECONDS,
> "key": {
> "publicKey": "0xSESSION_KEY_ADDRESS",
> "type": "secp256k1"
> },
> "permissions": [
> {
> "type": "root"
> }
> ]
> }
> ]
>}'

This will return two key elements:

  1. The session ID
  2. The signature request you’ve got to sign to authorize the session key

Keep note of the session ID, you’ll need it later!

${
> "jsonrpc": "2.0",
> "id": 1,
> "result": {
> "sessionId": "0xSESSION_ID",
> "signatureRequest": {
> "type": "eth_signTypedData_v4",
> "data": {...userOperation}
> }
> }
>}

3. Sign the Session Key Authorization

Sign the signature request from the owner key (used in step 1), then store the resulting signature.

4. Prepare Calls With the Session Key

With the session ID received in step 2 and the signature from step 3, we’re now ready to prepare some calls!

If you aren’t using a session key, you can omit the “permissions” parameter in the capabilities object.

$curl --request POST \
> --url https://api.g.alchemy.com/v2/API_KEY \
> --header 'accept: application/json' \
> --header 'content-type: application/json' \
> --data '
>{
> "id": 1,
> "jsonrpc": "2.0",
> "method": "wallet_prepareCalls",
> "params": [
> {
> "capabilities": {
> "paymasterService": {
> "policyId": GAS_MANAGER_POLICY_ID, // put your gas manager policy ID here
> },
> "permissions": {
> "sessionId": 0xSESSION_ID,
> "signature": 0xPERMISSION_SIG,
> }
> },
> "calls": [
> {
> "to": "0x0000000000000000000000000000000000000000"
> }
> ],
> "from": "0xACCOUNT_ADDRESS",
> "chainId": "0xCHAIN_ID"
> }
> ]
>}
>'

This will return the userop request (the data field) and a signature request, for example:

$type: "user-operation-v070",
>data: {...useropRequest},
>chainId: "0xCHAIN_ID"
>signatureRequest: {
> type: "personal_sign",
> data: {
> raw: HASH_TO_SIGN,
> }
>}

5. Sign the userop

With the returned signature request, all you have to do is sign the userop hash returned in the signatureRequest.data.raw field from step 4.

  • If not using session keys, you’ll sign this with the owner of the account (from step 1). You can learn how to stamp the request on the frontend here.
  • If using session keys, you can sign this with the session key that was added in step 2 and 3. This signature will be valid as long as it is within the permissions the session key has.

Note that the type field in the signatureRequest indicates the signature type needed, in this case, we need to personal_sign the hash.

6. Send the Prepared Calls!

With the signature from step 5 and the useropRequest from step 4, you’re good to go to send the call!

If you are signing with the owner of the account and not a session key, you can omit the entire capabilities object.

$curl --request POST \
> --url https://api.g.alchemy.com/v2/API_KEY \
> --header 'accept: application/json' \
> --header 'content-type: application/json' \
> --data '
>{
> "id": 1,
> "jsonrpc": "2.0",
> "method": "wallet_sendPreparedCalls",
> "params": [
> {
> "type": "user-operation-v070",
> "data": {...useropRequest},
> "chainId": "0xCHAIN_ID" // E.g. "0x66eee" for Arbitrum Sepolia
> "capabilities": {
> "permissions": {
> "sessionId": 0xSESSION_ID,
> "signature": 0xPERMISSION_SIG,
> }
> }
> "signature": {
> "type": "ecdsa"
> "signature": "0xUSEROP_SIGNATURE",
> }
> }
> ]
>}
>'

This will return the array of prepared call IDs! These are concatenations of the chainID padded to 32 bytes and the userOp Hash.

Permission Types

To set specific permissions on a session key installation, add an object to the permissions array when calling client.grantPermission() via the SDK or when calling wallet_createSession via APIs directly.

1const permissions = await client.grantPermissions({
2 account: account.address,
3 expiry: Math.floor(Date.now() / 1000) + 60 * 60,
4 key: {
5 publicKey: await sessionKey.getAddress(),
6 type: "secp256k1",
7 },
8 permissions: [{PERMISSION_ONE}, {PERMISSION_TWO}],
9});
10

Native Token Transfer

This permission allows transfer of native tokens (like Ether) from the account.

1{
2 type: "native-token-transfer";
3 data: {
4 allowance: Hex; // a hexadecimal encoded transfer limit, for example, 1 ETH would be 0xde0b6b3a7640000 (1e18 in hex)
5 };
6}

ERC20 Token Transfer

This permission allows transfer or approval of erc20 tokens from the account. Both transfers and approvals count towards the limit.

1{
2 type: "erc20-token-transfer";
3 data: {
4 address: Address; // erc20 token contract address
5 allowance: Hex; // a hexadecimal encoded transfer limit
6 };
7}

Gas Limit

This permission allows the session key to spend gas for user operations up to a specified limit.

1{
2 type: "gas-limit";
3 data: {
4 limit: Hex; // a hexadecimal encoded gas limit, for example 300000 gas would be 0x493e0
5 };
6}

Contract Access

This permission grants access to all functions in a specific contract.

1{
2 type: "contract-access";
3 data: {
4 address: Address; // the target contract’s address
5 };
6}

Account Functions

This permission grants access to specific functions on the smart account itself.

1{
2 type: "account-functions";
3 data: {
4 functions: Hex[]; // array of allowed function selectors, e.g. ["0xabcdef01", "0x12345678"]
5 };
6}

Functions On All Contracts

This permission grants access to a set of function selectors across any address.

1{
2 type: "functions-on-all-contracts";
3 data: {
4 functions: Hex[]; // array of function selectors allowed globally, e.g. ["0xddf252ad"]
5 };
6}

Functions On Contract

This permission grants access to specific function selectors on one contract.

1{
2 type: "functions-on-contract";
3 data: {
4 address: Address; // the contract address you’re targeting
5 functions: Hex[]; // array of allowed function selectors for that contract, e.g. ["0xddf252ad"]
6 };
7}

Root

This permission grants full access to everything. Needless to say, this is a very dangerous permission to grant.

1{
2 type: "root"; // no additional data required
3}