Metadata

ABX stores a token's metadata as a set of fields on-chain. Each field maps to one representation and a value. A field can be stored fully on-chain, located off-chain, or committed on-chain by a hash. The model is deterministic: for a field it can resolve, any resolver reading the same chain produces the same value.

Fields, representations, and values

Metadata is a key-value store at two scopes:

  • Token scope: tokenId -> field -> { representation, value }.
  • Collection scope: field -> { representation, value }, contract-wide. This is what ERC-7572 contractURI reads.

A field is the JSON key it produces, such as image, description, or attributes. A representation says how the value is carried. A field has exactly one active representation; setting it again replaces the previous one. Field resolution reads token scope first and falls back to collection scope.

The contract stores the representation as an opaque value and does not act on it. The representation vocabulary is a protocol convention, defined in the SDK rather than in Solidity, so new representations are additive.

Representations

Representation Value Where the content lives
inline the content bytes on-chain, in the field
inline-gzip gzip(content) on-chain, decompressed off-chain
reader a reader contract and a storage pointer on-chain, behind a reader contract
renderer a field-renderer contract address computed on read, nothing stored
arweave an Arweave transaction id off-chain, content-addressed
ipfs an IPFS CID off-chain, content-addressed
url a URL off-chain
url-template a URL with an {id} placeholder off-chain; the resolver substitutes the token id
keccak256 / sha256 a hash of the content off-chain, verified against the on-chain hash

Three mechanisms sit behind these:

  • On-chain storage (reader) returns bytes that were written to chain storage. The field stores a reader contract and a pointer, and IAbxOnChainReader.read(pointer) returns the stored content. Storage details such as SSTORE2, compression, and chunk stitching live inside the reader. The content is fixed.
  • On-chain compute (renderer) produces the value at read time. The field stores only a field-renderer address, and IAbxFieldRenderer.render(token, tokenId, field), a view function, computes the value from current chain state. Nothing is stored for the value, so a renderer's output can depend on the token's state, such as its parameters, seed, or owner, while staying deterministic and reproducible.
  • Off-chain compression is signaled by the -gzip suffix, because gzip cannot be reversed on-chain. A -gzip field is not on-chain-renderable.

How tokenURI resolves

How the tokenURI and contractURI documents are produced is configurable per contract:

  • Off-chain pointer (default). tokenURI returns a URL to a resolver, which assembles the JSON from the fields.
  • On-chain renderer. tokenURI returns a data:application/json document assembled on-chain by a contract behind IAbxMetadataRenderer.

The toggle is the stored renderer address: non-zero resolves on-chain, zero uses the stored pointer. Resolution precedence is the same for tokenURI and contractURI:

  1. If a renderer is set, resolve on-chain through it.
  2. Otherwise, if a per-token override URL is set, return it.
  3. Otherwise, if a base URL is set, return {base}/{chainId}/{address}/{tokenId}.
  4. Otherwise, return an empty string.

An on-chain renderer can serve only the representations it can produce on-chain: inline, reader, renderer, and url. A field that points off-chain (ipfs, arweave) or is committed by hash (keccak256, sha256) is served by the off-chain resolver instead. A project that resolves fully on-chain therefore uses on-chain representations for every field.

Locking

Two locks make metadata permanently immutable: a per-field lock, on (scope, field), freezes a field's representation and value; a URI-config lock freezes the pointer and renderer. With both set, the resolved metadata cannot change.

Provenance

Served metadata includes an abx_provenance entry per field. It records the source (for example inline, ipfs, renderer, or effect:<key>) and a status: on-chain, verified, mismatch, anchored, off-chain, or n/a. This states, per field, where a value came from and whether the chain commits to it.