Skip to content

Smart accounts (ERC-4337)

A Sigil smart account is an ERC-4337 contract account owned by one of the user’s EOA wallets. It’s opt-in, per-chain, and Sigil sponsors the gas — your users can send transactions without ever holding native gas tokens. The cap today is €10 per wallet per calendar month, EUR-equivalent across all chains the wallet uses.

It’s the right choice when:

  • You want a wagmi-style UX where users mint, swap, or play without pre-funding a wallet.
  • You want batched calls (an approve + a swap in a single signature).
  • You’re shipping a consumer app where “what’s gas?” friction kills conversion.

Smart accounts don’t replace EOA wallets — they sit on top. The underlying EOA is still the signing authority; the smart account is a thin contract that executes whatever the EOA approves.

Implementation

  • Factory: Alchemy’s LightAccount v2, deployed deterministically at the same address on every EVM chain.
  • Address derivation: CREATE2 from (factory, owner, salt=0). The address is identical across base, arbitrum, optimism, polygon, ethereum, … for the same owner — no per-chain surprises.
  • Bundler + paymaster: Pimlico.
  • Entry point: ERC-4337 v0.7 (0x...71727De22E5E9d8BAf0edAc6f37da032).
  • Owner: the user’s EOA. Signs the userOpHash exactly the way it would sign any EIP-191 message; LightAccount v2’s verifier accepts that as the owner signature.

What “per-chain” means

The smart account address is the same on every chain. The row in Sigil’s database is per-chain: each enableSmartAccount(chain) call records one (wallet, chain, factory) tuple with its own deployed flag, sponsor policy, and budget contribution.

This matters because:

  • The first UserOp on a given chain pays the deployment cost (the bundler bundles initCode into that UserOp). Subsequent UserOps on the same chain are cheaper.
  • Until the first UserOp lands, the address holds no contract — it’s counterfactual. You can still send funds to it; they’ll be spendable once the account deploys.
  • A chain you never call enableSmartAccount on simply doesn’t exist for Sigil’s routing logic, and sendTransaction on that chain falls back to the EOA’s own gas.

Provisioning

From the SDK (browser, end-user-driven):

const sa = await sigil.enableSmartAccount('base');
// -> { id, address, chain: 'base', factory: 'lightaccount_v2',
// deployed: false, ownerAddress: '0x...', ... }

Or from your backend (sk_live_…), e.g. during onboarding:

Terminal window
curl -X POST https://api.sigilkeys.com/v1/s2s/wallets/{walletID}/smart-accounts \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"chain": "base"}'

Both are idempotent — calling them twice for the same (wallet, chain) returns the existing row.

To list what’s provisioned:

const all = await sigil.listSmartAccounts();
for (const sa of all) {
console.log(sa.chain, sa.address, sa.deployed);
}

Sending

You don’t have to change how you call sendTransaction. The iframe detects whether the requested chain has a smart account and routes accordingly:

// If a SA is provisioned on 'base' → routes through 4337, gas
// sponsored by Sigil. If not → routes through the raw EOA path,
// user pays gas.
const r = await sigil.sendTransaction({
chain: 'base',
to: NFT_CONTRACT,
data: encodeMint(1),
preview: { title: 'Mint #42' },
});

The returned shape is identical ({ chain, chainId, txHash, from }) in both cases. The only observable difference is the from:

  • Smart account path: from is the smart account address.
  • EOA path: from is the EOA address.

Behind the scenes the smart-account path:

  1. Builds a UserOp (LightAccount execute(to, value, data) calldata).
  2. Asks Pimlico to sponsor + estimate gas.
  3. Asks the iframe to sign the userOpHash with the owner EOA’s key (EIP-191 wrapped, same as signMessage).
  4. Submits to the bundler.
  5. Polls up to 60 s for the on-chain receipt.

The confirm screen the user sees says “Gas sponsored by Sigil” — the contrast with the raw-tx path is intentional, so integrators A/B-ing the two lanes can tell which one a wallet just used.

Gas budget — the €10 / month cap

Sigil tracks per-wallet sponsorship spend in EUR micros, keyed by (wallet_id, period_year_month). The cap is hardcoded at €10 / wallet / calendar month and applies across all chains the wallet uses. Conversion from wei to EUR happens at record time using the current native-token price from CoinGecko — we snapshot the converted value, so later price moves don’t retroactively bust budgets.

When a wallet drains its bucket, the next sendTransaction that would route through the smart account is rejected with code: 'gas_cap_exceeded'. The SDK error looks like:

Error: sigil sendTransaction: gas_cap_exceeded — monthly gas
sponsorship cap of €10.00 reached for this wallet

Your app should catch that and either:

  • Fall back to asking the user to fund their EOA and retry on the raw-tx lane (works without code changes — disable the smart account routing client-side, or call sigil.sendTransaction on a chain you haven’t provisioned a smart account on).
  • Wait for the next calendar month — the bucket resets at UTC midnight on the 1st.

Per-org elastic caps (paid plans) will land in a later sprint; today the cap is platform-wide.

Recovery

Smart accounts derive from their owner EOA. There is no separate recovery flow for the smart account itself — recover the underlying EOA via the standard Recovery flow, and the smart account on every chain is automatically restored (same factory + same owner = same CREATE2 address).

Dependencies

  • Pimlico: Sigil’s bundler + paymaster provider.
  • An EVM RPC for the target chain: Sigil’s infrastructure manages this — you just pass a chain handle like 'base'.
  • An owner EOA: either client or TEE mode. You can’t have a smart account without one.
  • A wallet on the secp256k1 curve: smart accounts are EVM-only. The endpoint refuses non-EVM curves with invalid_curve.

Caveats

  • Bundled deployment: the first UserOp on a chain is heavier (deployment + the call). Subsequent UserOps are cheaper. This is invisible to the user but counts against the cap.
  • No cross-chain coordination: the cap is per wallet across chains, but the underlying ops are per chain. A chain with no EVM RPC wired in Sigil is silently unavailable.
  • Address parity check: smart accounts are deterministic for the (factory, owner, salt=0) tuple Sigil uses. If you ever need a second smart account at a different address for the same owner, that’s a future enhancement (non-zero salt) — file a request.

Worked example

// 1. Provision once, anywhere — typically at sign-up or first send
await sigil.enableSmartAccount('base');
// 2. Send anywhere, anytime — no user funding required
const r = await sigil.sendTransaction({
chain: 'base',
to: USDC_BASE,
data: encodeTransfer(toAddress, 1_000_000n), // 1 USDC
preview: {
title: 'Send 1 USDC',
description: `To ${shortAddress(toAddress)}`,
},
});
console.log(r.txHash, r.from);
// from = smart account address (deterministic across EVM chains)