Industry-optimal patterns for production Web3 DApps. Combines battle-tested production patterns with 2026 best practices: Account Abstraction, embedded wallets, Permit2, on-chain indexing, invariant testing, and gasless UX.
text
Framework: Next.js 15+ App Router + React 19
State: Zustand (client) + TanStack Query v5 (server cache)
Wallet: Wagmi v2 + Viem (EVM) | @solana/web3.js (Solana)
Onboarding: Privy or Dynamic (embedded wallets + social login)
OR RainbowKit/ConnectKit (wallet modal only)
Account Abstraction: ERC-4337 SDK (ZeroDev / Alchemy AA / Biconomy)
Local DB: Dexie (IndexedDB) for offline/frontend-mode
Styling: Tailwind CSS 4 + Framer Motion
Testing: Vitest + Playwright (E2E)
text
Framework: Fastify (TypeScript ESM) or tRPC + Next.js API routes
Database: PostgreSQL + Drizzle ORM
Queue: BullMQ + Redis (strategy workers)
Auth: JWT rotation + SIWE + Google OAuth + passkeys
Chain: Viem (EVM) + @solana/web3.js
Events: EventBus (ring buffer + JSONL) → SSE push
Encryption: AES-256-GCM + PBKDF2 vault
Indexing: Ponder (self-hosted) or Envio or The Graph
text
Language: Solidity 0.8.25+ (EVM) | Anchor/Rust (Solana)
Framework: Foundry (test/deploy/verify)
Testing: Unit → Fuzz → Invariant → Mainnet Fork
Static Analysis: Slither + custom detectors
Deployment: Foundry script + CREATE2 (deterministic addresses)
Upgradeability: UUPS proxy (prefer) or Transparent proxy (standard)
NeedChoiceReasonWallet connection UI onlyRainbowKit / ConnectKitLightest, prettiestSocial login + new user onboardingPrivy / DynamicEmbedded wallets, zero frictionGasless UXERC-4337 PaymasterUsers never see gasBatch approve+swap+stakeMulticall / ERC-4337 bundling1 click, 1 txSelf-hosted indexingPonderFull control, TypeScriptDecentralized indexingThe GraphCensorship resistanceReal-time streamingGoldsky / EnvioSub-second latency
text
┌─────────────────────────────────────────────────────┐
│ FRONTEND (Next.js + React) │
│ ┌──────────┐ ┌───────────┐ ┌─────────────────────┐ │
│ │ Wagmi/ │ │ TanStack │ │ Zustand Store │ │
│ │ Viem │ │ Query │ │ (auth, settings, │ │
│ │ Hooks │ │ Cache │ │ execution mode) │ │
│ └────┬─────┘ └─────┬─────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ ┌────▼─────────────▼───────────────────▼──────────┐ │
│ │ Execution Mode Router (frontend|backend|hybrid) │ │
│ └────┬────────────────────────────────────────────┘ │
└───────┼──────────────────────────────────────────────┘
│ REST / SSE / WebSocket
┌───────▼──────────────────────────────────────────────┐
│ BACKEND (Fastify) │
│ ┌──────────┐ ┌───────────┐ ┌────────────┐ │
│ │ Auth + │ │ Services │ │ EventBus │ │
│ │ RBAC │ │ (trade, │ │ (dual-write│ │
│ │ Vault │ │ strat, │ │ ring+JSONL│ │
│ │ │ │ signal) │ │ → SSE) │ │
│ └────┬─────┘ └─────┬─────┘ └──────┬─────┘ │
│ ┌────▼─────────────▼──────────────▼──────────────┐ │
│ │ PostgreSQL + Drizzle │ BullMQ Workers │ │
│ └────┬────────────────────┴──────────────────────┘ │
└───────┼──────────────────────────────────────────────┘
│ RPC (health-managed)
┌───────▼──────────────────────────────────────────────┐
│ BLOCKCHAIN │
│ EVM (Ethereum, BSC, Base, Arbitrum, Polygon) │
│ Solana | Sui | other L1/L2 │
└──────────────────────────────────────────────────────┘
The #1 UX improvement for 2026. Every consumer-facing DApp should implement AA.
text
Smart Account: User's programmable contract wallet (session keys, social recovery)
EntryPoint: Singleton contract managing UserOperation verification + execution
Bundler: Service that collects UserOps and submits them on-chain
Paymaster: Contract that sponsors gas or accepts ERC-20 fee payment
typescript
// Using ZeroDev / Alchemy AA / Biconomy SDK
import { createKernelAccount, createKernelAccountClient } from '@zerodev/sdk'
const kernelAccount = await createKernelAccount(publicClient, {
plugins: { sudo: ecdsaValidator },
entryPoint: ENTRYPOINT_ADDRESS_V07,
})
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain, bundlerTransport: http(bundlerUrl),
middleware: { sponsorUserOperation: pimlicoBundler.sponsorUserOperation },
})
// User does approve+swap in ONE click, gas paid by paymaster
await kernelClient.sendUserOperation({
userOperation: { callData: encodeFunctionData({ abi, functionName: 'multicall', args: [...] }) },
})
text
Existing EOA users can temporarily gain AA features (batching, paymaster)
without deploying new contract or moving funds.
Use when: migrating existing user base to AA gradually.
text
Pre-approve specific actions for a time window:
- "Allow this game contract to call move() for 24 hours, max 0.1 ETH"
- User signs once → session key auto-signs subsequent txs
- Revocable at any time
typescript
// Privy — social login creates wallet automatically
import { PrivyProvider, usePrivy } from '@privy-io/react-auth'
<PrivyProvider appId={PRIVY_APP_ID} config={{
loginMethods: ['email', 'google', 'apple', 'twitter', 'passkey'],
embeddedWallets: { createOnLogin: 'users-without-wallets' },
appearance: { theme: 'dark' },
}}>
<App />
</PrivyProvider>
// User signs in with email → gets wallet → never sees "MetaMask" popup
typescript
// Wagmi + RainbowKit / ConnectKit
import { WagmiProvider, createConfig, http } from 'wagmi'
import { mainnet, base, bsc, arbitrum } from 'wagmi/chains'
import { RainbowKitProvider, connectorsForWallets } from '@rainbow-me/rainbowkit'
const config = createConfig({
chains: [mainnet, base, bsc, arbitrum],
transports: { [mainnet.id]: http(), [base.id]: http(), ... },
})
text
For apps needing persistent auth (not just wallet connection):
1. Backend generates nonce → frontend creates SIWE message → wallet signs
2. Backend verifies signature → issues JWT + refresh token
3. Session stored in DB with userId ↔ walletAddress binding
PrivyDynamicRainbowKitSocial login✅ Best✅ Great❌ NoEmbedded wallet✅✅❌Multi-chain (EVM+Sol)✅✅ BestEVM onlyCustomizationMediumHighHighCostPaidPaidFreeBest forConsumer appsEnterpriseCrypto-native
typescript
// Instead of: approve(spender, amount) → swap() [2 transactions]
// Now: sign permit → swap(permit, signature) [1 transaction]
// Using Uniswap Permit2 contract (universal, any ERC-20)
const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
// 1. User signs EIP-712 message off-chain (no gas)
const permitData = {
details: { token, amount, expiration, nonce },
spender: routerAddress,
sigDeadline: deadline,
}
const signature = await walletClient.signTypedData({ ... })
// 2. Contract uses signature to transfer tokens (1 tx)
await router.write.swap([permitData, signature, swapParams])
text
For tokens that natively implement permit():
sign off-chain → contract calls permit() + action() in same tx
Saves 1 full transaction (~46k gas)
typescript
// Batch multiple reads into 1 RPC call
const results = await publicClient.multicall({
contracts: [
{ address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: pool, abi: poolAbi, functionName: 'getReserves' },
],
})
// Batch multiple writes into 1 tx (via multicall contract or AA bundling)
// approve + swap + stake = 1 transaction
text
1. Use calldata > memory for function inputs
2. Pack storage slots (uint128 + uint128 fits in 1 slot)
3. unchecked{} blocks where overflow impossible
4. Minimize SSTORE operations (21k gas each)
5. Use events for data that doesn't need on-chain access
6. Deploy on L2 (Base/Arbitrum) for 10-100x cheaper gas
7. Dynamic gas pricing: queue non-urgent txs for low-fee windows
text
users (id, email, role, masterKeyHash, settings)
└→ user_auth_providers (provider: password|google|wallet|passkey, providerId, credential)
└→ sessions (refreshTokenHash, tokenPrefix, expiresAt, ipAddress)
text
1. Login → issue accessToken (15min) + refreshToken (7d)
2. refreshToken stored as bcrypt hash + plaintext prefix (first 16 chars)
3. Prefix indexed for O(1) lookup (avoids full-table bcrypt scan)
4. On refresh: find by prefix → bcrypt.compare → rotate → new pair
5. Logout: delete session row → old refresh invalidated
text
WebAuthn / FIDO2 authenticator:
1. Registration: user creates passkey (biometric/hardware key)
2. Challenge-response: server sends challenge → authenticator signs
3. No passwords, no seed phrases, phishing-resistant
4. Works with AA smart accounts as signer
text
Key Derivation: PBKDF2(password, randomSalt, 100_000 iterations, sha512) → 32-byte AES key
Encryption: AES-256-GCM(plaintext, key, randomIV) → { ciphertext, iv, authTag }
Per-user: Each user has own masterKey derived from their password + unique salt
Auto-lock: 8h inactivity → zero-fill key buffer → delete from memory
text
1. Wallet private keys (EVM hex / Solana base58)
2. CEX API keys (apiKey, secret, passphrase — each field encrypted independently)
3. Stored as: encryptedBlob + iv + authTag in PostgreSQL columns
4. UNIQUE(userId, exchange) — one credential set per exchange per user
text
- Zero-fill Buffer on lock: key.fill(0) before delete
- Vault MUST be unlocked for any write/decrypt operation
- List operations NEVER return decrypted values
- Approve only required token amounts (never maxUint256)
- Never log/print raw private keys or API secrets (mask: first5***last4)
typescript
// chains.ts — THE ONLY place chain data is defined
// All services import from here. Never define chain constants elsewhere.
export const VIEM_CHAINS: Record<string, Chain> = { bsc, eth: mainnet, base, arb: arbitrum }
export const WRAPPED_NATIVE: Record<string, `0x${string}`> = {
bsc: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
eth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
base: '0x4200000000000000000000000000000000000006',
}
export const EXPLORER_API: Record<string, { url: string; key: string }> = { ... }
export const CHAIN_DISPLAY: Record<string, string> = { ... }
Rule: Adding a new chain = edit chains.ts ONLY. All consumers auto-discover.
typescript
// 1. Enums for bounded values
export const chainEnum = pgEnum('chain', ['bsc', 'eth', 'base', 'arb', 'sol'])
export const statusEnum = pgEnum('status', ['created', 'running', 'paused', 'stopped', 'failed'])
// 2. text for big numbers (never float for money)
amountIn: text('amount_in').notNull(), // "1000000000000000000" not 1.0
price: text('price').notNull(), // "0.00000456" exact
// 3. jsonb for extensible config
params: jsonb('params').default({}).notNull(), // strategy config, user settings
// 4. Indexes on every FK + query pattern
(t) => ({ userIdx: index('idx_user').on(t.userId), statusIdx: index('idx_status').on(t.status) })
// 5. Unique constraints for business rules
(t) => ({ uniq: uniqueIndex('idx_uniq').on(t.userId, t.exchange) })
// 6. Separate tables per exchange (not polymorphic)
binancePositions, gatePositions, hyperliquidPositions // each with exchange-specific fields
text
users → auth → sessions → wallets → credentials
↓
strategies → trades → audit_logs
text
Emit → Ring Buffer (500, in-memory) + JSONL File (append-only, crash recovery)
→ Notify all SSE subscribers
→ Notify typed listeners (service-to-service)
typescript
type EventType =
| 'trade:pending' | 'trade:confirmed' | 'trade:failed'
| 'strategy:started' | 'strategy:stopped' | 'strategy:error'
| 'signal:detected' | 'health:rpc' | 'scanner:buy'
| 'price:update' | 'ai:decision' | 'system:info'
typescript
// Auth: JWT via query param (EventSource doesn't support headers)
// Heartbeat: every 30s
// Reconnect: auto (EventSource default behavior)
// Frontend: useServerEvents(types[], callback) hook with mode-awareness
typescript
// ponder.config.ts
export default createConfig({
networks: { mainnet: { chainId: 1, transport: http(RPC_URL) } },
contracts: {
Router: { network: 'mainnet', abi: routerAbi, address: ROUTER, startBlock: 12345678 },
},
})
// src/Router.ts — event handler
ponder.on('Router:Swap', async ({ event, context }) => {
await context.db.Swap.create({ id: event.log.id, data: { ... } })
})
text
Canonical (Ponder/Envio): Poll-based, reorg-safe, source of truth
Real-time (WebSocket/SSE): Push-based, for instant UI updates, NOT authoritative
Reconciliation: Periodically compare real-time state with canonical
text
REST API → Service → BullMQ Queue → Worker → DB + EventBus
typescript
const worker = new Worker('dca', async (job) => {
const { strategyId, userId, params } = job.data
try {
const wallet = await vault.getDecryptedKey(userId, params.walletId)
const result = await tradeExecutor.execute({ ... })
await db.insert(trades).values({ userId, strategyId, status: 'confirmed', txHash: result.hash })
emitEvent('trade:confirmed', { strategyId, txHash: result.hash })
} catch (err) {
emitEvent('trade:failed', { strategyId, error: err.message })
throw err // BullMQ handles retry
}
}, { connection: redis, concurrency: 5 })
created → running → paused → stopped/completed/failed#POST /api/strategies/stop-all → kill all jobs + update DB#typescript
// Per-chain: [{ url, healthy, latencyMs, failCount }]
function getBestRpc(chain: string): string {
const healthy = nodes.filter(n => n.healthy).sort((a,b) => a.latencyMs - b.latencyMs)
if (healthy.length > 0) return healthy[0].url
return nodes.sort((a,b) => a.failCount - b.failCount)[0].url // graceful degradation
}
// Success: failCount=0, update latency
// Failure: failCount++; ≥3 → mark unhealthy
// Probe: eth_blockNumber every 30s, 5s timeout
text
1. Query all V2 routers: getAmountsOut()
2. Query all V3 quoters: quoteExactInputSingle() × [100, 500, 3000, 10000] fee tiers
3. Sort by amountOut descending → select best
4. Turbo mode: direct pool.swap() bypasses router, saves 15-30k gas
text
✓ Approve ONLY required amount (never maxUint256)
✓ getAmountsOut fails → BLOCK trade (prevent sandwich)
✓ amountOutMin = expectedOut × (10000 - slippageBps) / 10000
✓ Insert trade row BEFORE execution (audit trail)
✓ Emit event after completion (trade:confirmed / trade:failed)
text
Smart Contracts: Foundry (forge test)
- Unit tests: Individual function tests
- Fuzz tests: Property-based, random inputs
- Invariant tests: Stateful fuzzing, protocol properties
- Fork tests: Test against mainnet state (Anvil)
Application: Vitest
- Unit: Business logic, utils, hooks
- Integration: API routes, DB queries, service logic
- E2E: Playwright for wallet flows
Static Analysis: Slither → CI pipeline
CI Pipeline:
1. forge test --fuzz-runs 1000
2. slither .
3. vitest run
4. playwright test (E2E)
solidity
// Define protocol properties that must ALWAYS hold
function invariant_totalSupplyMatchesBalances() public {
assertEq(token.totalSupply(), sumOfAllBalances);
}
// Handler contracts guide fuzzer through realistic user journeys
#RuleWhy1AES-256-GCM + PBKDF2 for all keys at restIndustry standard encryption2Zero-fill key buffers on vault lockPrevent memory scanning3Approve exact amount, never maxUint256Prevent unlimited drain4Block trade on quote failurePrevent sandwich attacks5JWT + refresh token rotationPrevent token reuse6tokenPrefix index for O(1) lookupPrevent full-table bcrypt scan7Rate limit all endpoints (IP + user)Prevent DoS8Audit log all mutationsForensic trail9SSE requires JWT authPrevent unauthorized streaming10User isolation on ALL queriesPrevent data leaks11Timelocks on privileged operationsDetection window12Pin dependencies + lockfilePrevent supply chain attacks13Invariant tests + fuzz in CICatch edge cases automatically14Slither static analysis in CIAutomated vulnerability scan15Passkey/hardware key for adminPhishing-resistant admin access
text
Frontend: Vercel (Next.js native) or Cloudflare Pages
Backend: Docker on VPS + Nginx reverse proxy
Database: Managed PostgreSQL (Neon / Supabase / RDS)
Redis: Managed Redis (Upstash serverless / ElastiCache)
Indexer: Ponder on VPS or managed service
Contracts: forge script + CREATE2 for deterministic addresses
Monitoring: Prometheus + Grafana (self-hosted) or Datadog
Logs: Structured JSON → aggregation pipeline
Environment:
NEXT_PUBLIC_CHAIN_ID, API_BASE_URL
DATABASE_URL, REDIS_URL
JWT_SECRET, VAULT_TIMEOUT_MS
BUNDLER_URL, PAYMASTER_URL
RPC_ETH, RPC_BSC, RPC_BASE (multiple per chain for failover)
This skill recommends many third-party services. Below is the complete registry of environment variables, their purpose, which service needs them, and when the agent should request them.
VariableService/PurposeRequired WhenSensitivityDATABASE_URLPostgreSQL connection stringBackend init🔴 SecretREDIS_URLRedis/BullMQ connection stringBackend init🔴 SecretJWT_SECRETAuth token signingBackend init🔴 SecretVAULT_TIMEOUT_MSVault auto-lock durationBackend init⚪ ConfigRPC_ETHEthereum RPC endpointChain interaction🟡 SensitiveRPC_BSCBSC RPC endpointChain interaction🟡 SensitiveRPC_BASEBase RPC endpointChain interaction🟡 SensitiveRPC_ARBArbitrum RPC endpointChain interaction🟡 SensitiveBUNDLER_URLAA UserOp bundler endpointAccount Abstraction🟡 SensitivePAYMASTER_URLGas sponsorship endpointGasless UX🟡 SensitivePRIVY_APP_IDPrivy embedded walletSocial login onboarding🟡 SensitivePRIVY_APP_SECRETPrivy server-side authBackend Privy verification🔴 SecretDYNAMIC_ENV_IDDynamic embedded walletAlt onboarding provider🟡 SensitiveNEXT_PUBLIC_CHAIN_IDDefault chain for frontendFrontend init⚪ ConfigAPI_BASE_URLBackend URL for frontendFrontend init⚪ ConfigEXPLORER_API_KEY_ETHEtherscan API keyContract verification🟡 SensitiveEXPLORER_API_KEY_BSCBscScan API keyContract verification🟡 SensitivePONDER_RPC_URL_*RPC for Ponder indexerIndexer setup🟡 SensitiveDEPLOYER_PRIVATE_KEYContract deployment signerforge script deploys🔴 Critical
text
⚪ Config: Safe to commit to .env.example as placeholder values
🟡 Sensitive: Must be in .env (gitignored), contains API keys or endpoints
🔴 Secret: Must NEVER appear in logs, code, or git history
🔴 Critical: Private keys — highest security, see §XIX rules
text
Rule 1: NEVER invent, generate, or hardcode real secrets. Use placeholder values only.
Rule 2: When scaffolding, create .env.example with descriptive placeholders:
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=change-me-to-a-64-char-random-string
DEPLOYER_PRIVATE_KEY=0x_DO_NOT_COMMIT_REAL_KEY
Rule 3: Add .env to .gitignore BEFORE any .env file is created.
Rule 4: When a feature requires a credential not yet configured:
→ Inform the user which variable is needed and why
→ Provide the service's signup URL
→ Ask the user to set it in .env
→ NEVER ask the user to paste secrets into chat
Rule 5: For DEPLOYER_PRIVATE_KEY, always recommend:
→ cast wallet new (Foundry) to generate locally
→ Hardware wallet / Ledger for mainnet deploys
→ NEVER use a funded mainnet key in .env on a dev machine
This section defines mandatory rules for any AI agent following this skill when handling private keys, API secrets, and credential material during development, testing, and deployment.
text
1. NEVER echo, log, or print private keys or API secrets to stdout/stderr.
2. NEVER include real secrets in code, comments, commit messages, or artifacts.
3. NEVER store secrets in source-controlled files (only .env, which is gitignored).
4. NEVER transmit secrets over unencrypted channels.
5. When writing code that handles secrets at runtime:
→ Use environment variables (process.env.XXX)
→ Validate presence at startup: if (!process.env.JWT_SECRET) throw new Error(...)
→ Mask in logs: key.slice(0,5) + '***' + key.slice(-4)
6. When writing Foundry deploy scripts:
→ Use --private-key only on testnets
→ For mainnet: use --ledger or --trezor flags
→ NEVER pass private keys as CLI arguments in production
7. When creating Docker/CI configs:
→ Use secrets managers (Docker secrets, GitHub Actions secrets)
→ NEVER bake secrets into Docker images or CI YAML
text
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ GENERATION │──▶│ STORAGE │──▶│ USAGE │
│ │ │ │ │ │
│ cast wallet │ │ .env file │ │ process.env │
│ openssl rand │ │ Vault table │ │ Vault decrypt│
│ Service UI │ │ HW wallet │ │ --ledger │
└──────────────┘ └──────────────┘ └──────────────┘
│
┌─────▼─────┐
│ ROTATION │
│ JWT: 15m │
│ Refresh:7d│
│ API keys: │
│ 90d max │
└───────────┘
The agent should always generate this file when scaffolding a new project:
bash
# === Core Infrastructure ===
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=CHANGE_ME_run_openssl_rand_hex_32
VAULT_TIMEOUT_MS=28800000
# === Blockchain RPC (get from: https://alchemy.com or https://infura.io) ===
RPC_ETH=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
RPC_BSC=https://bsc-dataseed1.binance.org
RPC_BASE=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
RPC_ARB=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
# === Account Abstraction (get from: https://zerodev.app or https://pimlico.io) ===
BUNDLER_URL=https://api.pimlico.io/v2/YOUR_CHAIN/rpc?apikey=YOUR_KEY
PAYMASTER_URL=https://api.pimlico.io/v2/YOUR_CHAIN/rpc?apikey=YOUR_KEY
# === Embedded Wallet (choose one: https://privy.io or https://dynamic.xyz) ===
PRIVY_APP_ID=your-privy-app-id
PRIVY_APP_SECRET=your-privy-app-secret
# DYNAMIC_ENV_ID=your-dynamic-env-id
# === Frontend ===
NEXT_PUBLIC_CHAIN_ID=1
API_BASE_URL=http://localhost:3001
# === Contract Deployment (NEVER commit a funded mainnet key) ===
DEPLOYER_PRIVATE_KEY=
# ↑ This is Foundry's default Anvil key #0 — safe for local dev only
# === Block Explorer APIs (for contract verification) ===
EXPLORER_API_KEY_ETH=your-etherscan-key
EXPLORER_API_KEY_BSC=your-bscscan-key
text
1. Init: Next.js 15+ (frontend) + Fastify (backend) + Foundry (contracts)
2. Copy .env.example → .env, fill in real values (see §XVIII for each variable)
3. Ensure .env is in .gitignore
4. Define chains.ts (single source of truth)
5. Design DB schema: users, auth_providers, sessions, wallets, credentials, strategies, trades
6. Build Vault: AES-256-GCM + PBKDF2, auto-lock, zero-fill
7. Implement auth: JWT rotation + SIWE + social login (Privy/Dynamic)
8. Choose wallet strategy: embedded (Privy) for consumer, RainbowKit for crypto-native
9. Implement AA: Paymaster for gasless UX, Permit2 for gasless approvals
10. Build EventBus: ring buffer + JSONL + SSE
11. Set up BullMQ workers for async strategies
12. Build RPC health manager (30s probe, 3-fail threshold)
13. Implement trade executor: smart router, slippage protection, exact approvals
14. Set up indexer: Ponder (self-hosted) or The Graph (decentralized)
15. Testing: Foundry (fuzz+invariant) + Vitest + Playwright
16. CI: forge test → slither → vitest → playwright → deploy
17. Deploy: Vercel (frontend) + Docker (backend) + managed DB/Redis
Web3 DApp Builder v1.1.0 — Industry best practices, 2026