abx.
Protocol

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.

Reserved fields

A field's JSON key is either a reserved key, projected straight into the served document, or an arbitrary creator key, which never appears at the top level — it surfaces only through the data plane's artifacts manifest.

ScopeReserved keys
Token (tokenURI)name, description, image, animation_url, external_url, attributes, background_color, youtube_url
Collection (contractURI)name, description, banner_image, featured_image, external_link, collaborators, artist, display_notes, artist_links, license

The collection-scope authorship-and-rights keys — artist, display_notes, artist_links, and license (a rights identifier such as CC0-1.0 or CC-BY-NC-4.0) — are ordinary reserved fields, not a separate extension. Set them at deploy (abx deploy … --artist "…" --license "…") or later (abx set-field <addr> --collection --field artist --text "…"); both the resolver and the on-chain renderer project them into contractURI when present.

Three more keys appear in served JSON but are never set with set-field — they are computed, assembled at read time from state no single field holds:

  • artifacts: the data plane's complete listing of every named piece of content the token anchors, reserved-key duplicates included.
  • abx_params: the token's configured parameters — every set value, contract scope and token scope merged. Parameters enumerate on-chain, so this block is complete on both the resolver and the on-chain renderer.
  • abx_provenance: one entry per served field, saying where its value came from and whether the chain vouches for it — see Provenance.

set-field and attach both refuse artifacts, abx_params, and abx_provenance as field names.

Representations

RepresentationValueWhere the content lives
inlinethe content byteson-chain, in the field
inline-gzipgzip(content)on-chain, decompressed off-chain
readera reader contract and a storage pointeron-chain, behind a reader contract
reader-gzipas reader, but the assembled bytes are gzip(content)on-chain, decompressed off-chain
renderera field-renderer contract addresscomputed on read, nothing stored
arweavean Arweave transaction idoff-chain, content-addressed
ipfsan IPFS CIDoff-chain, content-addressed
urla URLoff-chain
url-templatea URL with an {id} placeholderoff-chain; the resolver substitutes the token id
keccak256 / sha256a hash of the contentoff-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 — see On-chain storage for the reference implementation. 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. It is also the only representation that declares its own type: render returns a content type alongside the bytes, and one value of it is special — text/uri-list means the computed bytes are a locator, so they land verbatim as the field's value instead of being wrapped as a data: URI. That is how a computed field points somewhere rather than carrying content.
  • 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, url, and url-template. 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.

Configured parameters are not fields and carry no such limit. The parameter store enumerates its own keys on-chain, so the renderer emits the token's complete configured set as abx_params — a field with an arbitrary key needs a resolver to be seen; a parameter never does.

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.

Worked examples

Same reserved-field vocabulary, two different resolution paths.

On-chain rendered. An image field renderer computes the SVG at read time; tokenURI is a data:application/json;base64 document, decoded here:

{
  "name": "Drift #42",
  "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...",
  "attributes": [{ "trait_type": "Palette", "value": "#0e1a40" }],
  "artifacts": [
    { "key": "image", "mimeType": "image/svg+xml", "uri": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..." }
  ],
  "abx_params": { "palette": "#0e1a40" },
  "abx_provenance": [ // {field, source, onChain, note} per field
    { "field": "name", "source": "fallback", "onChain": true },
    { "field": "image", "source": "renderer", "onChain": true },
    { "field": "attributes", "source": "renderer", "onChain": true },
    { "field": "abx_params", "source": "params", "onChain": true }
  ]
}

No off-chain custody, no resolver: one eth_call to tokenURI reconstructs the whole document. artifacts carries exactly one entry here — the reference renderer emits it only for a renderer-represented image, whose content type is already known from the staticcall. Every other on-chain representation would duplicate a reserved key, so it's left out. abx_params carries the collector-set palette the renderer drew the SVG from — enumerated from the chain, not from a list anyone maintains.

Off-chain resolver. The same reserved keys, but image is a creator-set ipfs field, and a render effect produced a still and a 3D model:

{
  "name": "Drift #7",
  "image": "ipfs://QmStill/nft.png",
  "artifacts": [
    { "key": "image",               "mimeType": "image/png",         "uri": "ipfs://QmStill/nft.png" },
    { "key": "render/image",        "mimeType": "image/png",         "uri": "ipfs://QmRender" },
    { "key": "world.rebuild/model", "mimeType": "model/gltf-binary", "uri": "ar://TX" }
  ],
  "abx_provenance": [ // {field, source, status, note} per field
    { "field": "image", "source": "ipfs", "status": "anchored" },
    { "field": "world.rebuild/model", "source": "effect:world.rebuild", "status": "off-chain" }
  ]
}

Here artifacts lists the creator's image (deliberately duplicating the reserved key) plus every current effect output — a resolver has no size or field-enumeration limit an on-chain renderer does.

On-chain storage, Field renderers, Data plane, Effects

On this page