This file helps AI assistants (Claude Code, Cursor, Copilot, etc.) understand this repository.
Working examples for building ERC-4337 smart wallets with AbstractionKit. Each folder demonstrates a specific feature. All examples target Arbitrum Sepolia.
Public endpoints for immediate development:
| Service | URL |
|---|---|
| Bundler | https://api.candide.dev/public/v3/421614 |
| Paymaster | https://api.candide.dev/public/v3/421614 |
| RPC | https://sepolia-rollup.arbitrum.io/rpc |
| Chain ID | 421614 |
Create .env:
CHAIN_ID=421614
NODE_URL=https://sepolia-rollup.arbitrum.io/rpc
BUNDLER_URL=https://api.candide.dev/public/v3/421614
PAYMASTER_URL=https://api.candide.dev/public/v3/421614
PUBLIC_ADDRESS=<your-eoa-address>
PRIVATE_KEY=<your-eoa-private-key>If using chain-abstraction examples
BUNDLER_URL1=https://api.candide.dev/public/v3/11155111
BUNDLER_URL2=https://api.candide.dev/public/v3/11155420
NODE_URL1=https://ethereum-sepolia-rpc.publicnode.com
NODE_URL2=https://sepolia.optimism.io
CHAIN_ID1=11155111
CHAIN_ID2=11155420
SPONSORSHIP_POLICY_ID1=
SPONSORSHIP_POLICY_ID2=Run any example:
npm install
npx ts-node <folder>/<script>.ts| Goal | Folder | Key File |
|---|---|---|
| Gasless transactions | sponsor-gas/ |
sponsor-gas.ts |
| Gasless — any ERC-7677 provider | erc7677/ |
sponsor-gas.ts |
| Passkey/biometric login | passkeys/ |
index.ts |
| Multi-owner wallet | multisig/ |
multisig.ts |
| Pay gas with ERC-20 | pay-gas-in-erc20/ |
pay-gas-in-erc20.ts |
| Pay gas with ERC-20 — any ERC-7677 provider | erc7677/ |
pay-gas-in-erc20.ts |
| Batch multiple txs | batch-transactions/ |
batch-transactions.ts |
| Account recovery | recovery/ |
recovery.ts |
| EIP-7702 delegation | eip-7702/simple-account/ |
01-upgrade-eoa.ts |
| EIP-7702 pay gas in ERC-20 | eip-7702/simple-account/ |
02-upgrade-eoa-erc20-gas.ts |
| EIP-7702 EP v0.9 | eip-7702/simple-account/ |
03-upgrade-eoa-ep-v09.ts |
| EIP-7702 revoke delegation | eip-7702/simple-account/ |
04-revoke-delegation.ts |
| EIP-7702 typed-data builder (drive signTypedData yourself) | eip-7702/simple-account/ |
07-typed-data-builder.ts |
| EIP-7702 migrate v0.8 → v0.9 (sponsored, no revoke) | eip-7702/simple-account/ |
08-migrate-v08-to-v09.ts |
| Debug with Tenderly | simulate-with-tenderly/ |
simulate-with-tenderly.ts |
| Multichain-chain add owner | chain-abstraction/ |
add-owner.ts |
| Multichain add guardian | chain-abstraction/ |
add-guardian.ts |
| Multichain add owner (Eip-712 Wallet Signed) | chain-abstraction/ |
add-owner-eip712-signed.ts |
| Multichain add owner (Passkey) | chain-abstraction/ |
add-owner-passkey.ts |
| EIP-712 signed UserOp | eip-712-signing/ |
eip-712-signing.ts |
| Nested Safe accounts | nested-safe-accounts/ |
nested-safe-accounts.ts |
| Spending limits | spend-permission/ |
spend-permission.ts |
| Track active users / userOps on-chain | onchain-identifier/ |
onchain-identifier.ts |
| Calibur 7702 upgrade EOA + gas sponsorship | eip-7702/calibur-account/ |
01-upgrade-eoa.ts |
| Calibur 7702 passkeys | eip-7702/calibur-account/ |
02-passkeys.ts |
| Calibur 7702 key management | eip-7702/calibur-account/ |
03-manage-keys.ts |
New in abstractionkit v0.3.2: the ExternalSigner API lets you plug viem, ethers, hardware wallets, HSMs, MPC services, or any custom signer into AbstractionKit without passing raw private keys to the SDK. The signer/ folder has one self-contained example per adapter so you only pull in the signing library you actually use.
| Goal | Folder | Key File |
|---|---|---|
| Overview + adapter matrix | signer/ |
README.md |
| viem LocalAccount | signer/ |
fromViem.ts |
| ethers Wallet | signer/ |
fromEthersWallet.ts |
| viem WalletClient (typed-data path) | signer/ |
fromViemWalletClient.ts |
| Custom (HSM / MPC / hardware) | signer/ |
customSigner.ts |
| Simple7702 external signer | eip-7702/simple-account/ |
05-external-signer.ts |
| Simple7702 EP v0.9 external signer | eip-7702/simple-account/ |
06-external-signer-v09.ts |
| Calibur external signer | eip-7702/calibur-account/ |
04-external-signer.ts |
| Multichain add owner (external signer) | chain-abstraction/ |
add-owner-with-external-signer.ts |
| Variable | Required | Description |
|---|---|---|
CHAIN_ID |
Yes | Target chain (421614 for Arbitrum Sepolia) |
NODE_URL |
Yes | Chain RPC endpoint |
BUNDLER_URL |
Yes | ERC-4337 bundler endpoint |
PAYMASTER_URL |
For sponsored | Candide paymaster endpoint |
PUBLIC_ADDRESS |
Yes | Your EOA public address |
PRIVATE_KEY |
Yes | Your EOA private key (becomes account owner) |
SPONSORSHIP_POLICY_ID |
Optional | For custom sponsorship policies |
TOKEN_ADDRESS |
For ERC-20 gas | Token to pay gas with |
SPONSORSHIP_POLICY_ID1 |
For chain-abstraction | Sponsorship policy for chain 1 |
SPONSORSHIP_POLICY_ID2 |
For chain-abstraction | Sponsorship policy for chain 2 |
BUNDLER_URL1 |
For chain-abstraction | ERC-4337 bundler endpoint |
BUNDLER_URL2 |
For chain-abstraction | ERC-4337 bundler endpoint |
NODE_URL1 |
For chain-abstraction | Chain 1 RPC endpoint |
NODE_URL2 |
For chain-abstraction | Chain 2 RPC endpoint |
CHAIN_ID1 |
For chain-abstraction | Target chain 1 (11155111 for Sepolia) |
CHAIN_ID2 |
For chain-abstraction | Target chain 2 (11155420 for OP Sepolia) |
For production endpoints: https://dashboard.candide.dev
Account has insufficient ETH and no paymaster is sponsoring.
Fix: Use the sponsor-gas/ example with the public paymaster, or fund the smart account address.
Nonce mismatch - previous transaction not yet confirmed. Fix: Wait for previous transaction to be included, or fetch fresh nonce.
The transaction would fail on-chain. Fix:
- Verify the
toaddress exists and is correct - Check calldata encoding matches the target function
- Use
simulate-with-tenderly/to debug
Signature doesn't match expected signer(s). Fix:
- For multisig: signatures must be sorted by signer address (ascending)
- Verify you're signing the correct UserOperation hash
- Check the signer is an owner of the account
Fix:
- Verify
PAYMASTER_URLis correct - Check the paymaster supports the target chain
- For token paymaster: ensure account has enough tokens
Use signUserOperationWithSigners (Safe, multi-signer array) or signUserOperationWithSigner (Simple7702 / Calibur, single signer) with an ExternalSigner to avoid passing raw private keys into the SDK.
Three built-in adapters cover the common cases:
| Adapter | For |
|---|---|
fromViem(localAccount) |
Any viem LocalAccount (most projects) |
fromEthersWallet(wallet) |
Any ethers.Wallet / HDNodeWallet |
fromViemWalletClient(client) |
viem WalletClient (typed-data only; no multi-op) |
For HSM / MPC / hardware wallets, pass an inline object matching ExternalSigner: { address, signHash?(hash): Promise<hex>, signTypedData?(data): Promise<hex> }. At least one of signHash or signTypedData is required (compile-time check).
If all you have is a raw 0x-hex private key, the shortest path is the legacy sync API: safe.signUserOperation(userOp, [privateKey], chainId). A fromPrivateKey(pk) adapter is also exported for multi-owner setups where pk and HSM owners need to flow through the same async interface.
Signing is async. Capability mismatches (e.g. a typed-data-only signer against a hash-only account) throw offline with an actionable message, so no HSM / hardware prompt fires on a trip that would fail anyway.
Canonical per-adapter examples: signer/. Account-specific starters for the flows that differ from the standard Safe-sponsored flow: eip-7702/*/0X-external-signer*.ts, chain-abstraction/add-owner-with-external-signer.ts.
All examples follow this structure:
import { SafeAccountV0_3_0 as SafeAccount, MetaTransaction, CandidePaymaster } from "abstractionkit";
// 1. Initialize account
let smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
// Or for existing: new SafeAccount(accountAddress)
// 2. Create transaction(s)
const tx: MetaTransaction = {
to: targetAddress,
value: 0n,
data: callData,
};
// 3. Create UserOperation
let userOp = await smartAccount.createUserOperation(
[tx],
nodeUrl,
bundlerUrl,
);
// 4. (Optional) Add paymaster for sponsorship
const paymaster = new CandidePaymaster(paymasterUrl);
[userOp] = await paymaster.createSponsorPaymasterUserOperation(smartAccount, userOp, bundlerUrl);
// 5. Sign
userOp.signature = smartAccount.signUserOperation(userOp, [privateKey], chainId);
// 6. Send and wait
const response = await smartAccount.sendUserOperation(userOp, bundlerUrl);
const receipt = await response.included();| Class | Use Case | EntryPoint | External Signer method |
|---|---|---|---|
SafeAccountV0_3_0 |
Most examples (recommended) | v0.7 | signUserOperationWithSigners(op, signers[], chainId) |
SafeAccountV0_2_0 |
Legacy/v0.6 compatibility | v0.6 | signUserOperationWithSigners(op, signers[], chainId) |
Simple7702Account |
EIP-7702 delegation | v0.8 | signUserOperationWithSigner(op, signer, chainId) |
Simple7702AccountV09 |
EIP-7702 delegation (EP v0.9) | v0.9 | signUserOperationWithSigner(op, signer, chainId) |
SafeMultiChainSigAccountV1 |
Chain abstraction (Safe Unified Account) | v0.9 | signUserOperationsWithSigners(ops[], signers[]) |
Calibur7702Account |
EIP-7702 Calibur (passkeys, key mgmt) | v0.8 (default) | signUserOperationWithSigner(op, signer, chainId) |
# Install dependencies
npm install
# Run an example
npx ts-node sponsor-gas/sponsor-gas.ts
# Build TypeScript
npm run build
# Clean build artifacts
npm run cleanThis examples repo is the source of truth for working code. Documentation at docs.candide.dev may occasionally lag behind. If docs and examples differ, trust the examples.
- Library: https://github.com/candidelabs/abstractionkit
- Docs: https://docs.candide.dev
- Dashboard: https://dashboard.candide.dev
- Discord: https://discord.gg/KJSzy2Rqtg