Onchain Metadata for Token Registries
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.
Examples
Event tickets are a good fit for onchain metadata. A ticket’s details are what give it value, so they belong with the token, and the token remains a complete record of the event after it ends.
Example: Event ticket
An ERC-721 token is a single ticket. For tokenId == 1:
"event"→bytes("Example Jazz Festival 2027, Night 1")"venue"→bytes("Example Arena, Lisbon")"start"→abi.encode(uint64(1815249600))(Unix time, July 10, 2027, 20:00 UTC)"seat"→bytes("Section 104, Row F, Seat 12")
A venue contract or scanner can read these values onchain at the door without an offchain API. After the event, the issuer can add records such as "setlist", so the ticket also becomes a commemorative item.
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:
| 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. |
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 Address component of an ERC-7930 Interoperable Address for the chain named by <chain-id>. For an EVM chain, the 20-byte address. |
account[<chain-id>][<index>] |
OPTIONAL. An additional account of the agent on the chain identified by <chain-id>, distinguished by <index>. See “Additional accounts” below. |
The Address component of an ERC-7930 Interoperable Address for the chain named by <chain-id>. For an EVM chain, the 20-byte address. |
Endpoint types
Keys are case-sensitive (the value of key is compared as exact bytes). Endpoint types in this profile are therefore lowercase: 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-8004webservice usage; the value MAY be any URI, withhttpsRECOMMENDED).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 the Address component of an ERC-7930 Interoperable Address for that chain, the chain’s native address bytes. It is the Address component alone, not a full Interoperable Address.
For example, the Chain Identifier for Ethereum mainnet (chain ID 1) is 0x00010000010100, and for Base (chain ID 8453, i.e. 0x2105) is 0x0001000002210500. The agent’s mainnet account would be stored as:
- Key:
"address[0x00010000010100]"→ Value: the 20 bytes of the EVM address.
Additional accounts
An agent MAY list additional accounts under the OPTIONAL key account[<chain-id>][<index>]. Any account qualifies, for example a token-bound account or a smart contract wallet.
address[<chain-id>] is the agent’s primary account on that chain. Clients SHOULD send funds to address[<chain-id>] and SHOULD NOT assume an account listed under account[<chain-id>][<index>] accepts payments.
<chain-id> is the ERC-7930 Chain Identifier and the stored value is the ERC-7930 Address component for that chain, both exactly as in address[<chain-id>].
<index>, an unsigned base-10 integer with no leading zeros, distinguishes multiple accounts on the same chain.
For example, an agent’s additional account at index 0 on Ethereum mainnet would be stored as:
- Key:
"account[0x00010000010100][0]"→ 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 the contract’s own write policy or a distinct metadata authority holds write authority.
Optional Metadata Authority Extension
This optional, standalone extension lets a token’s metadata be written by a metadata authority instead of the token’s owner or holders. The authority can be an account or a contract. A contract authority can apply its own rules, for example letting different parties write different keys. 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.
Authorization
For every function that writes metadata for a token, implementations MUST apply the following authorization semantics:
- When
metadataAuthority(tokenId) != address(0),setMetadataMUST authorize only that exact authority. - When
metadataAuthority(tokenId) == address(0), no authority is set. Whether anyone can update the token’s metadata, and who, is up to the contract’s own write policy, and the metadata may not be updatable at all.
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. Two authorities each maintain their own regulated fields, while the owner maintains everything else:
- The housing authority writes
gov.housing-authority.size(the assessed size). - The county assessor writes
gov.county-assessor.value(the assessed value). - The owner writes
usr.<key>records (for exampleusr.listing-note).
Using its implementation-specific mechanism, the implementation sets a policy contract as the token’s metadata authority. While metadataAuthority(tokenId) returns the policy contract, every metadata write goes through it, and owner status alone is not enough. The policy contract lets the housing authority write only the size, the county assessor write only the value, and the owner write usr.* keys.
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 its address, carried in JSON, and is on the same chain as the token registry. 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 address of the metadata contract encoded as a single JSON string: a 0x prefix followed by the 40 hexadecimal characters of the 20-byte address. Clients MUST accept any letter case.
Agent NFT Example:
{
"name": "Example",
"image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"metadata_contract": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"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/urias they already do for display metadata. - If a top-level
"metadata_contract"string is present, parse the value as a 20-byte address. If it is not a0x-prefixed string of 40 hexadecimal characters, ignore it. - Read
metadata(uint256 tokenId, string key)from that contract on the same chain as the token registry, using the sametokenIdas for thetokenURI/uricall and the metadata key from this ERC. The return value is the same as if the token contract had stored the mapping locally.
The metadata contract is always on the same chain as the token registry. The target MUST implement metadata(uint256,string), which makes it an EVM contract, so a chain-agnostic address format adds nothing here, and a plain address is what an implementer reaches for first.
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 write authority for a token rests with a distinct authority or with the contract’s own write policy. One authority per token keeps the interface small. When different parties need to govern different keys, the authority can be a contract that applies those rules. Onchain Metadata Contract Reference extends this model to already-deployed tokens by pointing metadata_contract in tokenURI / uri JSON to a sidecar by its contract address.
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;
}
}
Metadata Authority 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, written for ERC-721. 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 OnchainMetadataAuthorityExample is
IERC8048Metadata,
IERC8048MetadataAuthority,
IERC165
{
// Mapping from tokenId => key => value
mapping(uint256 => mapping(string => bytes)) private _metadata;
// Mapping from tokenId => authority
mapping(uint256 => address) private _metadataAuthority;
/// @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 _metadata[tokenId][key];
}
/// @notice Return the current authority, or zero when none is set.
function metadataAuthority(uint256 tokenId) external view override returns (address) {
return _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 {
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 {
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.
Copyright
Copyright and related rights waived via CC0.