A web3 development company can ship a clean interface, pass a demo on Sepolia, and still leave a client with a fragile production system: one public RPC endpoint, wallet logic glued to window.ethereum, no indexed read layer, and an admin key that can rewrite the protocol’s risk profile in a single transaction.
That is not an edge case. It is what happens when a team treats blockchain development frameworks as a coding preference instead of an operating model. The stack determines latency, retry behavior, deployment reproducibility, oracle-read integrity, incident response, and recurring infrastructure cost. Contracts are only one execution layer. The production system is much larger.
The baseline is not one universal toolchain. There isn’t one. But competent teams make their choices visible: framework, local chain, RPC topology, wallet provider boundary, indexing path, key management, and monitoring. If those answers arrive as a logo wall, the architecture has not been stress-tested.
Core development frameworks and local execution environments
The first useful question is not “Do you use Solidity?” Every EVM shop uses Solidity or touches it eventually. The useful question is: what happens between a contract change and a reproducible test result?
A practical EVM stack commonly combines a development environment with a local execution node and command-line tooling. Hardhat 3 covers Solidity testing, TypeScript testing, deployment, verification, debugging, code coverage, and multichain simulation. Foundry splits the workflow more aggressively:
- Forge compiles, tests, and deploys Solidity contracts.
- Cast queries chains and sends transactions from the terminal.
- Anvil provides a local EVM node for fast deployment and stateful testing.
- Viem sits naturally at the TypeScript boundary for reads, writes, account interactions, and transport control.
Anvil’s default chain ID is 31337. That sounds trivial until a frontend silently points to the local network, a deployment script signs against the wrong chain, or a test fixture assumes production block timing. Chain IDs are not decoration. They are routing controls.
A solid web3 development company should be able to show how it handles three separate test surfaces:
| Surface | What it is for | What it cannot prove |
|---|---|---|
| Local EVM node | Fast unit tests, forks, impersonation, deterministic failure cases | Real public-network congestion, external service behavior, wallet quirks |
| Sepolia | Public integration testing for Ethereum applications | Production liquidity, mainnet transaction competition, full operational load |
| Hoodi | Validator and staking-oriented testing | A default dApp integration environment |
| Mainnet fork | Reproducing live state and contract interactions | Mainnet resilience under actual traffic unless executed on mainnet |
Sepolia is the recommended default public Ethereum testnet for application development. Holesky was deprecated in September 2025, so a team still making it the centerpiece of a general dApp deployment workflow is carrying stale operational assumptions. Hoodi has a different job: validator and staking testing.
The distinction matters most for oracle-dependent protocols. Local tests can mock a feed response. Fork tests can call against a historical state. Sepolia can validate the full transaction path. None of those alone proves that a protocol will behave safely when a feed update is delayed, a sequencer is unavailable, or a price crosses the contract’s deviation threshold during a volatile block window.
A green test suite measures code paths. It does not measure the operational reality around those paths.
The framework decision is less about Hardhat versus Foundry than about test density and deployment discipline. The team should have answers for:
1. How are forks pinned? A fork without a block number is a moving target. Reproducing an incident requires the exact state, not “roughly yesterday’s mainnet.”
2. How are deployment artifacts tracked? Bytecode, constructor arguments, proxy implementation addresses, compiler versions, optimizer settings, and verification state need a durable record.
3. How are privileged paths tested? Pauses, feed-address changes, role transfers, upgrade authorization, emergency withdrawals. These are often the highest-value tests.
4. How are failures injected? Reverts from external calls, stale oracle data, delayed responses, incorrect decimals, missing liquidity, and failed transaction replacement.
5. What runs before a signer can deploy? Compilation is not a gate. Tests, static analysis, simulation, role validation, and bytecode comparison are separate gates.
For a client evaluating a delivery partner, this is where a repository walkthrough beats a polished architecture diagram. Ask to see a failed test for stale data. Ask to see the deployment manifest. Ask how the team recreates a transaction failure at a fixed block. The answer should take minutes, not a week of “we’ll check internally.”
RPC node selection is a latency and failure-budget decision
Most applications begin with a managed JSON-RPC provider. That is rational. Running full nodes, archive nodes, load balancers, storage, observability, and chain-specific upgrade procedures is not free. It is not even cheap in engineering attention.
Managed platforms expose JSON-RPC endpoints so the product team does not need to operate every chain node itself. Providers such as Alchemy document access across more than 100 blockchains through one API key. Infura similarly positions managed API access as an alternative to direct node operation. That solves a real problem, but it does not eliminate architecture.
The mistake is treating an RPC URL as plumbing. It is a production dependency with its own throughput ceiling, rate limits, geographic latency, data-retention characteristics, and failure modes.
| Decision point | Managed RPC | Self-hosted nodes |
|---|---|---|
| Initial operating load | Low. Endpoint, credentials, usage controls | High. Node sync, storage, upgrades, monitoring |
| Control over data and methods | Provider-defined capabilities | Full control, subject to client and hardware limits |
| Archive and trace requirements | Plan- and chain-dependent | Possible, but storage and maintenance rise sharply |
| Incident isolation | Provider outage may affect all traffic | Team owns diagnosis and recovery |
| Cost shape | Variable usage spend | Infrastructure spend plus operator time |
| Best fit | Fast product iteration, multichain expansion | Specialized tracing, deterministic access, high-control workloads |
There is no universal winner. A trading terminal querying historical state, a risk engine consuming rapid oracle updates, and a consumer wallet sending occasional transactions have different read/write profiles.
Start with traffic classification:
- Critical writes: deployments, liquidations, keeper executions, settlement calls. These need explicit retry policy, nonce management, confirmation logic, and a fallback route that does not produce duplicate transactions.
- Latency-sensitive reads: latest price, account health, pool reserves, block number. These need caching rules and freshness limits, not blind retries.
- Heavy historical reads: logs, traces, state at historical blocks. These are where archive capability and provider-specific limits become visible.
- Background indexing: event backfills and reconciliation. This traffic should not compete with a user’s transaction path.
- Browser-originated reads: potentially large, potentially abusive, and often exposed to public rate limits if teams cut corners.
A web3 SDK integration should separate these channels. Do not run user writes, backend jobs, and indexer backfills through one shared endpoint with one shared quota and call it resilient.
Viem makes this separation explicit. A Public Client is for reads such as getBlockNumber and getBalance. A Wallet Client handles actions such as sendTransaction and signMessage. A Test Client supports testing operations such as mining and account impersonation. That division is operationally useful because each client can carry its own transport, timeout, retry, and logging policy.
WebSocket usage needs the same discipline. Viem’s WebSocket transport enables reconnection by default, with up to five reconnect attempts, three request retries, and a 10,000 ms request timeout. Those defaults are a starting point, not a service-level agreement. A liquidation worker whose economic window is two blocks cannot simply inherit a 10-second timeout and hope for the best.
Public RPC fallback is another common trap. Viem warns that dropping to a public RPC URL can trigger rate limiting. More importantly, a fallback that changes response quality without observability is not resilience. It is silent degradation.
Instrument at least:
- p50, p95, and p99 latency by RPC method;
- error rate by provider, chain, and response code;
- head-block lag against an independent reference;
- log-query range failures and backfill duration;
- WebSocket disconnect frequency;
- transaction broadcast-to-inclusion time;
- gas estimation deviations versus actual gas used.
RPC redundancy without per-provider telemetry is just two opaque failure domains instead of one.
For teams building execution products, the same data hygiene applies to strategy inputs. A price stream should expose timestamp, source, confidence logic, and update cadence. A signal based on a loose spot quote is not interchangeable with a TWAP or VWAP-derived execution model. The gap becomes visible fast in automated trading and copy-trading systems; this analysis of technical criteria for evaluating DeFi AI trading tools is useful precisely because it separates signal claims from the execution and risk mechanics underneath them.
Wallet interoperability: build around EIP-1193, not browser folklore
Wallet connections fail in predictable ways: account changes that never reach application state, chain switches that leave stale balances on screen, provider-specific methods leaking into business logic, and a frontend that assumes one injected wallet forever.
EIP-1193 defines a minimal, event-driven JavaScript Ethereum Provider API. Its center is request(), plus events including connect, disconnect, chainChanged, accountsChanged, and message.
The critical detail: window.ethereum is a common browser convention, not a requirement of EIP-1193.
That distinction tells you whether a team is designing for provider interoperability or merely testing against a single extension in a single browser. The provider should sit behind an adapter layer. The application should not care whether the transport comes from an injected wallet, WalletConnect-style connection, embedded wallet, or a custom EIP-1193 provider.
A robust wallet boundary has a few hard rules:
- Rebuild chain-specific clients after
chainChanged. Cached contract instances can point to the wrong network. - Clear account-scoped reads after
accountsChanged. Balances, allowances, positions, and signatures belong to the prior identity until refreshed. - Treat disconnects as state changes, not cosmetic UI events.
- Keep signing intent explicit. A signature is not a generic “continue” button.
- Do not put secrets intended for backend use into browser-delivered source code. Public frontend environments are not secret stores.
- Keep read transports separate from user-signing providers wherever possible. A wallet should sign; it does not need to carry the full read workload.
The gas overhead here is usually negligible compared with the debugging cost of an uncontrolled wallet state machine. One clean provider abstraction pays for itself the first time a user switches networks during a multi-step transaction.
Indexing architecture: raw logs are not an application database
The first version of a dApp often reads logs directly from RPC, decodes them in the frontend, and calls it a data layer. That survives until the event range expands, a user wants a history page, an RPC provider caps the request, or a reorg forces the UI to reconcile state it never modeled.
A Subgraph provides a cleaner route for read-heavy applications. The Graph describes a Subgraph as a custom API that extracts, processes, and stores blockchain data for GraphQL queries. Its standard shape is straightforward:
subgraph.yamldeclares the data sources, network, contracts, and event handlers;schema.graphqldefines the queryable data model;mapping.tstransforms chain events into entities.
Simple, but not trivial. The schema becomes part of the product’s performance envelope.
For an oracle-integrated lending market, a useful index may include feed-update entities, price observations, user positions, liquidation events, and risk snapshots. The temptation is to index every event field and compute everything at query time. That makes GraphQL convenient and costs unpredictable.
Instead, decide which numbers must be precomputed:
- current collateral ratio;
- last accepted oracle price and timestamp;
- liquidation eligibility state;
- rolling TWAP checkpoints where the contract or risk engine uses them;
- account-level aggregates needed by the interface.
Do not confuse an indexer with a transaction node. A Subgraph is read-oriented. It does not replace RPC infrastructure for transaction submission, simulation, or contract writes.
Self-hosting Graph Node is an infrastructure project, not a Docker checkbox. It requires an EVM-compatible JSON-RPC endpoint, IPFS, and PostgreSQL. Some Subgraphs need archive-node capabilities or parity tracing. The Graph’s own operational guidance recommends separating query nodes from indexing nodes for more agile scaling. That is the correct instinct: indexer load is bursty and chain-bound; query load is user-bound. Mixing them turns a popular dashboard into a synchronization incident.
The Graph documents support for more than 60 networks, and its free plan allows 100,000 Subgraph queries per month before additional usage charges apply. Useful entry economics. Not a capacity model. Measure query depth, pagination behavior, cache hit rate, entity growth, and reindex time after schema changes.
For high-value protocols, keep a reconciliation path outside the indexer. Periodically compare indexed positions, reserves, or oracle observations with direct on-chain reads at a fixed block. The index is optimized for product queries. The chain remains the settlement source.
Security-first infrastructure is not “audit completed”
A contract audit is a point-in-time review. Infrastructure security is a continuous execution property.
The baseline contract pattern remains Checks-Effects-Interactions: validate conditions, update internal state, then interact with external contracts. Solidity documentation emphasizes that any external contract call—not only Ether transfers—can hand control to another contract. This matters for token hooks, callback-heavy integrations, routers, oracle consumers, and upgrade modules.
But the largest operational failures often appear around permissions rather than reentrancy.
DEFAULT_ADMIN_ROLE is highly privileged. OpenZeppelin explicitly warns about it and provides AccessControlDefaultAdminRules to enforce a single-admin restriction with safer transfer behavior. The right question for a web3 development company is not whether it imports OpenZeppelin. Everyone can import a package. The question is how authority changes are constrained in the deployed system.
Look for a real permission model:
1. Separate operational roles from upgrade roles. A keeper should not be able to upgrade an implementation. A pauser should not automatically control treasury movement.
2. Put high-impact authority behind delay and review. Timelocks do not make bad changes good, but they create a measurable response window.
3. Test role revocation and transfer. Teams test granting far more often than losing access. Production incidents enjoy the second path.
4. Monitor privileged events. Admin-role changes, proxy upgrades, feed changes, parameter changes, and pause events belong in alerting pipelines.
5. Model oracle failure explicitly. Check timestamps, decimals, answer validity, bounds, and protocol-specific deviation rules. “The oracle returned a number” is not validation.
For data-feed consumers, the contract needs a defined response to stale or abnormal data. Revert? Pause borrowing? Freeze new positions while allowing repayments? Use a bounded fallback? There is no generic answer. There must be an answer before volatility exposes the missing branch.
What a capable partner optimizes first
The stack should not be evaluated by how many tools appear in a pitch deck. Evaluate the handoffs.
Can the team reproduce a mainnet failure on a pinned fork? Can it identify whether a slow user action came from wallet confirmation, RPC latency, gas estimation, simulation, mempool delay, or contract execution? Can it query product history without loading raw logs in the browser? Can it rotate an RPC credential without redeploying the frontend? Can it explain exactly what happens when an oracle update is stale?
That is the working definition of infrastructure maturity.
For node operators and platform teams, the immediate optimization sequence is blunt:
- Split read, write, indexer, and administrative traffic before one quota or outage couples them.
- Set latency SLOs per RPC method, not per provider marketing tier.
- Benchmark fallback providers with the same block-range, trace, and WebSocket workload you will run in production.
- Pin fork tests to incident blocks and make stale-data scenarios mandatory in CI.
- Keep GraphQL for indexed reads and direct RPC for settlement-critical verification.
- Alert on head lag, feed age, role changes, and abnormal gas overhead before users report them.
A web3 development company earns trust when its stack makes failure observable, reproducible, and bounded. Everything else is just a deployment that has not met its first bad block yet.




