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-7572contractURIreads.
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
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, andIAbxOnChainReader.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, andIAbxFieldRenderer.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
-gzipsuffix, because gzip cannot be reversed on-chain. A-gzipfield is not on-chain-renderable.
How tokenURI resolves
How the tokenURI and contractURI documents are produced is configurable per contract:
- Off-chain pointer (default).
tokenURIreturns a URL to a resolver, which assembles the JSON from the fields. - On-chain renderer.
tokenURIreturns adata:application/jsondocument assembled on-chain by a contract behindIAbxMetadataRenderer.
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:
- If a renderer is set, resolve on-chain through it.
- Otherwise, if a per-token override URL is set, return it.
- Otherwise, if a base URL is set, return
{base}/{chainId}/{address}/{tokenId}. - 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.