Post 24

Account Abstraction (ERC-4337) for Front-End Engineers

Smart accounts remove the two worst parts of onboarding: seed phrases and having ETH before you can do anything. Here is what actually changes in the front end — bundlers, paymasters, session keys, and batched calls — with permissionless.js.

Sep 7, 2026/13 min readWeb3
ShareY
Account Abstraction (ERC-4337) for Front-End Engineers

Two things kill Web3 onboarding: the seed phrase, and the fact that a brand-new user has to acquire ETH before they can do anything at all. Account abstraction (ERC-4337) fixes both by making the account a smart contract instead of a keypair. For a front-end engineer that shifts a few concepts around — you stop sending transactions and start sending user operations — but the payoff is real: gasless flows, batched calls, and login with a passkey.

The moving parts

PieceWhat it isWho runs it
Smart accountThe contract wallet that holds funds and validates opsDeployed for the user (often on first use)
UserOperationThe 'transaction' — a struct, not an RLP txYour front end builds it
BundlerPackages UserOps into a real transaction to the EntryPointA service (Pimlico, Alchemy, Stackup)
PaymasterSponsors gas, or lets the user pay in an ERC-20A service, or your own contract
EntryPointThe singleton contract that verifies and executes opsCanonical, one per chain
Note

You do not talk to the EntryPoint directly. Your front end builds a UserOperation, asks a paymaster to sponsor it, signs it with the user's key (or passkey), and sends it to a bundler's RPC. The bundler does the on-chain part.

A smart account client with permissionless.js

lib/aa.ts
1import { createSmartAccountClient } from "permissionless";2import { toSafeSmartAccount } from "permissionless/accounts";3import { createPimlicoClient } from "permissionless/clients/pimlico";4import { createPublicClient, http } from "viem";5import { base } from "viem/chains";6 7const publicClient = createPublicClient({ chain: base, transport: http() });8 9const pimlico = createPimlicoClient({10  transport: http(`https://api.pimlico.io/v2/base/rpc?apikey=${KEY}`),11});12 13export async function getSmartAccountClient(owner: LocalAccount) {14  const account = await toSafeSmartAccount({15    client: publicClient,16    owners: [owner],17    version: "1.4.1",18  });19 20  return createSmartAccountClient({21    account,22    chain: base,23    bundlerTransport: http(pimlico.transport.url),24    paymaster: pimlico,          // sponsor gas25    userOperation: {26      estimateFeesPerGas: async () =>27        (await pimlico.getUserOperationGasPrice()).fast,28    },29  });30}

From here the client looks almost like a normal viem wallet client — sendTransaction, writeContract — except it is building and submitting UserOperations under the hood.

The batched-call superpower

A regular EOA can only do one thing per transaction, which is why 'approve then swap' is two wallet prompts and two confirmations. A smart account executes an array of calls atomically in one UserOp.

approve + swap in one operation
1const hash = await smartAccountClient.sendTransaction({2  calls: [3    {4      to: USDC,5      data: encodeFunctionData({6        abi: erc20Abi,7        functionName: "approve",8        args: [ROUTER, amountIn],9      }),10    },11    {12      to: ROUTER,13      data: encodeFunctionData({14        abi: routerAbi,15        functionName: "swapExactTokensForTokens",16        args: [amountIn, minOut, path, account.address, deadline],17      }),18    },19  ],20});21// one signature, one confirmation, atomic — approve can't succeed22// while swap fails and strand an allowance

Gasless, and 'gas in USDC'

With a verifying paymaster you sponsor the op entirely — the user needs zero ETH. With an ERC-20 paymaster they pay the fee in a stablecoin they already hold. Either way the front-end change is one config line, but the UX change is enormous: the 'you need ETH for gas' wall disappears.

let the user pay gas in USDC
1const client = createSmartAccountClient({2  account,3  chain: base,4  bundlerTransport: http(bundlerUrl),5  paymaster: {6    async getPaymasterData(userOperation) {7      return pimlico.getPaymasterData({8        ...userOperation,9        token: USDC_ADDRESS,   // charge the fee in USDC10      });11    },12  },13});
Watch out

Still show the fee. 'Gasless' does not mean 'free' when the user is paying in USDC, and even sponsored ops have a cost you may want to surface for transparency. A silent fee is worse than a visible small one.

Session keys: approve once, act many times

A session key is a temporary key with a narrow permission — 'can call placeBet on this contract, up to 50 USDC, for the next hour'. The user signs one approval, then your app acts on their behalf within those limits without another prompt. This is what makes onchain games and trading UIs feel like Web2.

grant a scoped session (module-dependent shape)
1await smartAccountClient.grantPermissions({2  permissions: [{3    target: GAME_CONTRACT,4    selector: "placeMove(uint256)",5    policies: [6      { type: "spend-limit", limit: parseUnits("50", 6) },7      { type: "expiry", timestamp: Math.floor(Date.now() / 1000) + 3600 },8    ],9  }],10});11// store the session key client-side (in-memory or an encrypted store)12// and use it for subsequent moves with no wallet prompt

What still bites

  • Account deployment. The first UserOp deploys the account, so it costs more and can fail for reasons a normal tx would not. Handle the 'account not deployed yet' state.
  • Address ≠ signer. The smart account address is counterfactual and different from the owner key's address. Every 'connect wallet' assumption about msg.sender needs revisiting.
  • Bundler and paymaster are infra you now depend on. They have rate limits, downtime, and policies. Have a fallback and surface failures clearly.
  • Signature verification. Contracts check smart-account signatures with EIP-1271, not ecrecover. If you verify signatures anywhere, support both.
Account abstraction does not make Web3 UX good on its own. It removes the two hardest objections — seed phrases and 'buy ETH first' — and hands you the primitives to build the rest.

Found this useful? Pass it on.

All posts

Want a custom write-up for your team? Get in touch.

Building something like this?

If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.