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:
- What the signature does. "Proves you own this address", "Places a limit order", "Lets the app spend your USDC".
- What it costs. Most signatures are free and don't create a transaction. Say so, because people often assume otherwise.
- 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.
sellAmountandexpiresAtread better thanamt0anddeadline_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
chainIdandverifyingContract. 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,
1000000000may 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.