Notus API

Custom User Operation

Build and execute a sequence of contract calls in a single UserOperation.

Overview

Use this guide to create a simple custom UserOperation that transfers tokens from your smart wallet to a recipient. You'll then sign and execute it.

Steps

Install dependencies and initialize Wallet Account

Install viem to sign messages and interact with the blockchain, then initialize your wallet account.

npm install viem
pnpm add viem
yarn add viem
bun add viem

Use your private key to initialize a wallet account.

import { privateKeyToAccount } from 'viem/accounts'

const BASE_URL = 'https://api.notus.team/api/v1'
const API_KEY = '<api-key>'

const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'

const privateKey = '0x<private-key>'
const account = privateKeyToAccount(privateKey)

Register a Smart Wallet Address

Register and retrieve the smart wallet address used for Account Abstraction.

async function setupSmartWallet() {
  const FACTORY_ADDRESS = '0x0000000000400CdFef5E2714E63d8040b700BC24'
  const externallyOwnedAccount = account.address

  const res = await fetch(`${BASE_URL}/wallets/register`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      externallyOwnedAccount,
      factory: FACTORY_ADDRESS,
      salt: '0',
    }),
  })

  if (!res.ok) return

  const data = await res.json()
  return data.wallet.accountAbstraction
}

The smart wallet is deployed automatically on the first onchain transaction via a UserOperation.

Create a Custom User Operation

Compose a single ERC-20 transfer operation. In this example, we transfer 5 USDC on Polygon to a recipient.

const smartWalletAddress = await setupSmartWallet()

const customUserOperationResponse = await fetch(`${BASE_URL}/crypto/custom-user-operation`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    chainId: 137, // Polygon
    walletAddress: smartWalletAddress,
    payGasFeeToken: USDC_POLYGON, // token used to pay fees
    operations: [
      {
        address: USDC_POLYGON, // ERC-20 token address
        methodSignature: 'function transfer(address to, uint256 amount)',
        arguments: [
          '0x<recipient-address>', // recipient
          '5000000', // 5 USDC (6 decimals)
        ],
      },
    ],
    metadata: { useCase: 'custom-flow', requestId: 'req_123' },
  }),
}).then((r) => r.json())

const { userOperation } = customUserOperationResponse
console.log('userOperationHash:', userOperation.userOperationHash)
  • payGasFeeToken must be a token the smart wallet holds; it pays partner fees and gas.
    - For a native token transfer, omit methodSignature and set only address (recipient) and value (in wei).

Example: Native Token Transfer

const nativeTransferResponse = await fetch(`${BASE_URL}/crypto/custom-user-operation`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    chainId: 137, // Polygon
    walletAddress: smartWalletAddress,
    payGasFeeToken: USDC_POLYGON, // token used to pay gas fees (separate from the native token being transferred)
    operations: [
      {
        address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', // recipient address
        value: '1000000000000000000', // 1 MATIC in wei (18 decimals)
      },
    ],
    metadata: { useCase: 'native-transfer', requestId: 'req_124' }, // optional metadata for tracking transactions
  }),
}).then((r) => r.json())

Execute the User Operation

Sign the returned userOperationHash with the EOA, then queue execution.

const signature = await account.signMessage({
  message: { raw: userOperation.userOperationHash },
})

const executionResponse = await fetch(`${BASE_URL}/crypto/execute-user-op`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    userOperationHash: userOperation.userOperationHash,
    signature,
  }),
}).then((r) => r.json())

console.log(executionResponse)

If your wallet uses EIP-7702, include the authorization signature when executing.