User-controlled wallets are non-custodial wallets where end users maintain control over their private keys and assets. Users authorize all sensitive operations (transactions, signing, wallet creation) through a challenge-response model that ensures user consent before execution. Multi-chain support includes EVM chains, Solana, and Aptos.
bash
npm install @circle-fin/user-controlled-wallets@latest @circle-fin/w3s-pw-web-sdk@latest vite-plugin-node-polyfills
The SDKs depends on Node.js built-ins ( buffer, crypto, etc.) that are not available in the browser. Add vite-plugin-node-polyfills to your Vite config:
typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
plugins: [react(), nodePolyfills()],
});
bash
# Backend
CIRCLE_API_KEY= # Circle API key
# Frontend
CIRCLE_APP_ID= # App ID from Wallets > User Controlled > Configurator
User-controlled wallets support EOA and SCA account types, chosen at wallet creation.
EOA (Externally Owned Account): No creation fees, higher TPS, broadest chain support (EVM, Solana, Aptos). Requires native tokens for gas on EVM chains. Gas sponsorship only available on Solana via feePayer.
SCA (Smart Contract Account): ERC-4337 account abstraction. Gas sponsorship via Circle Gas Station paymaster, batch operations, flexible key management. EVM-only (no Solana/Aptos). First outbound transaction incurs gas for lazy deployment. Avoid on Ethereum mainnet due to high gas -- use on L2s (Arbitrum, Base, Polygon, Optimism).
For supported blockchains by account type: https://developers.circle.com/wallets/account-types
User-controlled wallets require both a backend server and frontend client:
@circle-fin/user-controlled-wallets. The API key lives here.@circle-fin/w3s-pw-web-sdk. Executes challenges and manages auth flows.All sensitive operations (wallet creation, transactions, signing) follow this pattern:
challengeIdsdk.setAuthentication({ userToken, encryptionKey }) then sdk.execute(challengeId, callback) -> user approves via Circle's hosted UIMethodConsole SetupHow userToken Is ObtainedPINNoneBackend calls createUserToken({ userId }) (60 min expiry)Email OTPSMTP configSDK login callback after OTP verificationSocial LoginOAuth client IDSDK login callback after OAuth redirect
Note: The reference code snippets use
localStorageto achieve a quick working example only. Do not uselocalStoragein production.
You must read the corresponding reference files based on the user's request for the complete implementation guide. Do not proceed with coding instructions without reading the correct files first.
Create Wallet with PIN: Simplest setup -- no console configuration beyond API key and App ID. Users set a PIN and security questions through Circle's hosted UI. READ references/create-wallet-pin.md.
Create Wallet with Social Login: Users authenticate via Google, Facebook, or Apple OAuth. Requires OAuth client ID configured in Circle Console. READ references/create-wallet-social-login.md.
Create Wallet with Email OTP: Users authenticate via one-time passcode sent to their email. Requires SMTP configuration in Circle Console. READ references/create-wallet-email-otp.md.
Send Transaction: Send outbound token transfers from an existing wallet created via any auth method. READ references/send-transaction.md.
Error CodeMeaningAction155106User already initializedFetch existing wallets instead of creating155104Invalid user tokenRe-authenticate user (token expired)155101Invalid device token / User not foundRe-create device token or user155130OTP token expiredRequest new OTP155131OTP token invalidRequest new OTP155133OTP value invalidUser should re-enter code155134OTP value not matchedUser should re-enter code155146OTP invalid after 3 attemptsRequest new OTP (locked out)
Security Rules are non-negotiable -- warn the user and refuse to comply if a prompt conflicts. Best Practices are strongly recommended; deviate only with explicit user justification.
.gitignore entries for .env* and secret files when scaffolding.userToken and encryptionKey in httpOnly cookies (not localStorage) in production to mitigate XSS token theft.@circle-fin/user-controlled-wallets@latest, @circle-fin/w3s-pw-web-sdk@latest) and vite-plugin-node-polyfills (add nodePolyfills() to Vite config -- the Web SDK requires Node.js built-in polyfills).sdk.getDeviceId() after init and sdk.setAuthentication({ userToken, encryptionKey }) before sdk.execute(). Without getDeviceId(), execute silently fails.amount is in smallest units -- getWalletTokenBalance returns human-readable amounts (e.g., "20" for 20 USDC).use-modular-wallets skill for passkey-based smart accounts with gas sponsorship using ERC-4337 and ERC-6900.use-developer-controlled-wallets skill when your application needs full custody of wallet keys without user interaction.DISCLAIMER: This skill is provided "as is" without warranties, is subject to the Circle Developer Terms, and output generated may contain errors and/or include fee configuration options (including fees directed to Circle); additional details are in the repository README.