EIP-7702 Wallet API Quickstart

Learn how to send user ops using EIP-7702

This short guide will introduce how to prepare and send user operations using a 7702 account in a matter of minutes. We’ll demonstrate how to do it using the SDK client or by using platform-agnostic JSON-RPC APIs.

The logical flow is to request an account, prepare the calls you’re looking to send, and send them!

In API calls, it’s as simple as:

This guide assumes you have an account you can sign with, like an Alchemy Signer or a user’s EOA. You will also need an Alchemy API key, and a gas manager policy ID if you want to sponsor gas.

Don't have an API key?

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

Using The TypeScript SDK

1. Install Prerequisites

You’re going to need the @account-kit/wallet-client and @account-kit/infra. We’ll also be using LocalAccountSigner from @aa-sdk/core as the signer for demonstration purposes.

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

2. Create A Smart Wallet Client

Create a client for a given signer (e.g. a LocalAccountSigner imported from @aa-sdk/core or an Alchemy Signer).

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});

3. Send A Sponsored User Op

All you need to do is follow a few simple steps to start sending user ops with Wallet APIs!

1// Request the account using the "7702" account type.
2const account = await client.requestAccount({
3 creationHint: {
4 accountType: "7702"
5 }
6});
7
8// Prepare the calls.
9const preparedCalls = await client.prepareCalls({
10 calls: [
11 // Calls here can include `to`, `data`, and `value` params.
12 { to: "0x0000000000000000000000000000000000000000", data: "0x" }
13 ],
14 from: account.address,
15 capabilities: {
16 paymasterService: {
17 policyId: "your-gas-manager-policy-id", // put your gas manager policy ID here
18 },
19 },
20});
21
22// Sign the calls.
23const signedCalls = await client.signPreparedCalls(preparedCalls);
24
25// Send the userOp.
26const { preparedCallIds } = await client.sendPreparedCalls(signedCalls);
27
28// Check calls status.
29const status = await client.getCallsStatus(preparedCallIds[0]);

Using The JSON-RPC APIs Directly

1. Request An Account For The Owner Signer

Given an EOA address, call wallet_requestAccount with the “7702” account type.

$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", // put the EOA address here
> "creationHint": {
> "accountType": "7702"
> }
> }
> ]
>}
>'

This will return the smart account address, which should be the same as the owner address when using 7702:

1{
2 "jsonrpc": "2.0",
3 "id": 1,
4 "result": {
5 "accountAddress": "0xACCOUNT_ADDRESS",
6 "id": "11111111-2222-3333-4444-555555555555"
7 }
8}

2. Prepare Your Calls

Now you can simply prepare whatever calls you’d like to send.

$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": [
> {
> "calls": [
> // Put the call data here, including `to`, `data`, and optional `value`.
> {
> "to": "0x0000000000000000000000000000000000000000",
> "data": "0x",
> "value": "0x0"
> }
> ],
> "from": "0xACCOUNT_ADDRESS", // put the account address here
> "chainId": "0xCHAIN_ID", // put the chain ID here
> "capabilities": {
> "paymasterService": {
> "policyId": "your-gas-manager-policy-id" // if sponsoring gas, put your gas manager policy ID here
> }
> }
> }
> ]
>}
>'

If the account isn’t already delegated to Modular Account V2 onchain, this will return an array of calls that must be signed:

1{
2 "type": "array",
3 "data": [
4 {
5 "type": "authorization",
6 "data": {
7 "address": "0xDELEGATION_ADDRESS",
8 "nonce": "0xNONCE"
9 },
10 "chainId": "0xCHAIN_ID",
11 "signatureRequest": {
12 "type": "eip7702Auth"
13 }
14 },
15 {
16 "type": "user-operation-v070",
17 "data": {...},
18 "chainId": "0xCHAIN_ID",
19 "signatureRequest": {
20 "type": "personal_sign",
21 "data": {
22 "raw": "0xHASH_TO_SIGN"
23 }
24 }
25 }
26 ]
27}

For subsequent calls, only one call will be returned:

1{
2 "type": "user-operation-v070",
3 "data": {...},
4 "chainId": "0xCHAIN_ID",
5 "signatureRequest": {
6 "type": "personal_sign",
7 "data": {
8 "raw": "0xHASH_TO_SIGN"
9 }
10 }
11}

3. Sign The Call(s)

Sign the prepared calls. How exactly you do this will differ depending on your language. Here is an example using Viem in TypeScript:

1// Assuming you have created a wallet client (see https://viem.sh/docs/clients/wallet).
2
3// Assuming `preparedCalls` is the result from `wallet_prepareCalls`.
4const userOpCall = preparedCalls.type === "array"
5 ? preparedCalls.data.find((it) => it.type === "user-operation-v070")
6 : preparedCalls
7
8const authorizationCall = preparedCalls.type === "array"
9 ? preparedCalls.data.find((it) => it.type === "authorization")
10 : undefined
11
12// Sign the user operation hash.
13const userOpSignature = await walletClient.signMessage({
14 message: userOpCall.signatureRequest.data,
15})
16
17// Sign the EIP-7702 authorization, if it was included in the previous step's result.
18const signedAuthorization = !authorizationCall
19 ? undefined
20 : await walletClient.signAuthorization({
21 contractAddress: authorizationCall.data.address,
22 nonce: hexToNumber(authorizationCall.data.nonce),
23 chainId: hexToNumber(authorizationCall.chainId),
24 })

4. Send The Prepared Calls

Now that you have the prepared calls & signatures, you’re ready to send the calls!

$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": "array",
> "data": [
> {
> "type": "user-operation-v070",
> "data": {...}, // include the user op call data returned from `wallet_prepareCalls`
> "chainId": "0xCHAIN_ID", // put the chain id here
> "signature": {
> "type": "secp256k1",
> "data": "0xUSEROP_SIGNATURE"
> }
> },
> // The signed authorization only needs to be included if an authorization was returned from `wallet_prepareCalls`.
> {
> "type": "authorization",
> "data": {...}, // include the authorization call data returned from `wallet_prepareCalls`
> "chainId": "0xCHAIN_ID", // put the chain id here
> "signature": {
> "type": "secp256k1",
> "data": "0xAUTHORIZATION_SIGNATURE" // this can be serialized in hex format, or a raw signature (r,s,v or r,s,yParity)
> }
> }
> ]
> }
> ]
>}
>'

This will return the array of prepared call IDs.

5. Check The Calls Status

Now you can simply call wallet_getCallsStatus to check the status of the calls.

$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_getCallsStatus",
> "params": [
> "0xPREPARED_CALL_ID"
> ]
>}
>'

See here for all of the possible results.