This ERC defines an onchain metadata standard for multi-token and NFT registries including ERC-721, ERC-1155, and ERC-6909. The standard provides a key-value store allowing for arbitrary bytes to be stored onchain.
Motivation
This ERC addresses the need for fully onchain metadata while maintaining compatibility with existing ERC-721, ERC-1155, and ERC-6909 standards. It has been a long-felt need for developers to store metadata onchain for NFTs and other multitoken contracts; however, there has been no uniform standard way to do this. Some projects have used the tokenURI field to store metadata onchain using Data URLs, which introduces gas inefficiencies and has other downstream effects (for example making storage proofs more complex). This standard provides a uniform way to store metadata onchain, and is backwards compatible with existing ERC-721, ERC-1155, and ERC-6909 standards.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Scope
This ERC is an optional extension that MAY be implemented by any ERC-721, ERC-1155, or ERC-6909 compliant registries.
Required Metadata Function and Event
Contracts implementing this ERC MUST implement the following interface:
interfaceIERC8048Metadata{/// @notice Get metadata value for a key.
functionmetadata(uint256tokenId,stringcalldatakey)externalviewreturns(bytesmemory);/// @notice Emitted when metadata is set for a token.
eventMetadataSet(uint256indexedtokenId,stringindexedindexedKey,stringkey,bytesvalue);}
metadata(tokenId, key): Returns the metadata value for the given token ID and key as bytes
Contracts implementing this ERC MAY also expose a setMetadata(uint256 tokenId, string calldata key, bytes calldata value) function to allow metadata updates, with write policy determined by the contract.
Contracts implementing this ERC MUST emit the following event when metadata is set:
This ERC specifies that the key is a string type and the value is bytes type. This provides flexibility for storing any type of data while maintaining an intuitive string-based key interface.
Optional Key Parameters
Keys MAY include parameters to represent variations or instances of a metadata type, such as "registration/1" or "name/Maria"; see ERC-8119 for the standard parameterized key format.
Optional Diamond Storage
Contracts implementing this ERC MAY use Diamond Storage pattern for predictable storage locations. If implemented, contracts MUST use the namespace ID "erc8048.onchain.metadata.storage".
The Diamond Storage pattern provides predictable storage locations for data, which is useful for cross-chain applications using inclusion proofs. For more details on Diamond Storage, see ERC-8042.
Examples
It is possible to use this standard to tokenize an agent as an NFT: each token ID is the agent, and metadata holds agent fields as UTF-8 in bytes.
Example: AI agent metadata (NFT as agent)
Two key patterns:
context — one key. UTF-8 Markdown is enough for humans and models; issuers MAY also embed a JSON code block so clients can parse token lists, policy URIs, or version fields without a second key.
endpoint[<type>] — one URL per protocol; <type> is lowercase (mcp, a2a, ag-ui, …).
Example for tokenId == 1: store the UTF-8 encoding of a Markdown document like the following as the bytes value for "context" (shown as a file; not a Solidity literal):
I am an agent that can swap tokens on Ethereum mainnet. I maintain an official token list and only suggest swaps where both assets appear on that list.
```json
{"tokenListUri":"ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/tokenlist.json","network":"mainnet","defaultSlippageBps":50}```
This profile, nicknamed ERC-721T, defines a reserved set of this ERC’s metadata keys for ERC-721 tokens that represent, expose, control, or are associated with agents. It is a standard use of this ERC’s existing key-value interface; it does not define a new Solidity interface, a new ERC-165 interface ID, or new events. Consumers read and write these records through metadata(uint256,string) and rely on the MetadataSet event for change notifications, exactly as for any other key under this ERC.
A contract implementing this profile MUST implement ERC-721 and this ERC. The profile reserves the following per-token keys (the agent example earlier in this section is the same pattern; this profile fixes the canonical key set):
Key
Meaning
Value (bytes)
context
Agent context for the token.
UTF-8 text. Markdown is RECOMMENDED; issuers MAY embed a fenced JSON block for machine-readable fields, as in the AI-agent example above.
endpoint[<type>]
Endpoint URI for the named protocol <type>.
UTF-8 URI. For endpoint[web], an https URI is RECOMMENDED.
address[<chain-id>]
The agent’s account on the chain identified by <chain-id>, where <chain-id> is the ERC-7930 Chain Identifier (see “Chain-keyed addresses” below).
The 20-byte EVM address (raw, not text).
Endpoint types
Keys are case-sensitive (the value of key is compared as exact bytes). Endpoint types in this profile are therefore lowercase, consistent with the endpoint[<type>] convention defined earlier in this section: endpoint[mcp] and endpoint[MCP] are distinct keys, and only the lowercase spelling carries the meaning reserved here. Implementations MUST write the canonical lowercase spelling for the types this profile reserves.
The canonical endpoint types are:
mcp — Model Context Protocol endpoint.
a2a — Agent-to-Agent endpoint.
web — general web endpoint (aligned with ERC-8004web service usage; the value MAY be any URI, with https RECOMMENDED).
x402 — payment-enabled endpoint.
Additional endpoint types MAY be used without changing this profile, provided they use the endpoint[<type>] form. Standards that reserve new types SHOULD define their exact canonical (lowercase) spelling.
Chain-keyed addresses
In address[<chain-id>], <chain-id> is the ERC-7930Chain Identifier — an Interoperable Address with a zero-length address part — encoded as a lowercase 0x-prefixed hex string. The stored value is a normal 20-byte EVM address, not an ERC-7930 interoperable address; ERC-7930 is used only to name the chain.
For example, the Chain Identifier for Ethereum mainnet (chain ID 1) is 0x00010000010100, and for Base (chain ID 8453, i.e. 0x2105) is 0x000100000202210500. The agent’s mainnet account would be stored as:
Key: "address[0x00010000010100]" → Value: the 20 bytes of the EVM address.
Marketplace compatibility
tokenURI(uint256) remains the ERC-721 marketplace metadata mechanism and is unchanged by this profile. Contracts SHOULD keep tokenURI JSON compatible with existing marketplaces (name, description, image). Agent-aware clients SHOULD read the keys above from this ERC’s records rather than from tokenURI, and SHOULD treat both endpoints and context as untrusted input. Transferring the token transfers ownership of the record, not any offchain server, key, balance, or memory the records may reference.
Optional Metadata Hooks
Contracts implementing this ERC MAY use metadata hooks to redirect record resolution to a different contract for secure resolution from known contracts, such as singleton registries with verifiable security properties.
For the full specification of metadata hooks, see ERC-8121 (Metadata Hooks). Hooks are encoded in the metadata value itself and allow clients to jump to another contract to resolve the metadata value. When using hooks for token metadata with this ERC, the return type MUST be bytes and the hook encoding MUST be bytes.
Many ERC-721, ERC-1155, and ERC-6909 registries are already deployed without the metadata function or without storage reserved for the onchain key-value mapping. Adding that interface or storage layout is often impossible without an upgrade path, so those contracts cannot adopt the core pattern of this ERC on the token contract itself.
This optional extension keeps discovery compatible with existing tokenURI / uri flows while directing clients to a separate metadata contract that implements metadata(uint256,string) as defined in this ERC. The target is identified by an ERC-7930Interoperable Address carried in JSON. The function to call and its signature are fixed by this specification.
tokenURI / uri JSON field
When tokenURI (ERC-721), uri (ERC-1155), or the equivalent URI function for ERC-6909 returns a string that resolves to a JSON document (including JSON embedded in a data: URL), the document MAY include a top-level string property named "metadata_contract".
The value of "metadata_contract" MUST be the interoperable address encoded as a single JSON string: a 0x prefix followed by the hex encoding of the ERC-7930 bytes, all lowercase.
Agent NFT Example:
{"name":"Example","image":"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi","metadata_contract":"0x00010000010114d8da6bf26964af9d7eed9e03e53415d37aa96045","endpoint[mcp]":"https://agents.example.com/mcp/1","context":"I am an agent that can swap tokens on Ethereum mainnet."}
It is possible to include metadata both directly in the tokenURI / uri JSON and in the metadata_contract for additional metadata.
Client behavior
Clients that support this extension:
Obtain and parse the JSON from tokenURI / uri as they already do for display metadata.
If a top-level "metadata_contract" string is present, decode the value as ERC-7930 interoperable address bytes from the 0x-prefixed hex string.
Parse the interoperable address to obtain the target chain and contract address.
Read metadata(uint256 tokenId, string key) from that contract on the target chain, using the same tokenId as for the tokenURI / uri call and the metadata key from this ERC. The return value is the same as if the token contract had stored the mapping locally.
In most cases the metadata contract SHOULD be on the same chain as the NFT token registry, but it is possible to use a metadata contract on a different chain, depending on support for cross-chain reads in clients and in the ecosystem in general.
Clients that do not implement this extension or do not trust the target contract address MAY ignore "metadata_contract" and continue to use other JSON fields.
Relationship to IERC8048Metadata
Registries using only this extension are NOT required to implement IERC8048Metadata on the token contract. The contract referenced by metadata_contract MUST implement the metadata(uint256,string) external view returns (bytes) function from this ERC (or a compatible implementation).
Security: clients SHOULD only call metadata contracts they consider trustworthy; a malicious or mistaken metadata_contract could return attacker-controlled bytes. Clients SHOULD show or verify the target address the same as for any new contract interaction.
Rationale
This ERC standardizes a simple string-key, bytes-value metadata store for existing token registries. The optional setMetadata function allows updates under the contract’s chosen write policy, and MetadataSet provides an onchain audit trail. Onchain Metadata Contract Reference extends this model to already-deployed tokens by pointing metadata_contract to a sidecar via a ERC-7930 address in tokenURI / uri JSON.
The Onchain Metadata Contract Reference extension only adds an optional JSON property; clients that do not read "metadata_contract" behave as they do today.
Reference Implementation
The interface is defined in the Required Metadata Function and Event section above. Here are reference implementations:
Basic Implementation
pragmasolidity^0.8.25;import"./IERC8048Metadata.sol";contractOnchainMetadataExampleisIERC8048Metadata{// Mapping from tokenId => key => value
mapping(uint256=>mapping(string=>bytes))private_metadata;/// @notice Get metadata value for a key
functionmetadata(uint256tokenId,stringcalldatakey)externalviewoverridereturns(bytesmemory){return_metadata[tokenId][key];}/// @notice Set metadata for a token (optional implementation)
functionsetMetadata(uint256tokenId,stringcalldatakey,bytescalldatavalue)external{_metadata[tokenId][key]=value;emitMetadataSet(tokenId,key,key,value);}}