Market cap $2.28 T −6.41 % over 24 hours24h volume $91.15 BBTC dominance 58.7 %ETH 10.1 %Fear & Greed 13 Extreme fearCoins 22,238

How to build and deploy your first dApp on the Sepolia testnet

How to deploy a smart contract on Sepolia testnet: a Foundry contract, a viem page, wallet connection and verification on Etherscan and Sourcify.

Advanced 27 min readUpdated Read first: Best resources to learn blockchain: books, certifications, podcasts
A laptop, chain links, a gear, a key and connected network nodes
Table of contents
  1. Build your first dApp in three pieces: a contract, a page and a wallet
  2. Why Sepolia is the testnet for dApps in 2026
  3. Set up the Foundry project and the Guestbook contract
    1. Install Foundry and create the project
    2. The contract: a guestbook with no money in it
    3. Compile and test before anything leaves your machine
  4. A test wallet, an encrypted key and an RPC endpoint
    1. Create a wallet that exists only for testnets
    2. An RPC endpoint and a clean .env
    3. Get Sepolia ETH from a faucet
  5. Deploy a smart contract on Sepolia with forge create or forge script
    1. One command: forge create with –broadcast
    2. A repeatable deployment: forge script
    3. Check the contract from the terminal before you build a page
  6. Verify the contract on Etherscan and Sourcify
    1. Etherscan with an API V2 key
    2. Sourcify: open verification without a key
  7. Build the front end with Vite and viem
    1. Read the contract without a wallet
    2. Find the wallets with EIP-6963
    3. Connect a wallet through EIP-1193
    4. Write to the contract and show the result
    5. List the entries safely
  8. Run the dApp and check each link of the chain
  9. When the deployment or the page fails
  10. What changes on mainnet, in principle
  11. Put the verified deployment in your portfolio

You have a Solidity contract that compiles, passes its tests and lives on a local anvil chain that disappears when you close the terminal. Nobody else can call it. To turn it into a dApp you need three more things: an address on a public network, a source code that anyone can check, and a web page with a button that a stranger can press. This lesson builds all three and shows how to deploy a smart contract on the Sepolia testnet, where a mistake costs nothing but a few minutes.

The project is a guestbook. Anyone with a wallet signs a short message, the contract stores it with the author and the time, and the page lists the last ten entries. The whole thing is about 60 lines of Solidity and JavaScript. The hard part is not the code. It is the chain of tools around it: compiling, keeping a key safe, choosing an RPC endpoint, getting test ether, deploying, verifying, then wiring a page to the contract. Each of those steps fails in a typical way, and we name the failure every time.

A few rules hold for the whole lesson. Test ether has no value: you never buy it, and anyone who sells it or offers a “testnet airdrop” is running the kind of scheme described in our guide to avoiding crypto scams. You use a fresh wallet created only for testing. And that wallet’s key never comes near the seed phrase of a wallet holding real funds, for the reasons given in our lesson on seed phrase security.

Build your first dApp in three pieces: a contract, a page and a wallet

A decentralised application is a program whose logic runs on a blockchain and whose interface runs in a browser. If the idea itself is new, our explainer on what a dApp is covers it; here we build one. The contract holds the state and enforces the rules. The page reads that state through a node and shows it. The wallet signs the transactions that change the state, so the page never sees a private key.

This is lesson 4 of the developer path. It assumes you have done the hands-on Solidity lesson (events, custom errors, msg.sender, tests with Foundry) and know where to look things up after our tour of blockchain learning resources and certifications. We do not re-teach the language. You also need a terminal, Git, a current long-term-support release of Node.js with npm, and a browser with a wallet extension such as MetaMask. Count an afternoon the first time, most of it spent on accounts, keys and faucets rather than code.

We picked the tools below because each one is the smallest that does the job. Foundry compiles, tests, deploys and verifies from one command line. viem is a TypeScript library that talks to any Ethereum node and any wallet, without a framework. Vite serves and bundles the page. The versions are the ones current on 27 October 2026.

ToolVersion on 27 October 2026Role in this lessonWhere checked
Foundry (forge, cast, anvil)v1.8.6, released 21 October 2026Compile, test, deploy, verifyfoundry-rs GitHub releases
Solidity compiler0.8.38 (latest release)Compiles the contractbinaries.soliditylang.org
viem2.57.12 (published 26 October 2026)Reads and writes from the pagenpm registry, GitHub
Vite8.3.5Dev server and buildnpm registry
ethers6.17.0Alternative to viemnpm registry
wagmi3.7.9React layer on top of viem, for laternpm registry

Why viem rather than ethers v6 or wagmi? ethers v6 can do everything in this lesson and many tutorials use it: you would wrap the wallet in a BrowserProvider, get a signer from it and build a Contract object. wagmi adds React hooks, caching and wallet connectors on top of viem, which pays off in a larger app but hides the steps we want you to see. viem with plain JavaScript keeps every request visible, and what you learn carries over to wagmi later.

Why Sepolia is the testnet for dApps in 2026

Ethereum runs several public test networks, and picking the wrong one is the first classic mistake. The ethereum.org networks page is plain about it: “Sepolia is the recommended default testnet for application development”. The other names you will meet in older tutorials are gone or meant for someone else. Goerli is listed as deprecated. Holesky, a large network built for staking tests, is deprecated: the Ethereum Foundation announced its shutdown on 1 September 2025 and pointed app developers to Sepolia. Hoodi, its replacement, is meant for validators and staking providers, not for contracts and front ends.

Sepolia has chain ID 11155111, written 0xaa36a7 in hexadecimal, the form wallets use. Remember both numbers: the decimal one appears in Etherscan and Foundry, the hexadecimal one in wallet requests. Block explorers, faucets, RPC providers and both verification services we use support it. As of 27 October 2026, Etherscan’s list of chains marks Sepolia as available on its free API plan, while some layer 2 test networks such as Base Sepolia require a paid plan for API access. That is one reason to start on Sepolia itself and move to a layer 2 later, once the concepts in our guide to layer 1 and layer 2 networks matter for your project.

Why not stay on anvil? A local chain is perfect for tests, and you should keep using it. But nobody else can reach it, your wallet extension has to be pointed at it by hand, and it teaches you nothing about the real-world problems of latency, gas prices, nonces and explorers. Why not mainnet? Every transaction there costs real ether and cannot be undone. A testnet behaves like mainnet in all the ways that matter for learning, except the price.

Set up the Foundry project and the Guestbook contract

Install Foundry and create the project

Foundry installs with its own updater, foundryup. The two commands below come from the Foundry installation page; open a new terminal between them, or reload your shell profile, so that the updater is on your path. On Windows the docs ask for Git Bash or WSL rather than PowerShell.

curl -L https://foundry.paradigm.xyz | bash
foundryup

forge init guestbook
cd guestbook
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol

forge init creates a foundry.toml file, the folders src, test and script, each with an example Counter file, and installs the forge-std library in lib. We delete the three Counter files and replace them with our own. The final layout of the whole project looks like this:

guestbook/
  foundry.toml
  .env            (never committed)
  .env.example    (committed, values empty)
  src/Guestbook.sol
  script/Deploy.s.sol
  web/            (the Vite page)

The contract: a guestbook with no money in it

Save this file as src/Guestbook.sol. We compiled it with the Solidity compiler 0.8.38 (solc-js, optimizer on, 200 runs): no error, no warning, and 1,549 bytes of creation code. The pragma ^0.8.28 accepts any 0.8 compiler from 0.8.28 upwards.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Guestbook {
    struct Entry {
        address author;
        uint64 timestamp;
        string message;
    }

    Entry[] private entries;

    event MessageSigned(address indexed author, uint256 indexed id, string message);

    error EmptyMessage();
    error MessageTooLong(uint256 length);

    function sign(string calldata message) external {
        uint256 length = bytes(message).length;
        if (length == 0) revert EmptyMessage();
        if (length > 140) revert MessageTooLong(length);

        entries.push(Entry(msg.sender, uint64(block.timestamp), message));
        emit MessageSigned(msg.sender, entries.length - 1, message);
    }

    function count() external view returns (uint256) {
        return entries.length;
    }

    function getEntry(uint256 id) external view returns (Entry memory) {
        return entries[id];
    }
}

Every line uses something from the Solidity lesson, so we only explain the design choices. The contract holds no ether and has no payable function, so a bug can corrupt a list of messages but cannot lose anyone’s money: the right risk level for a first public contract. It has no owner and no constructor, which keeps verification simple because there are no constructor arguments to encode. The limit of 140 counts bytes, not characters: an accented letter or an emoji takes several bytes, so a French or Italian message hits the limit sooner than its visible length suggests.

The two custom errors are cheaper than revert strings and let the page tell the user exactly why a transaction failed. The event MessageSigned indexes the author and the entry number, so an explorer or an indexer can filter by either. Storage is the expensive part of the design: each message is written to the chain’s state, which is why the length is capped and why a real product would keep long texts off-chain.

Compile and test before anything leaves your machine

Run forge build to compile, then write at least one test in test/ that signs a message and checks count(), and one that expects the EmptyMessage error. The Solidity lesson showed how; the point here is the habit. Each step from now on touches a public network and takes longer to redo, so a bug caught by forge test on your laptop costs seconds, and the same bug found after deployment costs a new deployment, a new verification and a new address in your page.

A document, a software bug, a magnifying glass and a gear together
A bug caught by forge test on your laptop costs seconds; after deployment it costs a new address.

A test wallet, an encrypted key and an RPC endpoint

Create a wallet that exists only for testnets

In your wallet extension, create a new account or, better, a separate wallet with its own seed phrase, and name it so you cannot confuse it with anything else (“Sepolia deployer” works). This key will sit on a laptop and be typed into developer tools, so treat it as already half-exposed and never send real assets to its address. The key, not the wallet app, is what controls an address.

Then give that key to Foundry in an encrypted keystore rather than in a file in clear. The command below asks for the private key without echoing it and for a password to encrypt it; the account is then known to every Foundry command as deployer. The Foundry deployment docs call the keystore the recommended option and list a raw --private-key flag as not recommended. Two other routes avoid the keystore altogether: --ledger signs with a hardware wallet and --browser sends the transaction to your browser wallet for approval.

cast wallet import deployer --interactive
cast wallet list

Never use the default anvil accounts on a public network. Their keys are published in every copy of Foundry, and bots watch for funds sent to them.

An RPC endpoint and a clean .env

Your tools reach Sepolia through an RPC endpoint, a URL of a node that accepts JSON-RPC requests. Alchemy, Infura, QuickNode, Chainstack and others give a free Sepolia endpoint after you open an account, and the URL usually contains your API key. Public endpoints without an account exist too, but they are rate-limited and change often. viem’s built-in Sepolia definition does ship a default public RPC; do not rely on it for anything more than a first try.

Put the endpoint and, later, your Etherscan key in a .env file at the project root, and keep that file out of Git. Foundry reads a .env file in the project folder, and its own best-practice page asks you to keep .env files out of version control and to commit an .env.example with empty values instead, so others know which variables to set. Your .env holds two lines:

SEPOLIA_RPC_URL=https://your-provider.example/v2/your-key
ETHERSCAN_API_KEY=your-etherscan-key

Add a line reading .env to .gitignore before your first commit, and run git status to check that the file does not appear. Notice what is not in it: no private key, since the keystore holds it. A key pushed to a public repository by mistake must be treated as stolen, even on a testnet, because the same habit later costs real money.

Then give the endpoint a short name in foundry.toml, so that every command can say --rpc-url sepolia:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"

A detail that trips many people: in Foundry, --chain does not choose a network. The RPC URL decides where transactions go. Always pass --rpc-url.

Get Sepolia ETH from a faucet

Deployment needs gas, and gas on Sepolia is paid in test ether you get for free from a faucet. Ethereum.org lists ten Sepolia faucets, among them those of Alchemy, Google Cloud Web3, Infura, QuickNode, Chainstack and the PoW Faucet. Their rules differ, and they change. On 27 October 2026, Alchemy’s faucet page said no account was required, while Google Cloud’s asks you to sign in with a Google account. QuickNode’s base drip needs no account, but the claiming wallet must hold a small mainnet balance, which the faucet says is there to prevent abuse and spam. The PoW Faucet asks only for your address and a stretch of mining work done in the browser.

Amounts and conditions change from month to month, and the faucets’ own pages are the only reliable source, so we print none here. Paste the address of your test wallet, never its key, and wait for the transaction. A first deployment of a contract this size needs only a small fraction of what a typical faucet hands out. If a site asks for your seed phrase or a payment to “unlock” test ether, close it.

Deploy a smart contract on Sepolia with forge create or forge script

Foundry gives you two ways to put the Guestbook on Sepolia. forge create deploys one contract with one command. forge script runs a Solidity script that can deploy several contracts and call them, records everything it did, and can be replayed. Start with the first, then switch to the second for anything you will repeat.

One command: forge create with –broadcast

source .env
forge create src/Guestbook.sol:Guestbook \
  --rpc-url $SEPOLIA_RPC_URL \
  --account deployer \
  --broadcast

Foundry asks for the keystore password, signs, sends the transaction and prints three lines that matter: the deployer address, the address of the new contract and the transaction hash. Copy the contract address; the page needs it.

Look at --broadcast. In current Foundry, without it, forge create performs a dry run: it shows what would happen and sends nothing. Many tutorials written before this change omit the flag, and their readers wonder why no contract appears on the explorer. Older guides also pass --private-key on the command line, which leaves the key in your shell history.

A repeatable deployment: forge script

Save this script as script/Deploy.s.sol. vm.startBroadcast() without an argument uses the signer given on the command line, here the keystore account.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Script} from "forge-std/Script.sol";
import {Guestbook} from "../src/Guestbook.sol";

contract Deploy is Script {
    function run() external returns (Guestbook gb) {
        vm.startBroadcast();   // signer comes from --account on the command line
        gb = new Guestbook();
        vm.stopBroadcast();
    }
}

Run it three ways. The first command only simulates; the second sends for real; the third, used only if the network dropped halfway, loads the saved run and sends whatever is still missing.

forge script script/Deploy.s.sol --rpc-url sepolia --account deployer
forge script script/Deploy.s.sol --rpc-url sepolia --account deployer --broadcast
forge script script/Deploy.s.sol --rpc-url sepolia --account deployer --broadcast --resume

The simulation runs your script against the live state of Sepolia and estimates the gas without sending anything. The real run writes a record in broadcast/Deploy.s.sol/11155111/run-latest.json with every transaction, its hash, the contract address and the receipt. Many teams commit those records for public networks, because they are the history of what was deployed where. The cache/ folder is another matter: the Foundry docs say its copies store the RPC URL of each transaction, which can contain your API key, so add cache/ to .gitignore.

Check the contract from the terminal before you build a page

Open the transaction hash on sepolia.etherscan.io: you should see “Contract Creation”, your deployer as sender and a status of success. Then talk to the contract with cast, Foundry’s command-line client. The first call is free and needs no key, the second sends a real transaction from your test wallet.

export GUESTBOOK=0xYourContractAddress
cast call $GUESTBOOK "count()(uint256)" --rpc-url sepolia
cast send $GUESTBOOK "sign(string)" "hello from the terminal" --rpc-url sepolia --account deployer
cast call $GUESTBOOK "count()(uint256)" --rpc-url sepolia

If the second count returns 1, you have managed to deploy a smart contract on Sepolia that works, and every later problem is in the page, not in Solidity.

Verify the contract on Etherscan and Sourcify

Right after deployment, Etherscan shows only bytecode. Verification means you hand the explorer your source code and compiler settings, it recompiles them, and if the result matches what is on the chain it publishes the source next to the address. Readers can then check what the contract does, the explorer shows “Read” and “Write” tabs built from the ABI, and wallets and other tools can decode your function calls. For a portfolio project it is the difference between “trust me” and “read it”.

Etherscan with an API V2 key

Etherscan verification needs an API key, free with an Etherscan account. Since its API V2, one key works across every EVM chain Etherscan supports: requests go to a single endpoint and a chainid parameter picks the network, as the Etherscan API documentation explains. The documentation speaks of more than 60 chains; its chain-list API returned 57 on 27 October 2026. The same documentation gives the free plan 3 calls per second and up to 100,000 calls per day on selected chains, Sepolia among them, far more than verification needs.

forge verify-contract --chain sepolia $GUESTBOOK src/Guestbook.sol:Guestbook --watch

The key comes from ETHERSCAN_API_KEY in your environment, or from --etherscan-api-key. --watch waits and reports the result instead of leaving you to refresh the page. You can also verify during deployment: add --verify to forge create or to the broadcast run of forge script. A contract with constructor arguments needs them passed with --constructor-args $(cast abi-encode "constructor(...)" ...); ours has none, which is one reason we designed it that way.

Sourcify: open verification without a key

Sourcify is an open-source verification service and database. It stores the source and the compiler metadata of verified contracts in a public repository, so verification does not depend on one company. Its documentation advises Foundry and Hardhat users to verify through the framework’s own command line, and the Foundry example asks for no key:

forge verify-contract $GUESTBOOK src/Guestbook.sol:Guestbook --chain sepolia --verifier sourcify

Sourcify distinguishes two levels, renamed in its current documentation. An exact match (formerly “perfect match”) means the deployed bytecode is identical byte for byte, metadata hash included, so the source files are the very ones used to compile; adding a comment or renaming a variable breaks it. A plain match (formerly “partial match”) means the code compiles to the same contract but the metadata differs. Flattening the sources also rules out an exact match, so let Foundry submit the files as they are. A third verifier, Blockscout, used by many smaller chains, is available with --verifier blockscout.

When verification fails, the cause is almost always a difference between the settings used to deploy and those used to verify: another compiler version, the optimizer switched on in one case and off in the other, a different number of runs, or missing constructor arguments. Keep those settings in foundry.toml rather than on the command line, and both steps read the same values. A fresh contract can also be “not found” for a minute because the explorer has not indexed it yet; --watch waits it out.

Build the front end with Vite and viem

Create the page next to the contract, then install viem. The page needs two values at build time, which Vite reads from web/.env because their names start with VITE_.

npm create vite@latest web -- --template vanilla
cd web
npm install viem

Then create web/.env with the address printed at deployment and a read-only RPC URL:

VITE_CONTRACT_ADDRESS=0xYourContractAddress
VITE_RPC_URL=https://your-provider.example/v2/your-public-key

Everything in a front-end bundle is public, including that RPC URL. Use a separate key that your provider restricts to your domain, or a public endpoint, and never put the Etherscan key or anything secret in web/.env. Replace the body of web/index.html with this skeleton: a box for wallet buttons, a status line, a form and a list.

<h1>Guestbook (Sepolia)</h1>
<div id="wallets"></div>
<p id="status">No wallet connected.</p>
<form id="form"><input id="msg" maxlength="140" required><button>Sign</button></form>
<ul id="entries"></ul>
<script type="module" src="/main.js"></script>

The JavaScript below goes into web/main.js in five parts, in this order. Together they form one file of about 80 lines. We assembled the five parts and checked that the file parses as a JavaScript module; an earlier version of the same code built without error with Vite 8.3.5 and viem 2.57.12.

Read the contract without a wallet

import { createPublicClient, createWalletClient, custom, http, parseAbi } from 'viem'
import { sepolia } from 'viem/chains'

const CONTRACT = import.meta.env.VITE_CONTRACT_ADDRESS // set in .env: 0x... from the deployment
const abi = parseAbi([
  'function sign(string message)',
  'function count() view returns (uint256)',
  'function getEntry(uint256 id) view returns ((address author, uint64 timestamp, string message))',
  'event MessageSigned(address indexed author, uint256 indexed id, string message)',
  'error EmptyMessage()',
  'error MessageTooLong(uint256 length)',
])

// Reads need no wallet: a public RPC is enough.
const publicClient = createPublicClient({ chain: sepolia, transport: http(import.meta.env.VITE_RPC_URL) })

parseAbi turns human-readable signatures into the ABI viem needs and gives you type checking in an editor. You could import the full ABI from Foundry’s out/Guestbook.sol/Guestbook.json instead; for six entries, writing them is clearer. Do not skip the two error lines: without them viem cannot name the custom error when a transaction reverts. The public client talks to your RPC endpoint and needs no wallet, so a visitor without any extension can still read the guestbook. That matters: most visitors only read, and a page that demands a wallet before showing anything loses them.

Find the wallets with EIP-6963

For years, wallet extensions exposed themselves to pages through a single global object, window.ethereum. With two extensions installed, they fought over it and the page got whichever loaded last. EIP-6963, a Final standard called Multi Injected Provider Discovery, fixes that with two browser events. The page dispatches eip6963:requestProvider; each wallet answers with eip6963:announceProvider, carrying an info object (a unique id, a name, an icon and a reverse-domain identifier) and its provider. The page lists them and the user chooses.

// EIP-6963: wallets announce themselves; the page listens, then asks.
const providers = new Map()
window.addEventListener('eip6963:announceProvider', (event) => {
  providers.set(event.detail.info.uuid, event.detail)
  render()
})
window.dispatchEvent(new Event('eip6963:requestProvider'))

let walletClient, account

function render() {
  const box = document.getElementById('wallets')
  box.replaceChildren(...[...providers.values()].map(({ info, provider }) => {
    const b = document.createElement('button')
    b.textContent = 'Connect ' + info.name
    b.onclick = () => connect(provider)
    return b
  }))
}

Order matters: register the listener first, then dispatch the request, or the wallets answer before anyone is listening and the page shows no button. Wallets also announce themselves on their own when they load, so a listener registered early catches late arrivals too.

Connect a wallet through EIP-1193

The provider each wallet hands over follows EIP-1193, the standard JavaScript interface between a page and a wallet: one request({ method, params }) function plus events. eth_requestAccounts opens the wallet’s connection prompt and returns the addresses the user allows. wallet_switchEthereumChain asks the wallet to move to Sepolia, using the hexadecimal chain ID. If the user refuses, the wallet returns error 4001, which EIP-1193 defines as “User Rejected Request”.

async function connect(provider) {
  // EIP-1193: provider.request({ method, params })
  const [address] = await provider.request({ method: 'eth_requestAccounts' })
  try {
    await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0xaa36a7' }] })
  } catch (e) {
    if (e.code === 4001) return setStatus('Network switch refused.')
    throw e // unknown chain: offer wallet_addEthereumChain instead
  }
  account = address
  walletClient = createWalletClient({ account, chain: sepolia, transport: custom(provider) })
  provider.on('accountsChanged', () => location.reload())
  provider.on('chainChanged', () => location.reload())
  setStatus('Connected: ' + address)
}

function setStatus(t) { document.getElementById('status').textContent = t }

A wallet that does not know the requested chain returns an error instead of switching; the usual answer is to call wallet_addEthereumChain with the chain’s name, RPC URL and currency, then try again. In MetaMask, Sepolia is already in the network list: its support page tells users to turn on the “Show test networks” toggle to see it, with no need to add it by hand. Other wallets may not know it. The two event handlers reload the page when the user changes account or network in the wallet: crude, but it guarantees the page never signs from an address or on a chain it did not expect. A real app would update its state instead.

Write to the contract and show the result

document.getElementById('form').addEventListener('submit', async (e) => {
  e.preventDefault()
  if (walletClient === undefined) return setStatus('Connect a wallet first.')
  try {
    const hash = await walletClient.writeContract({
      address: CONTRACT, abi, functionName: 'sign', args: [document.getElementById('msg').value],
    })
    setStatus('Sent: ' + hash)
    await publicClient.waitForTransactionReceipt({ hash })
    setStatus('Confirmed.')
    load()
  } catch (err) {
    setStatus(err.shortMessage ?? err.message)
  }
})

writeContract builds the transaction, sends it to the wallet for signature and returns the hash once the wallet broadcasts it. That hash proves only that the transaction was sent. waitForTransactionReceipt waits until it is included in a block, usually within a slot or two (ethereum.org describes proof-of-stake Ethereum as divided into 12-second slots, and Sepolia runs the same consensus), and only then does the page reload the list. If the contract reverts, viem decodes the custom error with the two error entries of the ABI and puts a readable summary, naming EmptyMessage or MessageTooLong, in shortMessage.

A wallet, an electric plug, stacked blocks and a shield side by side
The wallet signs, the page waits for the receipt, and the message joins the chain.

List the entries safely

async function load() {
  const n = await publicClient.readContract({ address: CONTRACT, abi, functionName: 'count' })
  const list = document.getElementById('entries')
  list.replaceChildren()
  for (let i = Number(n) - 1; i >= Math.max(0, Number(n) - 10); i--) {
    const x = await publicClient.readContract({ address: CONTRACT, abi, functionName: 'getEntry', args: [BigInt(i)] })
    const li = document.createElement('li')
    li.textContent = x.author.slice(0, 8) + '…: ' + x.message   // textContent, never innerHTML
    list.append(li)
  }
}
if (CONTRACT) load()

The loop reads up to ten entries, newest first, one call each. That is fine for a demo and slow for a real product, which would read the MessageSigned events or use an indexer. Look at textContent. The messages are written by strangers and stored forever on a public chain. Inserting them with innerHTML would let anyone sign a message containing a script that runs in every visitor’s browser, a cross-site scripting hole you could never delete from the chain. Treat every on-chain string as untrusted input.

Start the development server with npm run dev from web/ and open the local address it prints. Then go through this list in order; each step tests one link.

  1. The page loads and, after a second, lists the entry you signed with cast. That proves the RPC URL and the contract address are right, with no wallet involved.
  2. A “Connect” button appears for each installed wallet. If none appears, the wallet is missing, disabled for this site, or does not support EIP-6963.
  3. Clicking it opens the wallet, and after you accept, the status line shows your test address and the wallet sits on Sepolia.
  4. Typing a message and pressing Sign opens a confirmation in the wallet that names the contract and an estimated fee in Sepolia ETH.
  5. The status changes to “Sent”, then “Confirmed”, and your message appears at the top of the list.
  6. On sepolia.etherscan.io, the contract page shows your transaction and, in the Logs of that transaction, a MessageSigned event with your address.

npm run build produces a static dist/ folder you can host anywhere. The contract address and the RPC URL are baked in at build time, so rebuild after any redeployment.

When the deployment or the page fails

Most failures in a first dApp come from a handful of causes. This table maps the message you see to its usual cause.

What you seeUsual causeFix
“insufficient funds” when deployingThe test wallet holds no Sepolia ETH, or you are deploying from another accountCheck the balance with cast balance, use a faucet, check –account
Command finishes but nothing on the explorer–broadcast missing: it was a dry runRun again with –broadcast
Transaction sent to the wrong networkRPC URL of another chain; –chain does not pick the networkPass –rpc-url sepolia and check the chain ID
“execution reverted” with EmptyMessage or MessageTooLongEmpty message, or more than 140 bytesShorten the message; accented letters and emoji count as several bytes
Verification “bytecode does not match”Compiler version, optimizer or runs differ from the deploymentKeep settings in foundry.toml; pass the same values
Page shows an empty listWrong contract address in web/.env, or the build predates itFix the address, restart npm run dev or rebuild
No wallet buttonListener added after the request, or the wallet does not support EIP-6963Register the listener first; try another wallet
A transaction stays pendingA previous transaction from the same account is stuck with a low feeWait, or replace it from the wallet with the same nonce and a higher fee

What changes on mainnet, in principle

On Ethereum mainnet the same commands work with another RPC URL, and every transaction is paid in real ether. Deploying costs the gas used by the creation transaction multiplied by the gas price at that moment; the gas used depends mostly on the size of the code you store, and the price moves from minute to minute, so we give no figure. The simulation of forge script or cast estimate gives the gas units; multiply by the price your wallet shows when you deploy. Our glossary entry on gas fees explains how the base fee and the tip combine. Layer 2 networks run the same contracts for a fraction of the fee, which is why most new apps launch there.

Money changes the security model as well. A contract that holds or moves funds needs an external audit before launch, the deployer key belongs on a hardware wallet (--ledger) and admin rights on a multisig rather than one address. Our guestbook avoided all of that by holding nothing, which is exactly why it suits a first deployment.

Then the rules. A testnet guestbook raises no legal question. A dApp that takes deposits, issues a token sold to the public or offers leveraged trading can bring in US regulators such as the SEC, for securities, or the CFTC, for derivatives, and anti-money-laundering rules, whatever the code says about decentralisation. On the tax side, the IRS states that digital assets are treated as property for US tax purposes, and tokens received as payment for work count as income. This is a description of the frame, not legal advice: a project that touches other people’s money needs a lawyer before mainnet.

Put the verified deployment in your portfolio

A verified contract on Sepolia, a public repository with tests and a live page is a better entry in a junior developer’s portfolio than a certificate. A recruiter can open the explorer, read the source, see the tests and sign the guestbook in under five minutes. Add a short README that lists the commands, the address and what you would change for mainnet.

From here, extend the project step by step: read events instead of looping, add an owner who can pause the contract and test that only the owner can, move the page to wagmi and React, or deploy the same contract to a layer 2 testnet. Our plan to become a blockchain developer places this project in the larger path, and our overview of blockchain jobs and salaries shows which roles ask for it. A later lesson of the careers path looks at jobs in DeFi.

Sources

  1. ethereum.org: Networks (testnets, Sepolia faucets) ethereum.org
  2. Ethereum Foundation blog: Holesky shutdown announcement, 1 September 2025 blog.ethereum.org
  3. Foundry documentation: Deploying (forge script, keystore, verification) getfoundry.sh
  4. Foundry documentation: Installation getfoundry.sh
  5. EIP-1193: Ethereum Provider JavaScript API eips.ethereum.org
  6. EIP-6963: Multi Injected Provider Discovery eips.ethereum.org
  7. Etherscan API V2 documentation docs.etherscan.io
  8. Sourcify documentation: How to verify docs.sourcify.dev
  9. viem documentation viem.sh
  10. IRS: Digital assets irs.gov
  11. ethereum.org: Proof-of-stake (12-second slots) ethereum.org
  12. Sourcify documentation: Exact match vs match docs.sourcify.dev
  13. Etherscan API documentation: rate limits docs.etherscan.io
  14. MetaMask support: How to view testnets in MetaMask support.metamask.io
  15. QuickNode Ethereum Sepolia faucet faucet.quicknode.com
Coindaro Editorial Team

Who wrote this article

Editorial team

Guides, reviews and glossary checked against primary sources

  • Crypto education
  • Exchange reviews
  • Wallets
  • Glossary

The Coindaro editorial team researches and writes every article, review and guide published on Coindaro. Each fact is checked against primary sources such as official documents, regulators and project documentation, and every page lists its sources. We cover crypto markets, technology and regulation for readers in the United States and Europe, independently and without paid placements.

Keep learning