Smart accounts (also known as Account Abstraction or ERC-4337 accounts) are smart contract-based accounts that enable advanced features like gasless transactions, batch operations, and social recovery. Unlike traditional EOA accounts, smart accounts are controlled by code and can sponsor gas fees via paymasters.
Key Benefits
- Gasless Transactions: Use paymasters to sponsor gas fees for users
- Batch Operations: Execute multiple calls in a single user operation
- Flexible Ownership: Support for multi-signature and social recovery
- Policy Controls: Apply transaction policies at the smart account level
Creating a Smart Account
Create an owner account
Smart accounts require an owner account (EOA) to sign operations:import { CdpClient } from "@coinbase/cdp-sdk";
const cdp = new CdpClient();
const owner = await cdp.evm.createAccount({ name: "Owner" });
from cdp import CdpClient
async with CdpClient() as cdp:
owner = await cdp.evm.create_account(name="Owner")
Create the smart account
Create a smart account with the owner:const smartAccount = await cdp.evm.createSmartAccount({
owner: owner,
name: "MySmartAccount"
});
console.log(`Smart account: ${smartAccount.address}`);
console.log(`Owner: ${owner.address}`);
smart_account = await cdp.evm.create_smart_account(
owner=owner,
name="MySmartAccount"
)
print(f"Smart account: {smart_account.address}")
print(f"Owner: {owner.address}")
Get or create pattern
Use idempotent creation for production:const smartAccount = await cdp.evm.getOrCreateSmartAccount({
name: "MySmartAccount",
owner: owner
});
smart_account = await cdp.evm.get_or_create_smart_account(
name="MySmartAccount",
owner=owner
)
Sending User Operations
User operations are the smart account equivalent of transactions:
import { encodeFunctionData } from "viem";
// Simple ETH transfer
const userOp = await smartAccount.sendUserOperation({
calls: [
{
to: "0x1234...",
value: 1000000000000000n, // 0.001 ETH
data: "0x"
}
],
network: "base-sepolia"
});
console.log(`User operation hash: ${userOp.userOpHash}`);
from cdp.evm_call_types import EncodedCall
# Simple ETH transfer
user_op = await smart_account.send_user_operation(
calls=[
EncodedCall(
to="0x1234...",
value=1000000000000000, # 0.001 ETH
data="0x"
)
],
network="base-sepolia"
)
print(f"User operation hash: {user_op.user_op_hash}")
Batch Operations
Execute multiple calls atomically:
const userOp = await smartAccount.sendUserOperation({
calls: [
{
to: "0xTokenContract...",
value: 0n,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: ["0xSpender...", 1000000n]
})
},
{
to: "0xDEXContract...",
value: 0n,
data: encodeFunctionData({
abi: dexAbi,
functionName: "swap",
args: [/* swap params */]
})
}
],
network: "base-sepolia"
});
user_op = await smart_account.send_user_operation(
calls=[
EncodedCall(
to="0xTokenContract...",
value=0,
data="0x...", # Encoded approve call
),
EncodedCall(
to="0xDEXContract...",
value=0,
data="0x...", # Encoded swap call
)
],
network="base-sepolia"
)
Gasless Transactions with Paymasters
Use a paymaster to sponsor gas fees:
const userOp = await smartAccount.sendUserOperation({
calls: [{
to: "0x1234...",
value: 1000000000000000n,
data: "0x"
}],
network: "base-sepolia",
paymasterUrl: "https://paymaster.example.com"
});
user_op = await smart_account.send_user_operation(
calls=[EncodedCall(
to="0x1234...",
value=1000000000000000,
data="0x"
)],
network="base-sepolia",
paymaster_url="https://paymaster.example.com"
)
Paymasters allow you to sponsor transactions for your users, enabling truly gasless experiences. Contact your paymaster provider for the correct URL.
Waiting for User Operations
Wait for a user operation to be confirmed:
const confirmedOp = await smartAccount.waitForUserOperation(
userOp.userOpHash,
{
timeoutSeconds: 30,
intervalSeconds: 0.5
}
);
console.log(`Status: ${confirmedOp.status}`);
console.log(`Transaction hash: ${confirmedOp.transactionHash}`);
confirmed_op = await smart_account.wait_for_user_operation(
user_op_hash=user_op.user_op_hash,
timeout_seconds=30,
interval_seconds=0.5
)
print(f"Status: {confirmed_op.status}")
print(f"Transaction hash: {confirmed_op.transaction_hash}")
Token Transfers
Transfer tokens using the simplified transfer method:
const result = await smartAccount.transfer({
to: "0x1234...",
amount: 10000n, // 0.01 USDC (6 decimals)
token: "usdc",
network: "base-sepolia",
paymasterUrl: "https://paymaster.example.com" // Optional
});
console.log(`Transfer user op: ${result.userOpHash}`);
result = await smart_account.transfer(
to="0x1234...",
amount=10000, # 0.01 USDC (6 decimals)
token="usdc",
network="base-sepolia",
paymaster_url="https://paymaster.example.com" # Optional
)
print(f"Transfer user op: {result.user_op_hash}")
Token Swaps
Execute token swaps via user operations:
// Get a swap quote first
const quote = await smartAccount.quoteSwap({
fromToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
toToken: "0x4200000000000000000000000000000000000006", // WETH
fromAmount: 100000000n, // 100 USDC
network: "base-sepolia",
paymasterUrl: "https://paymaster.example.com"
});
// Execute the swap
const result = await smartAccount.swap({
swapQuote: quote,
idempotencyKey: "unique-key-123"
});
console.log(`Swap user op: ${result.userOpHash}`);
from cdp.actions.evm.swap.types import SmartAccountSwapOptions
# Get a swap quote first
quote = await smart_account.quote_swap(
from_token="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC
to_token="0x4200000000000000000000000000000000000006", # WETH
from_amount="100000000", # 100 USDC
network="base-sepolia",
paymaster_url="https://paymaster.example.com"
)
# Execute the swap
result = await smart_account.swap(
SmartAccountSwapOptions(
swap_quote=quote,
idempotency_key="unique-key-123"
)
)
print(f"Swap user op: {result.user_op_hash}")
Checking Balances
const balances = await smartAccount.listTokenBalances({
network: "base-sepolia"
});
for (const balance of balances.tokenBalances) {
console.log(`${balance.symbol}: ${balance.amount}`);
}
balances = await smart_account.list_token_balances(
network="base-sepolia"
)
for balance in balances.token_balances:
print(f"{balance.symbol}: {balance.amount}")
Retrieving User Operations
Get details about a specific user operation:
const userOp = await smartAccount.getUserOperation(
"0xUserOpHash..."
);
console.log(`Status: ${userOp.status}`);
console.log(`Gas used: ${userOp.actualGasUsed}`);
user_op = await smart_account.get_user_operation(
"0xUserOpHash..."
)
print(f"Status: {user_op.status}")
print(f"Gas used: {user_op.actual_gas_used}")
Listing Smart Accounts
const response = await cdp.evm.listSmartAccounts({
pageSize: 50
});
for (const account of response.accounts) {
console.log(`${account.name}: ${account.address}`);
}
response = await cdp.evm.list_smart_accounts(page_size=50)
for account in response.accounts:
print(f"{account.name}: {account.address}")
Signing Typed Data
Sign EIP-712 typed data with a smart account:
const signature = await smartAccount.signTypedData({
domain: {
name: "MyApp",
version: "1",
chainId: 84532,
verifyingContract: "0x..."
},
types: {
Mail: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "contents", type: "string" }
]
},
primaryType: "Mail",
message: {
from: "0x...",
to: "0x...",
contents: "Hello!"
},
network: "base-sepolia"
});
from cdp.evm_message_types import EIP712Domain
signature = await smart_account.sign_typed_data(
domain=EIP712Domain(
name="MyApp",
version="1",
chain_id=84532,
verifying_contract="0x..."
),
types={
"Mail": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "contents", "type": "string"}
]
},
primary_type="Mail",
message={
"from": "0x...",
"to": "0x...",
"contents": "Hello!"
},
network="base-sepolia"
)
Troubleshooting
User Operation Failed
Check the user operation status for detailed error information:
const userOp = await smartAccount.getUserOperation(userOpHash);
console.log(`Status: ${userOp.status}`);
console.log(`Revert reason: ${userOp.revertReason}`);
Insufficient Funds
Smart accounts need ETH for gas unless using a paymaster:
// Request ETH from faucet
await cdp.evm.requestFaucet({
address: smartAccount.address,
network: "base-sepolia",
token: "eth"
});
Paymaster Rejection
If a paymaster rejects your operation:
- Verify the paymaster URL is correct
- Check if the operation meets paymaster policies
- Ensure the smart account is eligible for sponsorship
Next Steps