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 acrossbase,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
userOpHashexactly 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
initCodeinto 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
enableSmartAccounton simply doesn’t exist for Sigil’s routing logic, andsendTransactionon 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:
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:
fromis the smart account address. - EOA path:
fromis the EOA address.
Behind the scenes the smart-account path:
- Builds a UserOp (LightAccount
execute(to, value, data)calldata). - Asks Pimlico to sponsor + estimate gas.
- Asks the iframe to sign the
userOpHashwith the owner EOA’s key (EIP-191 wrapped, same assignMessage). - Submits to the bundler.
- 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 gassponsorship cap of €10.00 reached for this walletYour 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.sendTransactionon 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
secp256k1curve: smart accounts are EVM-only. The endpoint refuses non-EVM curves withinvalid_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 sendawait sigil.enableSmartAccount('base');
// 2. Send anywhere, anytime — no user funding requiredconst 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)