A non-custodial wallet is a solved problem. A Web3 super app — one that handles a stablecoin, a DEX, lending and borrowing, cross-chain bridges, NFTs, gasless transactions, and multisig accounts across iOS, Android, and web — is not. Building one revealed architectural constraints that no single tutorial addresses, because no single tutorial covers the full stack at once.
This post covers the decisions that determined whether the system held together or fell apart when all the DeFi primitives were running simultaneously.
Non-Custodial Key Management at Scale
Non-custodial means the user controls their private key — not you. This is a promise with significant engineering consequences. If your backend ever touches a user's private key in plaintext, you've broken the non-custodial model, regardless of your intentions.
The architecture we used: key generation and signing happen exclusively on the client device. The backend never sees private keys. For mobile, keys are stored in the device's secure enclave (iOS Secure Enclave, Android Keystore) and are never exported. For web, keys live in browser storage with client-side encryption, with the option to export a BIP-39 seed phrase for external backup.
The complexity comes from multi-device access. Users expect to use the same wallet on their phone and their laptop. The solution: an encrypted keystore backup that the user can restore on a new device, with the encryption key derived from a password or biometric that never leaves the client. The backend stores only the encrypted blob — useless without the client-side key.
Gasless Transactions: The UX Layer Nobody Talks About
Gas fees are the biggest UX friction point in Web3. New users shouldn't need to acquire ETH just to use a dApp. We implemented EIP-4337 (account abstraction) with a Paymaster contract that sponsored gas fees for eligible transactions, funded from protocol revenue.
Account abstraction changes the wallet model significantly. Instead of EOAs (externally owned accounts), users have smart contract wallets — which enables features that EOAs can't do: transaction batching (approve + swap in one transaction), social recovery, and session keys for limited delegated signing.
The operational complexity: the Paymaster needs to be adequately funded and the bundler (which submits UserOperations to the network) needs to be reliable. Both became critical infrastructure with their own monitoring requirements. A Paymaster that runs out of funds doesn't fail loudly — users just start getting errors with no clear explanation.
Composing DeFi Primitives Without Creating a Security Disaster
A DEX, a lending protocol, a stablecoin, and a bridge are each their own attack surface. Composing them in a single application multiplies the risk surface. The principle we applied: every primitive is isolated at the contract level, with no shared state between protocols except through explicit, audited integration contracts.
The DEX used an AMM model (Uniswap V3 fork) with concentrated liquidity. The lending protocol used an over-collateralization model with on-chain price oracles for liquidation triggers. The stablecoin was algorithmic, backed by the lending protocol's collateral pool. Each of these contracts went through independent audits before integration — and the integration contracts themselves were audited separately.
One hard lesson: flash loan attack vectors in composed systems are non-obvious. An action that's safe when executed across multiple blocks can become exploitable when a flash loan lets an attacker execute the entire sequence atomically. Every integration point was reviewed specifically for flash loan attack surface, not just standard reentrancy.
Multi-Platform Without the Rewrite Tax
Supporting iOS, Android, and web from a single codebase was a product requirement. The constraint: the non-custodial key management logic had platform-specific implementations (Secure Enclave on iOS, Keystore on Android, SubtleCrypto on web), but the DeFi logic above it needed to be shared.
The architecture: a core library in Rust handling cryptographic operations and key management, compiled to native code for mobile and to WASM for web. Above the Rust core, a React Native layer for mobile UI and a Next.js layer for web, sharing as much state management and contract interaction logic as possible through a TypeScript SDK.
This added complexity in the build pipeline — Rust compilation for three targets, WASM bundling, and native module linking — but the alternative (maintaining three separate codebases for crypto-sensitive code) created worse risks than the build complexity. A key management bug fixed in one platform would have to be manually applied to the others. That's not a trade-off worth making.
The Multisig Case: When Users Need More Than One Key
For business accounts and high-value use cases, single-key wallets aren't enough. We implemented multisig (M-of-N signing) using the account abstraction layer rather than Gnosis Safe clones, which gave us gasless multisig transactions — a combination that traditional multisig wallets don't offer.
The UX challenge: a 2-of-3 multisig transaction requires two signers to independently approve before anything happens. The coordination layer — notifying signers, collecting signatures off-chain, submitting when threshold is reached — is entirely application infrastructure. It doesn't exist on-chain. Building it reliably, with proper state management for pending transactions and signer notifications, took as much effort as the contract work.
