Abstract

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:

interface IERC8048Metadata {
    /// @notice Get metadata value for a key.
    function metadata(uint256 tokenId, string calldata key) external view returns (bytes memory);
    
    /// @notice Emitted when metadata is set for a token.
    event MetadataSet(uint256 indexed tokenId, string indexed indexedKey, string key, bytes value);
}
  • 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:

event MetadataSet(uint256 indexed tokenId, string indexed indexedKey, string key, bytes value);

Interface Detection

The interface ID is 0xdf670be1.

Contracts implementing IERC8048Metadata MUST implement ERC-165 and MUST return true from supportsInterface for both 0xdf670be1 (this interface) and 0x01ffc9a7 (ERC-165 itself), so that callers can detect onchain-metadata support before reading records.

Key/Value Pairs

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
}
```

Endpoint keys for the same token:

  • "endpoint[mcp]"bytes("https://agents.example.com/mcp/1")
  • "endpoint[a2a]"bytes("https://agents.example.com/a2a/1")
  • "endpoint[ag-ui]"bytes("https://agents.example.com/ui/1")

Example: Biometric Identity for Proof of Personhood

A biometric identity system using open source hardware to create universal proof of personhood tokens.

  • Key: "biometric_hash" → Value: bytes(bytes32(identity_commitment))
  • Key: "verification_time" → Value: bytes(bytes32(timestamp))
  • Key: "device_proof" → Value: bytes(bytes32(device_attestation))

Agent Metadata Profile (ERC-721T)

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-8004 web 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-7930 Chain 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. Where the Metadata Authority extension is in use, clients SHOULD read metadataAuthority(tokenId) to determine whether ERC-721 management authority (owner, approved, or operator) or a distinct metadata authority holds write authority.

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.

Optional Metadata Authority Extension

This optional, standalone extension lets the account authorized to write a token’s metadata be a metadata authority that is distinct from the token’s current owner. It is intended for assets where a party other than the holder governs certain records, for example a registered authority that maintains regulated fields on a real-world asset.

An implementation of this extension MUST also implement IERC8048Metadata (the core interface above) and MUST implement ERC-165.

The standardized surface is a single read function and one event.

interface IERC8048MetadataAuthority {
    /// @notice The current metadata authority for `tokenId`.
    function metadataAuthority(uint256 tokenId) external view returns (address);

    /// @notice Emitted whenever `tokenId`'s metadata authority is set, changed, or cleared.
    /// `authority` is the new authority, or the zero address when cleared.
    event MetadataAuthoritySet(uint256 indexed tokenId, address indexed authority);
}

Extension Interface Detection

This extension’s ERC-165 interface ID is 0xf9cb127d; contracts implementing it MUST return true from supportsInterface for 0xf9cb127d.

Applicability (unique current owner required)

An implementation MUST NOT implement this extension unless it can determine a canonical unique current owner for each tokenId. ERC-721 tokens qualify because each token has exactly one owner (approvals and operators are not owners, though they may write metadata in the zero-authority case). Ordinary multi-holder ERC-1155 and ERC-6909 balances do not qualify because they lack a unique owner.

Authorization

For every function that writes metadata for a token, implementations MUST apply the following authorization semantics:

  • When metadataAuthority(tokenId) == address(0), setMetadata MUST authorize the token’s owner, its getApproved(tokenId) address, or any operator approved for the owner via isApprovedForAll.
  • When metadataAuthority(tokenId) != address(0), setMetadata MUST authorize only that exact authority.

A successful metadata write emits MetadataSet. A call that fails authorization MUST NOT emit MetadataSet and MUST revert.

Setting the authority (implementation-specific)

Setting, changing, or clearing the authority is NOT part of this extension’s interface and is implementation-defined. Whatever mechanism an implementation uses, it MUST emit MetadataAuthoritySet whenever a token’s metadata authority is set, changed, or cleared, with authority set to the new authority, or to address(0) when the authority is cleared.

Example: owner and authority fields on a real-world asset

Consider a house tokenized as an ERC-721. The owner maintains owner-controlled fields while a housing authority maintains regulated fields:

  • The owner writes usr.<key> records (for example usr.listing-note).
  • The housing authority writes gov.housing-authority.size (the assessed size).

The housing authority is not the token owner, so the implementation assigns a policy contract as the metadata authority using its implementation-specific mechanism. While metadataAuthority(tokenId) returns that policy contract’s address, metadata writes must be authorized by the policy contract; owner status alone is insufficient. The policy contract can authorize the owner to write usr.* and the housing authority to write gov.housing-authority.*. How the metadata authority is assigned, changed, or cleared is outside this extension’s standardized interface.

Onchain Metadata Contract Reference (Optional Extension)

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-7930 Interoperable 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:

  1. Obtain and parse the JSON from tokenURI / uri as they already do for display metadata.
  2. If a top-level "metadata_contract" string is present, decode the value as ERC-7930 interoperable address bytes from the 0x-prefixed hex string.
  3. Parse the interoperable address to obtain the target chain and contract address.
  4. 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. The Metadata Authority extension standardizes a read for discovering whether metadata write authority rests with the token’s ERC-721 owner/approved/operator (when no authority is set). 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.

Backwards Compatibility

  • Fully compatible with ERC-721, ERC-1155, and ERC-6909.
  • Non-supporting clients can ignore the scheme.
  • 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

pragma solidity ^0.8.25;

import "./IERC8048Metadata.sol";

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

contract OnchainMetadataExample is IERC8048Metadata, IERC165 {
    // Mapping from tokenId => key => value
    mapping(uint256 => mapping(string => bytes)) private _metadata;
    
    /// @notice Get metadata value for a key
    function metadata(uint256 tokenId, string calldata key) 
        external view override returns (bytes memory) {
        return _metadata[tokenId][key];
    }
    
    /// @notice Set metadata for a token (optional implementation)
    function setMetadata(uint256 tokenId, string calldata key, bytes calldata value) 
        external {
        _metadata[tokenId][key] = value;
        emit MetadataSet(tokenId, key, key, value);
    }

    /// @notice ERC-165 interface detection
    function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
        return interfaceId == 0xdf670be1 || interfaceId == type(IERC165).interfaceId;
    }
}

Diamond Storage Implementation

pragma solidity ^0.8.20;

import "./IERC8048Metadata.sol";
import "./IERC8048MetadataAuthority.sol";

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

/// @dev Reference implementation of the core interface plus the metadata authority
///      extension, using Diamond Storage. It is `abstract`: a concrete contract
///      supplies `_uniqueOwnerOf` (the token's unique current owner, or `address(0)`
///      if the token does not exist). `setMetadataAuthority` is one example
///      assignment policy (the RECOMMENDED one) and is NOT part of the standardized
///      interface; the `MetadataAuthoritySet` it emits is the standardized interface
///      event, inherited from `IERC8048MetadataAuthority`.
abstract contract OnchainMetadataAuthorityDiamondExample is
    IERC8048Metadata,
    IERC8048MetadataAuthority,
    IERC165
{
    struct OnchainMetadataStorage {
        mapping(uint256 tokenId => mapping(string key => bytes value)) metadata;
        mapping(uint256 tokenId => address authority) metadataAuthority;
    }

    // keccak256("erc8048.onchain.metadata.storage")
    bytes32 private constant ONCHAIN_METADATA_STORAGE_LOCATION =
        keccak256("erc8048.onchain.metadata.storage");

    function _getOnchainMetadataStorage() private pure returns (OnchainMetadataStorage storage $) {
        bytes32 location = ONCHAIN_METADATA_STORAGE_LOCATION;
        assembly {
            $.slot := location
        }
    }

    /// @dev The unique current owner of `tokenId`, or `address(0)` if it does not exist.
    ///      Concrete contracts wire this to their token logic (e.g. ERC-721 `ownerOf`,
    ///      returning zero instead of reverting for a nonexistent token). ERC-721
    ///      approvals and operators MUST NOT be returned here.
    function _uniqueOwnerOf(uint256 tokenId) internal view virtual returns (address);

    /// @dev Conceptual ERC-721 approval views the concrete contract wires to its token
    ///      logic: the single approved address for `tokenId` (`getApproved`) and operator
    ///      approval for `owner` (`isApprovedForAll`). Used only in the zero-authority
    ///      metadata-write branch below; they never authorize setting the authority.
    function _getApproved(uint256 tokenId) internal view virtual returns (address);
    function _isApprovedForAll(address owner, address operator) internal view virtual returns (bool);

    // --- Reads (unchanged by the extension) ---

    function metadata(uint256 tokenId, string calldata key)
        external view override returns (bytes memory) {
        return _getOnchainMetadataStorage().metadata[tokenId][key];
    }

    /// @notice Return the current authority, or zero when none is set.
    function metadataAuthority(uint256 tokenId) external view override returns (address) {
        return _getOnchainMetadataStorage().metadataAuthority[tokenId];
    }

    // --- Writes (metadata records: owner or ERC-721 approval when no authority; else authority) ---

    function setMetadata(uint256 tokenId, string calldata key, bytes calldata value)
        external {
        OnchainMetadataStorage storage $ = _getOnchainMetadataStorage();
        address authority = $.metadataAuthority[tokenId];
        if (authority == address(0)) {
            address owner = _uniqueOwnerOf(tokenId);
            require(owner != address(0), "nonexistent token");
            require(
                msg.sender == owner
                    || msg.sender == _getApproved(tokenId)
                    || _isApprovedForAll(owner, msg.sender),
                "not authorized"
            );
        } else {
            require(msg.sender == authority, "not authority");
        }
        $.metadata[tokenId][key] = value;
        emit MetadataSet(tokenId, key, key, value);
    }

    /// @dev Example assignment policy only; not part of IERC8048MetadataAuthority.
    ///      Demonstrates the RECOMMENDED policy: the owner may set the authority
    ///      while it is unset; once set, only the current authority may change or
    ///      clear it (the zero address clears it). Other implementations may differ.
    function setMetadataAuthority(uint256 tokenId, address newAuthority) external {
        OnchainMetadataStorage storage $ = _getOnchainMetadataStorage();
        address previous = $.metadataAuthority[tokenId];
        if (previous == address(0)) {
            address owner = _uniqueOwnerOf(tokenId);
            require(owner != address(0) && msg.sender == owner, "not owner");
        } else {
            require(msg.sender == previous, "not authority");
        }
        $.metadataAuthority[tokenId] = newAuthority;
        emit MetadataAuthoritySet(tokenId, newAuthority);
    }

    // --- Introspection ---

    /// @notice ERC-165 interface detection: core, authority extension, and ERC-165.
    function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
        return interfaceId == 0xdf670be1
            || interfaceId == 0xf9cb127d
            || interfaceId == type(IERC165).interfaceId;
    }
}

Security Considerations

This ERC is designed to put metadata onchain, providing security benefits through onchain storage.

Implementations that choose to use the optional Diamond Storage pattern should consider the security considerations of ERC-8042.

Copyright and related rights waived via CC0.