Sign data
Use the following RPC methods to request cryptographic signatures from users:
eth_signTypedData_v4- Use this method to request the most human-readable signatures that are efficient to process onchain. We recommend this for most use cases.personal_sign- Use this method for the easiest way to request human-readable signatures that don't need to be efficiently processed onchain.
eth_sign is deprecated.
See MIP-3 for more information about this deprecation.
MetaMask supports signing transactions using Trezor and Ledger hardware wallets.
These wallets only support signing data using personal_sign.
If you can't log in to a dapp when using a Ledger or Trezor, the dapp might be requesting you to
sign data using an unsupported method, in which case we recommend using your standard MetaMask account.
Use eth_signTypedData_v4
eth_signTypedData_v4
provides the most human-readable signatures that are efficient to process onchain.
It follows the EIP-712 specification to allow users to sign
typed structured data that can be verified onchain.
It renders the structured data in a useful way (for example, displaying known
account names in place of addresses).

An eth_signTypedData_v4 payload uses a standard format of encoding structs, but has a different
format for the top-level struct that is signed, which includes some metadata about the verifying
contract to provide replay protection of these signatures between different contract instances.
We recommend using eth-sig-util to generate and
validate signatures.
Use eip712-codegen to generate most
of the Solidity required to verify these signatures onchain.
It currently doesn't generate the top-level struct verification code, so you must write that part manually.
See
this example implementation.
Since the top-level struct type's name and the domain.name are presented to the user prominently
in the confirmation, consider your contract name, the top-level struct name, and the struct keys to
be a user-facing security interface.
Ensure your contract is as readable as possible to the user.
Example
The following is an example of using eth_signTypedData_v4 with MetaMask:
- viem
- web3.js
- ethers.js
- Ethereum API
import { createEVMClient } from '@metamask/connect-evm'
const evmClient = createEVMClient({
dapp: {
name: 'Metamask Connect EVM Example',
url: window.location.href,
iconUrl: 'https://mydapp.com/icon.png', // Optional
},
api: {
supportedNetworks: {
'eip155:1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
'eip155:11155111': 'https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY',
},
},
})
const provider = evmClient.getProvider()
async function signTypedDataV4() {
// Get current account
const accounts = await provider.request({
method: 'eth_requestAccounts',
})
const message = {
domain: {
// This defines the network, in this case, Mainnet.
chainId: 1,
// Give a user-friendly name to the specific contract you're signing for.
name: 'Ether Mail',
// Add a verifying contract to make sure you're establishing contracts with the proper entity.
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
// This identifies the latest version.
version: '1',
},
// This defines the message you're proposing the user to sign, is dapp-specific, and contains
// anything you want. There are no required fields. Be as explicit as possible when building out
// the message schema.
message: {
contents: 'Hello, Bob!',
attachedMoneyInEth: 4.2,
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
},
// This refers to the keys of the following types object.
primaryType: 'Mail',
types: {
// This refers to the domain the contract is hosted on.
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
// Not an EIP712Domain definition.
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
// Refer to primaryType.
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
// Not an EIP712Domain definition.
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
}
const signature = await provider.request({
method: 'eth_signTypedData_v4',
params: [accounts[0], JSON.stringify(message)],
})
return signature
}
import { MetaMaskSDK } from '@metamask/sdk'
import { createPublicClient, createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
// Initialize SDK
const MMSDK = new MetaMaskSDK()
const provider = MMSDK.getProvider()
const walletClient = createWalletClient({ chain: mainnet, transport: custom(provider) })
const address = await walletClient.getAddresses()
const signature = await walletClient.signTypedData({
account: address[0],
domain: {
// This defines the network, in this case, Mainnet.
chainId: 1,
// Give a user-friendly name to the specific contract you're signing for.
name: 'Ether Mail',
// Add a verifying contract to make sure you're establishing contracts with the proper entity.
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
// This identifies the latest version.
version: '1',
},
// This defines the message you're proposing the user to sign, is dapp-specific, and contains
// anything you want. There are no required fields. Be as explicit as possible when building out
// the message schema.
message: {
contents: 'Hello, Bob!',
attachedMoneyInEth: 4.2,
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
},
// This refers to the keys of the following types object.
primaryType: 'Mail',
types: {
// This refers to the domain the contract is hosted on.
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
// Not an EIP712Domain definition.
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
// Refer to primaryType.
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
// Not an EIP712Domain definition.
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
})
import { MetaMaskSDK } from '@metamask/sdk'
import { ethers } from 'ethers'
import { BrowserProvider, parseUnits } from 'ethers'
// Initialize SDK
const MMSDK = new MetaMaskSDK()
const provider = MMSDK.getProvider()
const ethersProvider = new ethers.BrowserProvider(provider)
const signer = await ethersProvider.getSigner()
const fromAddress = await signer.getAddress()
const message = {
domain: {
// This defines the network, in this case, Mainnet.
chainId: 1,
// Give a user-friendly name to the specific contract you're signing for.
name: 'Ether Mail',
// Add a verifying contract to make sure you're establishing contracts with the proper entity.
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
// This identifies the latest version.
version: '1',
},
// This defines the message you're proposing the user to sign, is dapp-specific, and contains
// anything you want. There are no required fields. Be as explicit as possible when building out
// the message schema.
message: {
contents: 'Hello, Bob!',
attachedMoneyInEth: 4.2,
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
},
// This refers to the keys of the following types object.
primaryType: 'Mail',
types: {
// This refers to the domain the contract is hosted on.
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
// Not an EIP712Domain definition.
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
// Refer to primaryType.
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
// Not an EIP712Domain definition.
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
}
const params = [fromAddress, JSON.stringify(message)]
const method = 'eth_signTypedData_v4'
const signature = await signer.provider.send(method, params)
import { MetaMaskSDK } from '@metamask/sdk'
import { Web3 } from 'web3'
// Initialize SDK
const MMSDK = new MetaMaskSDK()
const provider = MMSDK.getProvider()
const web3 = new Web3(provider)
// Get user's Ethereum public address
const fromAddress = (await web3.eth.getAccounts())[0]
const message = {
domain: {
// This defines the network, in this case, Mainnet.
chainId: 1,
// Give a user-friendly name to the specific contract you're signing for.
name: 'Ether Mail',
// Add a verifying contract to make sure you're establishing contracts with the proper entity.
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
// This identifies the latest version.
version: '1',
},
// This defines the message you're proposing the user to sign, is dapp-specific, and contains
// anything you want. There are no required fields. Be as explicit as possible when building out
// the message schema.
message: {
contents: 'Hello, Bob!',
attachedMoneyInEth: 4.2,
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
},
// This refers to the keys of the following types object.
primaryType: 'Mail',
types: {
// This refers to the domain the contract is hosted on.
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
// Not an EIP712Domain definition.
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
// Refer to primaryType.
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
// Not an EIP712Domain definition.
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
}
const params = [fromAddress, JSON.stringify(message)]
const method = 'eth_signTypedData_v4'
const signature = await web3.eth.sendAsync(method, params)
Use personal_sign
personal_sign is the
easiest way to request human-readable signatures that don't need to be efficiently processed onchain.
It's often used for signature challenges that are authenticated on a web server, such as
Sign-In with Ethereum.

- Don't use this method to display binary data, because the user wouldn't be able to understand what they're agreeing to.
- If using this method for a signature challenge, think about what would prevent a phisher from reusing the same challenge and impersonating your site. Add text referring to your domain, or the current time, so the user can verify if this challenge is legitimate.