web3icons

Making signature requests readable

Why raw signature prompts confuse people and how EIP-712 typed data, Sign-In with Ethereum and clear explanations before the request make signing readable.

5 min read

A signature request asks someone to approve something with their private key, through a wallet popup that shows whatever your dApp sent. If that's a hex string, the person has to choose between blind trust and giving up. Neither is good for them, and both are bad for you: people who sign blindly are easy targets for phishing, and people who refuse don't use your product.

Readable signatures come from two places. The format of the request, which determines what the wallet can display, and your own interface, which can explain the request before the wallet opens. This guide covers both, plus the special care that approvals and permits need.

What the wallet can show

The signing method you choose decides how much the wallet can tell the person.

Method What the wallet can display Use it for
personal_sign with plain text The text itself Simple statements and confirmations
personal_sign with a hex hash An unreadable string Avoid for anything user-facing
eth_signTypedData_v4 (EIP-712) The app's domain and each named field Orders, permits, votes, any structured data
Sign-In with Ethereum (EIP-4361) A standard login message the wallet can recognize Sign-in
eth_sign An opaque 32-byte hash Nothing; many wallets block or strongly warn about it

The pattern is clear: the more structure you give the wallet, the more it can show. Hashing data before signing it throws that structure away.

Explain before you ask

You don't control the wallet popup, but you control the screen that opens it. Use it. Before every signature request, tell the person in your interface:

  1. What the signature does. "Proves you own this address", "Places a limit order", "Lets the app spend your USDC".
  2. What it costs. Most signatures are free and don't create a transaction. Say so, because people often assume otherwise.
  3. What happens next. "Your order will fill automatically if the price reaches 4,000 USDC. You can cancel it any time before then."

A short panel next to the button works well:

Sign to place your order

This signature authorizes a limit order to sell 1,000 USDC for at least 0.25 WETH. It's free and doesn't send a transaction. The order expires in 7 days.

Then the wallet popup confirms details the person has already read, rather than presenting them for the first time.

Typed data with EIP-712

EIP-712 defines a format for signing structured data. The wallet receives the data with its types, and can show each field by name along with a domain that identifies your app and contract.

const typedData = {
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Order: [
      { name: 'sellToken', type: 'address' },
      { name: 'buyToken', type: 'address' },
      { name: 'sellAmount', type: 'uint256' },
      { name: 'minBuyAmount', type: 'uint256' },
      { name: 'expiresAt', type: 'uint256' },
    ],
  },
  primaryType: 'Order',
  domain: {
    name: 'Example Exchange',
    version: '1',
    chainId: 8453,
    verifyingContract: ORDER_CONTRACT,
  },
  message: {
    sellToken: USDC_ADDRESS,
    buyToken: WETH_ADDRESS,
    sellAmount: '1000000000', // 1,000 USDC, 6 decimals
    minBuyAmount: '250000000000000000', // 0.25 WETH, 18 decimals
    expiresAt: String(Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60),
  },
}

const signature = await provider.request({
  method: 'eth_signTypedData_v4',
  params: [account, JSON.stringify(typedData)],
})

A few decisions here directly affect readability:

  • Field names are shown to the person. sellAmount and expiresAt read better than amt0 and deadline_ts. Name fields for humans, since you can't rename them later without changing the contract.
  • The domain name should match your product name, exactly as it appears in your interface. A mismatch is a warning sign, and a match is a small trust signal.
  • Include chainId and verifyingContract. They bind the signature to one contract on one chain, so it can't be replayed elsewhere.
  • Amounts may appear as raw integers. Depending on the wallet, 1000000000 may be shown as is, without decimals or a ticker. That's exactly why the explanation in your interface matters.

Client libraries such as viem and ethers have helpers that build the domain type for you; the raw provider call above shows what they send.

Sign-In with Ethereum

For login, use the EIP-4361 message format rather than an ad hoc string. It's plain text with a fixed structure that wallets can parse, so they can check the domain against the site making the request and warn about mismatches.

app.example.com wants you to sign in with your Ethereum account:
0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed

Sign in to Example. This request will not trigger a transaction or cost any fees.

URI: https://app.example.com
Version: 1
Chain ID: 1
Nonce: 8f3k2LmQ9x
Issued At: 2026-11-01T09:30:00Z
Expiration Time: 2026-11-01T09:40:00Z

The statement line is yours to write, and it's the only free-form text in the message. Use it to say what the signature does and doesn't do. Generate a fresh nonce on the server for every attempt, verify it once, and keep expiration short.

Ask for a sign-in signature once per session, not on every page load or every reconnect. Repeated signature prompts teach people to approve without reading, which is the habit phishing depends on.

Approvals and permits need warnings

Some requests are not harmless. A token approval lets a contract spend a person's tokens, and permit-style signatures grant the same permission through a signature instead of a transaction. Because they look like ordinary signature requests, permits are a common phishing tool: a fake site asks for a signature, and that signature authorizes draining a token balance.

When your app requests an approval or permit:

  • Say it in words, with numbers. "This lets Example Exchange spend up to 1,000 USDC from your wallet. The permission expires in 30 minutes."
  • Request exact amounts by default. Offer unlimited approval as an option the person chooses, with a sentence on the trade-off: fewer transactions later, more exposure if the contract is ever compromised.
  • Name the spender and show its address in a form that matches what the wallet shows; displaying wallet addresses covers truncation and checksum casing.
  • Use short expirations on permits wherever the protocol allows.

Review your own requests

List every signature your app requests. For each one, open the wallet popup yourself and read it as if you'd never seen your app. Ask whether someone could tell what they're agreeing to, and whether a phishing site could produce a prompt that looks the same. If the first answer is no, or the second is yes, change the format, the field names or the explanation around it. Signature clarity is one of the clearest trust signals in web3 interfaces, and one of the friction points described in Web3 UX: where most dApps lose people.

More in Web3 UX

All guides