Message signatures can be generated with any arbitrary message and an Ethereum wallet’s private key. Message signatures can be used to create a verification system for any application requiring a user to prove their identity. For example, you might consider using this tutorial to create an application allowing users to e-sign documents or pdfs. Creating and verifying signatures does not require a connection to the Ethereum network because it utilizes a message, wallet address, and private key to generate a signature hash. This means the entire process can occur off-chain and does not cost any gas to execute.
In part one of this tutorial, we will explore how a signature can be generated and verified using Viem, Ethers.js, or Web3.js libraries.
In part two, we will build upon what we learned in part one to build a full-stack signature generation DApp using ReactJS. With Ethers.js, we will use the provided starter files to create a frontend UI that lets you connect to a MetaMask wallet to sign/verify messages.
Part two of this tutorial will not cover ReactJS. We will only focus on the functionality necessary to connect the frontend UI to MetaMask. Therefore, you should have an understanding of React and React hooks such as useState and useEffect.
Before you continue in this tutorial, please ensure that you have accomplished the following:
- Install Node.js.
- Install a MetaMask browser wallet.
- Install an IDE (such as VS Code).
- Create an Alchemy account.
Head to Node.js and download the LTS version.
You can verify your installation was successful by running npm -version in your macOS terminal or Windows command prompt. A successful installation will display a version number, such as:
6.4.1Install MetaMask, a virtual wallet extension used to manage your Ethereum address and private key.
A development environment makes editing code in our project much easier to navigate. If you would like to follow along with exactly what I am using for this tutorial go ahead and install Visual Studio Code. However, feel free to use whatever development environment you prefer.
Although we are not sending any transactions on-chain, we will still use an Alchemy API key so we may monitor on-chain functionality if we so choose to add it in the future.
- Create a free Alchemy account.
- From the Alchemy Dashboard, hover over Apps then click +Create App.
- Name your app Signature-Generator.
- Select Ethereum as your chain and Sepolia as your network.
- Note: Because this tutorial does not perform any on-chain activity, you could use any testnet.
- Click Create app.

Your dashboard should look like this
Open VS Code (or your preferred IDE) and enter the following in the terminal:
mkdir my verify-msg-signature
cd verify-msg-signatureOnce inside our project directory, initialize npm (node package manager) with the following command:
npm initPress enter and answer the project prompt as follows:
package name: (signature-generator)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)Press enter again to complete the prompt. If successful, a package.json file will have been created in your directory.
The tools you will need to complete this tutorial are:
- Viem (recommended) or Ethers.js to utilize their cryptographic functions and create unique signatures.
- dotenv so that you can store your private key and API key safely.
To install the above tools, ensure you are still inside your root folder and type the following commands in your terminal:
Viem (Recommended):
npm install viemEthers.js:
npm install --save ethersDotenv:
npm install dotenv --saveCreate an .env file in your root folder. The file must be named .env or it will not be recognized.
In the .env file, we will store all of our sensitive information (i.e., our Alchemy API key and MetaMask private key).
Copy the following into your .env file:
API_URL = "https://eth-sepolia.g.alchemy.com/v2/{YOUR_ALCHEMY_API_KEY}"
PRIVATE_KEY = "{YOUR_PRIVATE_KEY}"- Replace
{YOUR_ALCHEMY_API_KEY}with your Alchemy API key found in your app’s dashboard, under VIEW KEY:

- Replace
{YOUR_PRIVATE_KEY}with your MetaMask private key.
To retrieve your MetaMask private key:
- Open the extension, click on the three dots menu, and choose Account Details.

2. Click Export Private Key and enter your MetaMask password.

3. Replace the Private Key in your .env file with your MetaMask Private Key.
The following section provides two options for verifying message signatures:
- Using Viem (recommended).
- Using Ethers.js v6.
Depending on your preferred library, feel free to use the appropriate tabs.
In your root folder create a file named VerifyMsg.js and add the following lines of code to it:
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
const main = async () => {
require("dotenv").config();
const { API_URL, PRIVATE_KEY } = process.env;
// Create account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Create wallet client
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(API_URL)
});
console.log('Wallet address:', account.address);
};
main();The code above creates an asynchronous function that contains the necessary variables to start using Alchemy's provider with Ethers. Below, you can see the same code with commented explanations at each step:
const main = async () => {
require("dotenv").config();
// Imports the secret .env file where our Private Key and API are stored
const { API_URL, PRIVATE_KEY } = process.env;
// We can now use these aliases instead of using our actual keys.
const { ethers } = require("ethers");
// Importing Ethers library
const { hashMessage } = require("@ethersproject/hash");
// Importing the hashMessage function which takes a string and converts it to a hash
// We need this because the Ethers sign function takes a message hash
// Note: We do not need this when using the Web3 library because the sign function automatically converts the message into a hash
// Creates a new provider instance with Alchemy using Ethers.js
const ethersAlchemyProvider = new ethers.JsonRpcProvider(API_URL);
};
main();In the same function, create a message to sign and a wallet instance, then use the wallet to both:
- Sign our message with the library's
signMessagefunction. - Verify it with the verification utilities.
The following code accomplishes the above and describes each action with commented notes:
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
import { verifyMessage } from 'viem'
const main = async () => {
require("dotenv").config();
const { API_URL, PRIVATE_KEY } = process.env;
// Create account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Create wallet client
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(API_URL)
});
const message = "Let's verify the signature of this message!";
console.log('Wallet address:', account.address);
// Sign the message
const signature = await walletClient.signMessage({
account,
message
});
// Verify the signature
const isValid = await verifyMessage({
address: account.address,
message,
signature
});
console.log('Signature:', signature);
console.log('Is valid signature:', isValid);
};
main();When using web3.js you can alternatively use the following to verify a message signature:
const messageSigner = web3.eth.accounts.recover(message, signMessage.v, signMessage.r, signMessage.s);Great! Now, we should add tests to check whether our message was signed and verified correctly.
The following code is the entire script with the checks:
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
import { verifyMessage } from 'viem'
const main = async () => {
require("dotenv").config();
const { API_URL, PRIVATE_KEY } = process.env;
try {
// Create account from private key
const account = privateKeyToAccount(PRIVATE_KEY);
// Create wallet client
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(API_URL)
});
const message = "Let's verify the signature of this message!";
// Sign the message
const signature = await walletClient.signMessage({
account,
message
});
// Verify the signature
const isValid = await verifyMessage({
address: account.address,
message,
signature
});
console.log("Success! The message: " + message + " was signed with the signature: " + signature);
console.log("The signer was: " + account.address);
console.log("Signature verification result: " + (isValid ? "Valid" : "Invalid"));
} catch (err) {
console.log("Something went wrong while verifying your message signature: " + err);
}
};
main();To use your script, type the following command in your terminal:
node VerifyMsg.jsIf successful, the message signature hash and signer address should return something like the following:
Success! The message: Let's verify the signature of this message! was signed with the signature: 0x16a08da8a50dc4ec2abf080528440821fc749323c69b6d38d88b8dedc03961772a7da6a2c74fcbde325085e552fcb197673e2a4741189bd6f9d9e1d07236c37c1b
The signer was: 0x5DAAC14781a5C4AF2B0673467364Cba46Da935dB
Signature verification result: ValidAwesome! You successfully signed a message and verified its signature!
You now know how to verify message signatures using Viem and Ethers.js. Check out part two to learn how to create a signature generator DApp and verify signatures using MetaMask!