Aidra Smart Wallet
Aidra is an ERC-4337 smart wallet that automates on-chain payments through natural language.
Problem Statement
Aidra is an ERC-4337 smart contract wallet that lets users execute blockchain transactions through natural language conversation instead of traditional forms and buttons. The Core Problem Traditional Ethereum wallets (MetaMask, Coinbase Wallet, etc.) are externally owned accounts (EOAs) - essentially just private keys that can sign transactions. They have fundamental limitations:No batching: Sending ETH to 10 people requires 10 separate transactions, each costing gas No programmability: You can't encode business logic like "send $50 every Friday" or "lock funds for recurring payments" Poor UX: Users navigate through multiple screens, paste addresses, manually enter amounts, and understand technical concepts (gas, nonces, hex data)My Solution Architecture Aidra consists of three integrated layers: Layer 1: Smart Contract Accounts (ERC-4337) I deployed three contracts on Ethereum Sepolia:AidraSmartWalletFactory (0x98579827...): Deploys deterministic wallet instances using CREATE2 AidraSmartWallet (0xD1d49aB9...): The actual wallet implementation with custom logic AidraIntentRegistry (0xd77d00B7...): Manages scheduled/recurring payment intentsEach user gets a smart contract wallet (not an EOA). This enables:Batch execution: Our executeBatch() function processes multiple transfers in ONE transaction, saving 50-70% gas Commitment system: Funds can be locked for future recurring payments, preventing accidental overspending Delegated execution: The registry contract can execute transfers on behalf of the wallet for scheduled payments Failure handling: Configurable behavior (skip or revert) when individual transfers fail in a batchLayer 2: AI-Powered Interface Instead of traditional UI forms, users chat with their wallet:User types: "Send 0.1 ETH to Bob and 0.2 ETH to Charlie" Gemini 2.0 Flash (via Vercel AI SDK 5.0) extracts: recipients=[Bob, Charlie], amounts=[0.1, 0.2], token=ETH AI calls the executeBatchEthTransfer tool Frontend shows confirmation dialog with full transaction details User clicks "Confirm & Sign" Transaction executes via ERC-4337I built 7 specialized AI tools:executeSingleEthTransfer - Single ETH transfer executeSinglePyusdTransfer - Single PYUSD transfer executeBatchEthTransfer - Multiple ETH recipients executeBatchPyusdTransfer - Multiple PYUSD recipients executeRecurringEthPayment - Scheduled ETH payments executeRecurringPyusdPayment - Scheduled PYUSD payments cancelRecurringPayment - Cancel intent and refundLayer 3: Event Indexing & Transaction History I integrated Envio HyperIndex to provide real-time transaction history:Indexes 11 events across our 3 contracts (IntentCreated, PaymentExecuted, ExecutedBatch, etc.) Creates 7 entity types with relationships (Wallet, Transaction, Intent, IntentExecution, etc.) Exposes GraphQL API for querying (sub-50ms response times) Powers AI-driven insights: "Show my recurring payments" or "How much did I spend this week?"Key InnovationsCustom ERC-4337 Implementation Most projects use pre-built templates (Safe, Biconomy). We built custom logic for:Batch execution with configurable failure handling Commitment tracking to guarantee scheduled payment execution Registry delegation pattern (allows intent contract to execute transfers)Intent-Based Recurring Payments When a user creates a recurring payment:Total required funds are calculated (e.g., $5 × 7 days = $35) Funds are committed (locked) in the wallet contract Intent is stored in the registry with schedule details Chainlink Automation checks if execution time has arrived Anyone can trigger execution (trustless, decentralized) Failed transfers are handled gracefully (skip vs. revert)Dual-Token Support All 7 payment types work with both:Native ETH transfers PayPal USD (PYUSD) ERC-20 token (0xCaC524BcA292...) This required careful handling of decimals (ETH=18, PYUSD=6) and different encoding formats.Security-First DesignAI NEVER executes transactions directly (only suggests them) User must explicitly confirm every transaction in the UI ERC-4337 signature validation ensures only the wallet owner can authorize Balance checks happen BEFORE AI suggests transactions Zod schemas validate all AI tool inputs TypeScript provides end-to-end type safetyReal-World Example User: "Send $5 PYUSD every day for a week to 0xABC..." What happens:AI parses: token=PYUSD, amount=5, interval=daily, duration=7 days AI calculates: totalCommitment = 5 × 7 = $35, interval=86400 seconds AI calls executeRecurringPyusdPayment tool Frontend shows confirmation: "You're about to create a recurring payment: $5 PYUSD daily for 7 days to 0xABC... Total commitment: $35" User confirms → Transaction sent via Privy wallet Smart account calls AidraIntentRegistry.createIntent() Registry calls back to wallet: increaseCommitment(PYUSD_address, 35e6) (6 decimals) Intent created with ID, schedule stored on-chain Chainlink Automation monitors intent Every 24 hours, performUpkeep() executes next payment Envio indexes IntentCreated and subsequent IntentExecuted events User can ask AI: "Show my active recurring payments" → Queries Envio GraphQLImpactGas savings: Send to 10 people in ONE transaction (95,000 gas) vs. 10 transactions (210,000 gas) = 55% savings UX improvement: Complex operations (batch + recurring) done in seconds via chat vs. minutes of form-filling Accessibility: No technical knowledge required - just conversational English Trustless automation: Recurring payments execute on-chain without centralized serversCurrent Deployment Live on Ethereum Sepolia testnet. Users connect via Privy embedded wallets, chat with Gemini 2.0 Flash, and execute ERC-4337 transactions. All events indexed by Envio for real-time history queries.
Solution
Technology Stack & Integration Blockchain LayerERC-4337 Account Abstractioni implemented a custom smart account following EntryPoint v0.7 specification Challenge: The EntryPoint expects a specific signature format. I had to carefully handle the getUserOpHash() calculation and ensure our signature matched exactly. Solution: Used viem's hashMessage() with EIP-191 prefix (\x19Ethereum Signed Message:\n32) to match what Privy's wallet signstypescript // The tricky part: Getting signatures right const userOpHash = await publicClient.readContract({ address: ENTRY_POINT_ADDRESS, functionName: "getUserOpHash", args: [packedUserOp], // EntryPoint calculates hash });// Privy automatically applies EIP-191 prefix const { signature } = await privySignMessage({ message: userOpHash // Raw bytes32 });// Verify before sending (caught many bugs here!) const recoveredAddress = await recoverAddress({ hash: hashMessage({ raw: userOpHash }), signature, });*Privy IntegrationUsed as embedded wallet provider (handles key management, MFA) Hacky part: Privy's wallet client doesn't natively support ERC-4337. We had to build a custom toSmartAccount() adapter that:Takes Privy's EOA wallet as the "owner" Wraps it in ERC-4337 UserOperation signing logic Handles nonce management Encodes calldata properly for EntryPointtypescript // Custom adapter - this took 5 days to get right const smartAccount = await toSmartAccount({ client: publicClient, owner: privyWalletClient, // EOA that signs UserOps entryPoint: ENTRY_POINT_ADDRESS, factoryAddress: FACTORY_ADDRESS,// Custom signature implementation async signUserOperation({ userOperation }) { const userOpHash = getUserOpHash(userOperation); const signature = await privyWalletClient.signMessage({ message: { raw: userOpHash } }); return signature; }}); Smart Contracts*Solidity DevelopmentWrote three contracts: Factory, Wallet, Registry Notable decision: Used a registry pattern instead of putting all logic in the wallet. Why?Wallets are deployed per-user (expensive to upgrade) Registry is singleton (easy to upgrade for all users) Registry holds intent logic, wallet stays simpleCommitment System ImplementationChallenge: How to "lock" funds for recurring payments without actually transferring them? Solution: On-chain accounting with mapping(address => uint256) public commitments When checking available balance: totalBalance - commitments[token] Edge case we found: What if user receives MORE tokens after committing? We calculate commitments as absolute amounts, not percentages.solidity function getAvailableBalance(address token) public view returns (uint256) { uint256 total = token == address(0) ? address(this).balance // ETH : IERC20(token).balanceOf(address(this)); // ERC-20uint256 committed = commitments[token]; return total > committed ? total - committed : 0;} *AI Integration (Vercel AI SDK 5.0)Tool Calling ArchitectureUsed Vercel AI SDK's new generateText() with tools parameter Each tool has strict Zod schema for type safety Temperature tuning: Started at 0.7 (too creative, wrong tools), ended at 0.1 (precise)typescript // Tool definition example const tools = { executeBatchEthTransfer: { description: "Send ETH to multiple recipients in one transaction", inputSchema: z.object({ recipients: z.array(z.string().regex(/^0x[a-fA-F0-9]{40}$/)), amounts: z.array(z.string().regex(/^\d+(.\d+)?$/)), }), execute: async ({ recipients, amounts }) => { // Return instruction to frontend, don't execute! return { action: "CONFIRM_BATCH_ETH", params: { recipients, amounts } }; } } };Why Gemini 2.0 Flash over GPT-4?Speed: 300ms vs 800ms average response time Cost: $0.00015/1K tokens vs $0.03/1K tokens (200× cheaper!) Tool calling accuracy: 98% vs 94% in our tests (tested with 100 ambiguous prompts) Trade-off: Less conversational, more robotic - but precision matters more for financial transactionsFrontend ArchitectureReact Query (TanStack Query) for Transaction ManagementUsed mutations for all transaction types Hacky optimization: We poll for transaction receipts instead of waiting Why? ERC-4337 bundlers don't return immediate confirmationstypescript const { mutate: executeSingleTransfer } = useMutation({ mutationFn: async (params) => { // Send UserOperation const hash = await smartAccountClient.sendUserOperation({ calls: [{ to: params.to, value: parseEther(params.amount) }] });Confirmation Dialog SystemDesign decision: AI suggests → Frontend confirms → User authorizes Built custom React Context to pass tool calls from AI route to frontend Different dialog colors for different operations (blue=single, purple=batch, orange=recurring)Envio HyperIndex IntegrationEvent Indexing SetupCreated config.yaml defining 3 contracts and 11 events Wrote TypeScript event handlers to transform raw logs into queryable entities Challenge: Envio doesn't handle contract dependencies automatically Solution: In AidraIntentRegistry events, we manually load the related Wallet entity firsttypescript // EventHandlers.ts IntentCreated.handler(async ({ event, context }) => { const walletAddress = event.params.wallet.toLowerCase();// Load or create wallet entity first let wallet = await context.Wallet.get(walletAddress); if (!wallet) { wallet = { id: walletAddress, owner: "0x0000000000000000000000000000000000000000", // Unknown createdAt: event.block.timestamp, }; await context.Wallet.set(wallet); } // Now create intent const intent = { id: event.params.intentId.toString(), wallet_id: walletAddress, // Foreign key token: event.params.token, status: "ACTIVE", totalCommitment: event.params.totalCommitment.toString(), createdAt: event.block.timestamp, }; await context.Intent.set(intent);});GraphQL Query IntegrationHacky approach: I don't use a GraphQL client library (Apollo, urql) Instead, raw fetch() calls because we only need 3 queries total Keeps bundle size small (~50KB saved)typescript // lib/envio.ts export async function getTransactionHistory(wallet: string) { const response = await fetch(ENVIO_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query:query GetTransactions($wallet: String!) { transactions( where: { wallet: $wallet } orderBy: { timestamp: desc } limit: 50 ) { id actionType recipient amount timestamp } }, variables: { wallet } }) }); return response.json(); } PayPal USD (PYUSD) IntegrationERC-20 Token HandlingPYUSD has 6 decimals (not 18 like most tokens) Bug we caught: Initially used parseEther() for PYUSD → sent 1 trillion times too much! Fix: Used parseUnits(amount, 6) specifically for PYUSDtypescript // Correct PYUSD encoding const amountInUnits = parseUnits(params.amount, 6); // Not parseEther! const transferData = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [params.to, amountInUnits], });await smartAccountClient.sendUserOperation({ calls: [{ to: PYUSD_ADDRESS, // 0xCaC524BcA292... data: transferData, value: 0n, // ERC-20 transfers don't send native ETH }] });Chainlink AutomationScheduled Payment ExecutionImplemented AutomationCompatibleInterface in AidraIntentRegistry Challenge: How to check ALL users' intents efficiently without hitting gas limits? Solution: Only check intents due in the next 5 minutes (time-windowed iteration)solidity function checkUpkeep(bytes calldata) external view returns (bool upkeepNeeded, bytes memory performData) { uint256 currentTime = block.timestamp; uint256 timeWindow = currentTime + 5 minutes;// Find intents ready for execution bytes32[] memory readyIntents = new bytes32[](50); // Max 50 per upkeep uint256 count = 0; for (uint256 i = 0; i < allIntentIds.length && count < 50; i++) { Intent storage intent = intents[allIntentIds[i]]; if (intent.active && intent.nextExecutionTime <= timeWindow) { readyIntents[count] = allIntentIds[i]; count++; } } return (count > 0, abi.encode(readyIntents, count)); }Particularly Hacky/Notable ThingsNo Backend Database: Everything is either on-chain or indexed by Envio. I have ZERO backend database (Postgres, MongoDB). This makes the app fully decentralized but required creative solutions for user-specific data. AI Safety Pattern: The AI model runs on Vercel Edge Functions, but we intentionally DON'T give it access to private keys or signing capability. The frontend controls ALL execution. The AI is essentially a "smart form auto-filler" - it just returns JSON instructions.Signature Debugging Tool: I built a custom debugging page (/debug-signature) that shows:UserOperation hash (what EntryPoint calculates) Signed message hash (what Privy signs) Recovered signer address This caught MANY signature mismatches during developmentType Safety Everywhere: We use Zod schemas as the "source of truth":AI tool inputs are validated with Zod Frontend forms use the same Zod schemas (via react-hook-form) Contract ABI types are generated with wagmi generate This prevents entire classes of bugs (type mismatches, missing fields)Technologies Used & WhyVercel AI SDK 5.0: Best DX for tool calling, excellent streaming support Google Gemini 2.0 Flash: 200× cheaper than GPT-4, faster, accurate tool calling Privy: Easiest embedded wallet with great UX (email login, social auth) viem: Modern Ethereum library, better TypeScript support than ethers.js Envio: 10× faster than The Graph, easier to set up, cheaper hosting PayPal USD: Consumer-friendly stablecoin, widely recognized brand Chainlink Automation: Most reliable keeper network (99.9% uptime) Next.js 15: App router for API routes + frontend in one codebase TanStack Query: Best React state management for async operations shadcn/ui: Copy-paste components (no heavy library dependency)Partner Technology BenefitsEnvio: Made transaction history instant (50ms queries vs 500ms with The Graph). Critical for AI responses feeling "snappy." PYUSD: Enabled dollar-denominated payments, making it accessible to non-crypto users ("Send $5" vs "Send 0.000X ETH")Total Development TimeTHE FULL THREE WEEKS AND I COULDNT STILL DO ALL I WANTED, BUT THE CORE SHOULD WORK.
Hackathon
ETHOnline 2025
2025
Contributors
- Stoneybro
67 contributions