Stage 5 of 6 · Estimated 4–6 weeks
This stage assembles all previous knowledge into the ability to build, deploy, and operate production-grade decentralized applications. You will learn wallet integration, Web3 libraries, on-chain/off-chain interaction patterns, indexing and querying blockchain data, production deployment infrastructure, and the architectural decisions that separate amateur dApps from production systems. You will understand the full vertical stack — from a user clicking a button in a browser, to a transaction landing on-chain, to indexed data being served back to the UI.
Knowledge of smart contracts alone does not produce a usable product. The largest fraction of dApp security and UX failures happen at the application layer: improper wallet handling, missing transaction error recovery, inadequate state synchronization, centralized dependencies that become single points of failure. This stage bridges the gap between protocol knowledge and deployed products.
You can build a complete dApp from scratch including: wallet connection, contract interaction, real-time state updates, transaction lifecycle management, indexed data queries, and production deployment on real infrastructure. You can reason about the centralization risks in your stack and mitigate them. You can design a system that handles network failures, transaction failures, and chain reorganizations gracefully.
Wallet Architecture
├── Accounts, private keys, and seedphrases
├── EIP-1193 Provider API
├── WalletConnect and multi-wallet support
└── ERC-4337 Account Abstraction
│
▼
Web3 Libraries and RPC
├── ethers.js / viem
├── wagmi + React integration
├── JSON-RPC API surface
└── Provider management (failover, load balancing)
│
▼
Contract Interaction Patterns
├── Encoding calls and decoding results
├── Event listening and log parsing
├── Multicall batching
├── Transaction lifecycle (submission → confirmation → finality)
├── Error handling and reverts
└── Simulation (eth_call before eth_sendTransaction)
│
▼
Frontend Architecture for dApps
├── State management for blockchain state
├── Optimistic UI updates
├── Handling chain reorganizations
├── Multi-chain support
└── Gas estimation and user experience
│
▼
Backend + Blockchain Architecture
├── When to use a backend
├── Off-chain computation, on-chain settlement
├── Signature-based authorization
└── Relayer patterns (meta-transactions, ERC-2771)
│
▼
Indexing and Querying Blockchain Data
├── Why raw RPC is insufficient
├── The Graph Protocol (subgraphs)
├── Custom indexers (Ponder, Envio)
├── Event-driven architecture
└── Historical data queries
│
▼
Production Infrastructure
├── RPC provider strategy (Infura, Alchemy, self-hosted)
├── IPFS and decentralized storage (Pinata, web3.storage, Filecoin)
├── Smart contract deployment strategies
├── Monitoring and alerting (on-chain and off-chain)
└── Security hardening of the application stack
What a Wallet Is
A crypto wallet does not store currency — it stores private keys (or the seed phrase that derives them). The actual assets exist as state on the blockchain; the wallet is the tool that generates, stores, and uses private keys to produce valid signatures.
HD Wallets and BIP-32/39/44
Modern wallets use hierarchical deterministic (HD) key derivation:
m/44'/60'/0'/0/index for Ethereum addresses. The path
encodes: purpose (44), coin type (60 = ETH), account, change, and
address index.
EIP-1193: The Browser Provider Standard
Browser wallets (MetaMask) inject a provider object at
window.ethereum implementing EIP-1193:
// Request accounts
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => { ... });
// Listen for chain changes
window.ethereum.on('chainChanged', (chainId) => { ... });
// Send a transaction
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{ from, to, value, data, gas }]
});
WalletConnect
WalletConnect (v2) is a protocol for connecting mobile wallets to dApps. It uses a relay server for QR code pairing and encrypted message passing. The relay can be self-hosted. WalletConnect enables: mobile wallet connections, hardware wallet connections via bridge apps, and cross-device session persistence.
ERC-4337: Account Abstraction
ERC-4337 introduces a new transaction type — UserOperations — that allows smart contract wallets to be first-class accounts without protocol changes:
EntryPoint contract: A global singleton that validates
and executes UserOperations.
UserOperation: A signed intent (not a direct
transaction). Contains: sender, callData,
signature, paymasterAndData.
Bundlers: Nodes that aggregate UserOperations into a
single L1 transaction (like block proposers for UserOps).
Paymasters: Contracts that can sponsor gas fees for
users, or accept ERC-20 tokens for gas instead of ETH.
Account abstraction enables: gasless transactions (paymaster sponsors gas), session keys (temporary limited signing keys), social recovery (approve transactions via multiple signers), and batch transactions.
Wallet UX is the largest bottleneck to blockchain adoption. ERC-4337 eliminates the requirement for users to hold ETH before they can do anything, enables one-click transactions (session keys), and makes recovery possible without a seed phrase. As an engineer, understanding wallet architecture lets you design better onboarding (gasless, social login) and avoid wallet integration bugs that lock users out.
| Tool | Purpose |
|---|---|
| MetaMask SDK | Browser extension and mobile wallet |
| WalletConnect v2 | Multi-wallet connection protocol |
wagmi |
React hooks library for wallet connection |
viem |
TypeScript-native Ethereum library |
permissionless.js |
ERC-4337 (smart contract wallet) SDK |
| Safe SDK | Multi-sig wallet integration |
m/44'/60'/0'/0/0 starting from a known seed phrase. What
is each level of the path for?
eth_sign and
personal_sign (EIP-191)? Why is
eth_sign dangerous?
ethers.js,
generate an HD wallet from a mnemonic. Derive the first 5 addresses at
the standard BIP-44 path. Verify the addresses match MetaMask for the
same seed.
Build a Wallet Connection Component. A React component that: supports MetaMask, WalletConnect v2, and Coinbase Wallet simultaneously; handles chain switching (display an error if the user is on wrong chain, offer to switch); shows connected address, ENS name resolved, and ETH balance; handles disconnection and re-connection gracefully; and detects and handles the case where the user changes accounts or chains in MetaMask mid-session. This seemingly simple component requires handling a dozen edge cases.
The JSON-RPC API
Every Ethereum node exposes a JSON-RPC API — a stateless request-response protocol over HTTP or WebSocket. Key methods:
| Method | Purpose |
|---|---|
eth_blockNumber |
Latest block number |
eth_getBalance(addr, block) |
ETH balance at block |
eth_call(tx, block) |
Execute a call without creating a transaction |
eth_estimateGas(tx) |
Estimate gas for a transaction |
eth_sendRawTransaction(rawTx) |
Submit a signed transaction |
eth_getTransactionReceipt(hash) |
Get receipt (includes logs, status) |
eth_getLogs(filter) |
Get event logs matching a filter |
eth_subscribe(event) |
WebSocket subscription for new blocks/logs |
debug_traceTransaction(hash) |
Full execution trace (node-specific) |
ethers.js v6
The most widely used JavaScript library for Ethereum:
import { ethers } from "ethers";
// Connect to provider
const provider = new ethers.JsonRpcProvider(process.env.ETH_RPC_URL);
// Connect wallet
const wallet = new ethers.Wallet(privateKey, provider);
// Interact with contract
const contract = new ethers.Contract(address, abi, wallet);
const result = await contract.balanceOf(userAddress);
// Send transaction
const tx = await contract.transfer(recipient, amount);
const receipt = await tx.wait(); // Wait for 1 confirmation
viem
A newer, TypeScript-native library with a focus on type safety and tree-shakability:
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const balance = await publicClient.getBalance({ address: "0x..." });
const walletClient = createWalletClient({ chain: mainnet, transport: http() });
const hash = await walletClient.sendTransaction({
to: "0x...",
value: parseEther("1"),
});
wagmi (React hooks)
wagmi wraps viem with React hooks for state management, caching, and wallet connection:
import { useReadContract, useWriteContract, useAccount } from 'wagmi'
function TokenBalance() {
const { address } = useAccount()
const { data: balance } = useReadContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address],
})
return <div>{balance?.toString()}</div>
}
Multicall
Batch multiple eth_calls into a single RPC call using the
Multicall3 contract (deployed at
0xcA11bde05977b3631167028862bE2a173976CA11 on most EVM
chains):
const multicall = await publicClient.multicall({
contracts: [
{
address: TOKEN_A,
abi: erc20Abi,
functionName: "balanceOf",
args: [user],
},
{
address: TOKEN_B,
abi: erc20Abi,
functionName: "balanceOf",
args: [user],
},
{
address: TOKEN_C,
abi: erc20Abi,
functionName: "balanceOf",
args: [user],
},
],
});
// Returns: [balanceA, balanceB, balanceC] in one RPC request
Understanding the raw JSON-RPC layer lets you debug library issues, build custom tooling, and reason about performance. Choosing the right library (ethers vs viem) and integration pattern (wagmi vs custom hooks) has significant impact on bundle size, type safety, and developer experience. Multicall is essential for performant dApps — a dApp that makes 50 individual RPC calls for a portfolio view will be slow and rate-limited.
| Tool | Purpose |
|---|---|
ethers.js v6 |
Comprehensive Ethereum JS library |
viem |
Modern TypeScript-native Ethereum library |
wagmi v2 |
React hooks for Ethereum |
@tanstack/react-query |
Data fetching/caching (used by wagmi) |
| Multicall3 | Batch RPC calls |
eth-sdk |
Type-safe contract bindings generation |
eth_call and
eth_sendTransaction? Can eth_call mutate
state?
eth_estimateGas not a reliable upper bound for gas
usage? What conditions could cause a transaction to consume more gas
than estimated?
eth_getTransactionReceipt and using
eth_subscribe to listen for new blocks? What are the
trade-offs?
tx.wait(1) (wait for one confirmation) not give
you finality guarantee on Ethereum PoS? What does
tx.wait(64) not guarantee?
curl. Fetch a block, get a balance, estimate gas, and
submit a transaction. Parse the responses manually.
Build a DeFi Portfolio Tracker. Given a wallet address, fetch (via multicall): ETH balance, ERC-20 token balances for the top 20 tokens, and Uniswap V3 LP positions. Display balances with USD values (from CoinGecko API or an on-chain oracle). Handle: rate limits, missing tokens, failed calls in multicall gracefully. Target: all data fetched in <2 RPC calls.
Transaction Lifecycle (Full Detail)
(selector + args) as
data.
eth_call with the same
parameters. If it reverts, surface the error before spending gas.
eth_estimateGas.
Apply a buffer (1.2–1.5x) for non-deterministic functions.
baseFee (from block header). Query user for
maxPriorityFeePerGas preference.
eth_sendRawTransaction. Receive
txHash immediately.
eth_getTransactionReceipt returns
blockNumber, status (1=success, 0=revert),
logs.
Decoding Revert Reasons
When a transaction reverts, the receipt status is 0. The revert reason is encoded in the return data:
Error(string): Standard revert — ABI-decode as
(string) after the 0x08c379a0 selector.
MyCustomError(args) — decode using the
contract ABI.
Event Decoding
Events are stored in transaction receipts as logs. Each log
contains:
address: The contract that emitted the event.topics[0]: Keccak-256 of the event signature (e.g.,
Transfer(address,address,uint256)).
topics[1..n]: ABI-encoded indexed parameters.data: ABI-encoded non-indexed parameters.Transaction Replacement and Cancellation
A transaction in the mempool can be replaced by sending a new
transaction with the same nonce but higher
maxPriorityFeePerGas (typically +10%+). Most mempools
enforce a minimum fee bump to replace. Cancel by sending to yourself
with 0 value at the same nonce.
The Sign-Then-Submit Pattern
For operations where the user should not be exposed to blockchain latency in the UI interaction:
Transaction management is where most dApp UX fails. A dApp that doesn't handle stuck transactions, doesn't decode revert reasons clearly, or loses track of pending transactions creates a confusing and frustrating user experience. For financial applications, transaction lifecycle correctness is a safety property — a missed reorg or a double-submission could result in financial loss.
flashbots_sendBundle) allows
submitting bundles of transactions to specific blocks without the
mempool, preventing front-running.
| Tool | Purpose |
|---|---|
| Tenderly | Transaction simulation, debugging, monitoring |
| Flashbots Protect RPC | MEV-protected transaction submission |
cast send (Foundry) |
CLI transaction submission |
ethers.js transaction handling |
tx.wait(), tx.replaceTransaction()
|
eth_getTransactionReceipt returning a result to conclude
that a transaction is confirmed? What else must you check?
Error(string) and custom errors).
Test it against known reverting transactions.
Transfer events from USDC since block 18,000,000.
Decode each event and aggregate total volume.
eth_call with the
same parameters. If it fails, display the reason. Only proceed if
simulation succeeds.
Build a Transaction Manager Library. A TypeScript class that: submits transactions, monitors their status (polling, with exponential backoff), detects stuck transactions and offers replacement, handles reorgs (monitors for block reorganizations and resubmits affected transactions), and decodes revert reasons. Test it under adverse conditions: simulate network failures, OOG reverts, and nonce conflicts.
Why Raw RPC is Insufficient
eth_getLogs with a filter works for recent data, but:
For a dApp displaying "all swap events for a user's address in the last year," raw RPC is completely impractical.
The Graph Protocol
The Graph is a decentralized indexing protocol. Developers write subgraphs — definitions of which events to capture and how to store them — and Graph nodes index the data.
# subgraph.yaml
dataSources:
- kind: ethereum/contract
name: UniswapV3Pool
network: mainnet
source:
address: "0x..."
abi: UniswapV3Pool
startBlock: 12369621
mapping:
kind: ethereum/events
eventHandlers:
- event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
// mapping.ts (AssemblyScript, compiled to WASM)
export function handleSwap(event: SwapEvent): void {
let swap = new Swap(event.transaction.hash.toHex());
swap.sender = event.params.sender;
swap.amount0 = event.params.amount0;
swap.timestamp = event.block.timestamp;
swap.save();
}
Subgraphs expose a GraphQL API for querying indexed data:
query {
swaps(where: { sender: "0x..." }, orderBy: timestamp, orderDirection: desc) {
id
amount0
amount1
timestamp
}
}
Custom Indexers: Ponder and Envio
Ponder (Node.js) and Envio (Rust) are alternatives to The Graph for self-hosted indexing:
When to Use What
| Approach | When |
|---|---|
eth_getLogs |
Simple, one-time queries over small block ranges |
| The Graph (Hosted) | Cross-team, external data sharing, GraphQL API |
| Ponder (self-hosted) | Custom indexing with TypeScript, internal use |
| Dune Analytics | Ad-hoc analytics with SQL, cross-protocol queries |
| Alchemy/Quicknode APIs | Enhanced APIs for specific data types (NFT metadata, token transfers) |
Virtually every production dApp with non-trivial data requirements uses an indexer. Without indexing, leaderboards, history, analytics, and aggregate views are impossible. The Graph's hosted service has experienced outages that took major DeFi protocols offline — understanding its architecture and failure modes helps you design resilient systems with fallback indexers.
| Tool | Purpose |
|---|---|
graph-cli |
The Graph subgraph CLI |
| Graph Studio | Hosted subgraph deployment |
ponder |
TypeScript-native Ethereum indexer |
envio |
Rust-based hypersync indexer |
| Dune Analytics | SQL-based blockchain analytics |
| Goldsky | Managed subgraph hosting with mirror sync |
eth_getLogs directly to serve a
production dApp's "transaction history" page? What specific limits
does this approach hit?
Build a DEX Analytics Dashboard. Create a subgraph for a Uniswap V2 fork (or use the official Uniswap subgraph). Display in real-time: top pools by volume (24h), price charts for selected pairs, and per-wallet trade history. The application must: handle loading states, error states, and stale data gracefully; refetch data every 30 seconds; and display data within 2 seconds of page load (using caching).
When You Need a Backend
Blockchains are expensive for computation and storage. Some logic belongs off-chain:
Sign-In With Ethereum (SIWE, EIP-4361)
Authentication without a password or centralized provider:
// SIWE message construction
import { SiweMessage } from "siwe";
const message = new SiweMessage({
domain: "app.example.com",
address: userAddress,
statement: "Sign in to Example App",
uri: "https://app.example.com",
version: "1",
chainId: 1,
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
});
const prepared = message.prepareMessage();
const signature = await wallet.signMessage(prepared);
// Backend verifies: message.verify({ signature })
Meta-Transactions (ERC-2771)
Users sign a message off-chain; a trusted "relayer" submits the
transaction on-chain and pays gas. The target contract uses
_msgSender() which returns the original signer (extracted
from the forwarded call data) rather than the relayer's address.
This enables gasless UX — users never hold ETH, but still interact with contracts as if they are the sender.
Off-Chain Order Books with On-Chain Settlement
The 0x Protocol pattern:
{makerToken, takerToken, makerAmount, takerAmount, expiry}.
Benefits: No gas cost for order placement/cancellation. On-chain cost only for executed trades.
Webhook Architecture for dApps
Alchemy Notify, Moralis, and The Graph can trigger webhooks when specific on-chain events occur (e.g., NFT transferred to an address, a large trade executed). This powers: notification systems, backend state updates, fraud detection.
| Tool | Purpose |
|---|---|
siwe library |
Sign-In With Ethereum implementation |
| ERC-2771 / OpenGSN | Meta-transaction / relayer framework |
| Alchemy Notify | On-chain event webhooks |
| 0x Protocol | Off-chain orderbook, on-chain settlement |
| Seaport (OpenSea) | NFT marketplace protocol |
Build a Gasless NFT Minting dApp. Users sign a minting request off-chain. Your relayer service (a simple Express.js server) validates the request, checks eligibility (from a Merkle allowlist), and submits the mint transaction, paying gas. The NFT contract uses ERC-2771 to record the user as the minter, not the relayer. Deploy on Sepolia testnet.
RPC Provider Strategy
Never rely on a single RPC endpoint:
Pattern: Primary provider (Alchemy/QuickNode) → Fallback provider (Infura) → Self-hosted node (Geth/Erigon, for critical operations).
IPFS and Decentralized Storage
For NFT metadata: Pin to multiple services. Never use a centralized URL
as the tokenURI.
Smart Contract Deployment Strategy
forge script or hardhat-deploy). Verify
contracts on Etherscan immediately after deployment.
Monitoring and Alerting
Essential monitoring for production dApps:
Tools: Forta Network (decentralized bot monitoring), OpenZeppelin Defender, Tenderly Alerts, custom CloudWatch/Grafana dashboards.
Security Hardening of the App Stack
| Layer | Threat | Mitigation |
|---|---|---|
| Frontend | Malicious code injection (supply chain) | Content Security Policy, Subresource Integrity |
| DNS | DNS hijacking | DNSSEC, IPFS hosting (ENS) |
| RPC | Man-in-the-middle | HTTPS only, never trust unsigned RPC responses for critical operations |
| Backend | SSRF (reading blockchain or calling contracts through backend) | Validate all inputs, use allowlists |
| Smart contract | Already deployed bugs | Immutable contracts + bug bounty; upgradeable contracts + monitoring + pause |
IPFS Frontend Hosting
For maximum censorship resistance, host the frontend on IPFS and serve via ENS:
contenthash to the CID.app.uniswap.eth pointing to your IPFS-hosted frontend.
Production failures in dApps typically aren't smart contract bugs — they're infrastructure failures: RPC downtime, IPFS gateway failures, event handling bugs, insufficient monitoring. The 2017 CryptoKitties launch exposed Ethereum's scalability limits. The 2022 Wintermute hack started with a vulnerable vanity address generator — a tooling security issue. Complex systems fail at the boundaries. This stage teaches you to design and monitor those boundaries.
| Tool | Purpose |
|---|---|
| OpenZeppelin Defender | Smart contract monitoring, automation |
| Tenderly | Transaction simulation, alerting, observability |
| Forta Network | Decentralized threat detection |
| Pinata | IPFS pinning service |
Foundry forge script |
Deployment scripting |
| Blockscout | Open-source block explorer (self-hostable) |
contenthash ENS record work for IPFS-hosted
frontends? What are the trust assumptions?
forge script that: deploys a contract, verifies it on
Etherscan, and transfers ownership to a specified Gnosis Safe.
Deploy a Production-Ready dApp. Take your AMM from Stage 4 or your lending protocol from Stage 3. Build a full production deployment: (a) deploy contracts to Sepolia with Foundry scripts; (b) verify on Etherscan; (c) transfer ownership to a testnet Gnosis Safe; (d) build a React frontend with wagmi; (e) deploy frontend to IPFS; (f) set up Tenderly monitoring for 3 critical events; (g) write a post-deployment runbook (how to pause, upgrade, and monitor the system).
End-to-end: ERC-721 contract with Merkle-proof allowlist minting, metadata on IPFS, a React frontend with wagmi wallet connection, reveal mechanism (pre-reveal placeholder → post-reveal real metadata via Chainlink VRF), and Etherscan-verified deployment on testnet.
Frontend for a DAO governor contract: display all active proposals (via subgraph), show vote counts, allow token holders to cast votes, and show the timelock queue. Handle: wallet connection, token delegation, and proposal execution transactions.
Build a monitoring tool that tracks the state of assets bridged between Ethereum mainnet and an OP Stack L2. Show: pending deposits (not yet relayed), completed withdrawals, and the 7-day challenge window status for optimistic rollup withdrawals.
Build a complete, production-grade DEX (decentralized exchange) frontend and backend for your AMM from Stage 4:
| Mistake | Reality |
|---|---|
| "The RPC will always respond" | Build retry and failover logic. Treat RPC endpoints as unreliable service dependencies. |
| "Gas estimation is exact" | It's a simulation result. Add a buffer. Gas usage can differ at execution time due to state changes between estimate and inclusion. |
| "Transaction confirmed = transaction final" | Confirmed ≠ finalized. On PoS Ethereum, PoS finality takes ~13 minutes. For high-value transactions, wait for finality or PoS justification. |
| "The Graph never lies" | Subgraphs can lag, miss events, or have handler bugs. Always validate critical on-chain data via direct RPC as a secondary check. |
| "IPFS is permanent" | IPFS data persists only as long as someone pins it. Without multiple pinners, content can disappear. |
| "ENS resolves for all users" |
ENS resolution requires a compatible browser or dApp. Most users
access dApps via app.example.com DNS, not ENS.
|
Documentation
Technical Blogs
Tools Reference
evm.codes — Opcode gas costsethernodes.org — Node client diversityl2beat.com — L2 security analysisdefillama.com — TVL and protocol analytics