How to write metadata manually
This guide shows you how to write an ERC-7730 descriptor by hand, for the contracts and EIP-712 messages the JSON Builder cannot cover.
Write the file manually when you need any of the following:
- One file that covers multiple deployments or contract variants
- EIP-712 or other structured messages
- Display logic that relies on nested paths, byte slices, or conditional rules
- Reusable enums, constants, or shared definitions across contracts
- The
addressMatcherorfactorycontext options for proxy patterns
If instead you target a single contract with clean function signatures and minimal formatting, use the JSON Builder — it is faster.
Prerequisites
- Contract ABI or EIP-712 schema for every interaction you plan to support
- Chain IDs and contract addresses
- A list of the user-facing fields each signer must display
Write the descriptor
The goal is a JSON file that turns raw calldata (for example 0x23b872dd…) into a readable display such as Send — Recipient: alice.eth, Amount: 500 USDT. This walkthrough uses Tether (USDT) deployed on three chains.
Define the context
The context section binds the metadata to one or more contract deployments.
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json",
"context": {
"$id": "Tether USD",
"contract": {
"abi": "https://api.etherscan.io/api?module=contract&action=getabi&address=0xdac17f958d2ee523a2206206994597c13d831ec7",
"deployments": [
{ "chainId": 1, "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" },
{ "chainId": 137, "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" },
{ "chainId": 42161, "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" }
]
}
}
}Working with EIP-712? Replace the contract block with an eip712 definition. See the context reference for the full schema, including the addressMatcher and factory options for proxy patterns.
Add protocol metadata
The metadata section introduces the protocol to users and stores reusable values such as token definitions and enums.
"metadata": {
"owner": "Tether",
"info": {
"legalName": "Tether Limited",
"url": "https://tether.to/",
"deploymentDate": "2017-11-28T12:41:21Z"
},
"token": {
"ticker": "USDT",
"name": "Tether USD",
"decimals": 6
}
}The metadata section also supports enums (value-to-label mappings) and constants (reusable values). See the metadata reference.
Define the display rules
The display.formats section maps contract function parameters to the labels and formats shown on the signer.
"display": {
"formats": {
"transfer(address,uint256)": {
"intent": "Send",
"fields": [
{ "path": "_to", "label": "Recipient", "format": "addressOrName" },
{
"path": "_value",
"label": "Amount",
"format": "tokenAmount",
"params": { "tokenPath": "@.to" }
}
],
"required": ["_to", "_value"],
"excluded": []
}
}
}You can key each entry in display.formats by Solidity declaration, canonical signature, or raw selector — use whichever form your tooling already produces:
"transfer(address _to,uint256 _value)": { }, // Solidity declaration
"transfer(address,uint256)": { }, // Canonical signature
"0xa9059cbb": { } // 4-byte selectorEvery function parameter must appear in either fields or excluded. Validation fails if any parameter is left unaccounted for.
For the full list of format types, path syntax, array helpers, and advanced patterns, see the format types reference and path system guide.
Reuse definitions with includes (optional)
To share display logic across multiple files, extract common snippets into a helper file and reference it with includes:
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json",
"includes": "common-swap.json",
"context": {
"contract": {
"deployments": [{ "chainId": 324, "address": "0x6fd4383cB451173D5f9304F041C7BCBf27d561fF" }]
}
}
}Shared definitions live under display.definitions in the included file:
{
"display": {
"definitions": {
"sendAmount": {
"label": "Amount to Send",
"format": "tokenAmount",
"params": { "tokenPath": "desc.srcToken" }
}
},
"formats": {
"swap(...)": {
"intent": "Swap",
"fields": [
{ "path": "desc.amount", "$ref": "$.display.definitions.sendAmount" }
]
}
}
}
}When the including file redeclares the same key, its version takes precedence. Use this to override only the fields that differ per chain while keeping shared logic in one place.
Validate and submit
Run the ERC-7730 validator on your completed file, then follow Validate and submit to open a pull request to the registry.
The complete file
Combining the sections above produces the full descriptor:
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json",
"context": {
"$id": "Tether USD",
"contract": {
"abi": "https://api.etherscan.io/api?module=contract&action=getabi&address=0xdac17f958d2ee523a2206206994597c13d831ec7",
"deployments": [
{ "chainId": 1, "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" },
{ "chainId": 137, "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" },
{ "chainId": 42161, "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" }
]
}
},
"metadata": {
"owner": "Tether",
"info": {
"legalName": "Tether Limited",
"url": "https://tether.to/",
"deploymentDate": "2017-11-28T12:41:21Z"
},
"token": {
"ticker": "USDT",
"name": "Tether USD",
"decimals": 6
}
},
"display": {
"formats": {
"transfer(address,uint256)": {
"intent": "Send",
"fields": [
{ "path": "_to", "label": "Recipient", "format": "addressOrName" },
{
"path": "_value",
"label": "Amount",
"format": "tokenAmount",
"params": { "tokenPath": "@.to" }
}
],
"required": ["_to", "_value"],
"excluded": []
}
}
}
}More examples
1inch Order (EIP-712)
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v1.schema.json",
"context": {
"eip712": {
"deployments": [
{ "chainId": 1, "address": "0x119c71d3bbac22029622cbaec24854d3d32d2828" },
{ "chainId": 10, "address": "0x11431a89893025d2a48dca4eddc396f8c8117187" },
{ "chainId": 56, "address": "0x1e38eff998df9d3669e32f4ff400031385bf6362" },
{ "chainId": 137, "address": "0x94bc2a1c732bcad7343b25af48385fe76e08734f" },
{ "chainId": 42161, "address": "0x7f069df72b7a39bce9806e3afaf579e54d8cf2b9" }
],
"domain": { "name": "Permit2" },
"schemas": [{
"primaryType": "OrderStructure",
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"OrderStructure": [
{ "name": "salt", "type": "uint256" },
{ "name": "maker", "type": "address" },
{ "name": "receiver", "type": "address" },
{ "name": "makerAsset", "type": "address" },
{ "name": "takerAsset", "type": "address" },
{ "name": "makingAmount", "type": "uint256" },
{ "name": "takingAmount", "type": "uint256" },
{ "name": "makerTraits", "type": "uint256" }
]
}
}]
}
},
"metadata": {
"owner": "1inch Limit Order Protocol"
},
"display": {
"formats": {
"OrderStructure": {
"intent": "1inch Order",
"fields": [
{ "path": "maker", "label": "From", "format": "raw" },
{ "path": "makingAmount", "label": "Send", "format": "tokenAmount", "params": { "tokenPath": "makerAsset" } },
{ "path": "takingAmount", "label": "Receive minimum", "format": "tokenAmount", "params": { "tokenPath": "takerAsset" } },
{ "path": "receiver", "label": "To", "format": "raw" }
],
"excluded": ["salt", "makerTraits"],
"required": ["maker", "makingAmount", "takingAmount", "receiver"]
}
}
}
}Troubleshooting
Validation fails with “unaccounted parameter”
Every parameter in the function signature must appear in either fields or excluded. Check the error message for the parameter name, then either add it to fields with an appropriate format or add it to the excluded array.
The function signature in my display key does not match
The key in display.formats must exactly match the function’s canonical signature, Solidity declaration, or 4-byte selector. Common mistakes:
- Parameter names included when the canonical form is expected: use
transfer(address,uint256), nottransfer(address to,uint256 value) - Tuple syntax differs from the ABI: use the full tuple expansion, for example
(address,uint256), notSwapParams - Extra whitespace around commas or parentheses
Path expression returns nothing or fails validation
Check the path root: # for the decoded data being signed (bare paths are relative to it), $ for values in the file, @ for the transaction envelope. See the path system reference for the full syntax.
ABI URL returns an error during validation
If the ABI URL is not reachable or returns an error during validation, embed the ABI directly in the file as a JSON array instead of a URL string. You can copy the ABI from Etherscan’s contract page under the Contract tab.
Next steps
Your descriptor is complete. Validate it and submit it to the registry:
For the full specification — factories, matchers, typed-data options, enums, constants, and includes — see the ERC-7730 reference and the ERC-7730 EIP .