abx.
Protocol

On-chain storage

The reader representation stores a field's content on-chain and serves it through a reader contract, so tokenURI can return real bytes with no server involved. This page opens up the reference reader — how it stores content, compresses it, and stitches multi-chunk content back together on read.

The reference reader: AbxChunkStore

AbxChunkStore is the reader every ABX project shares. One instance per chain, deployed once, ownerless — the CLI deploys it automatically the first time a chain needs it. A reader field's value is abi.encode(store, manifest); manifest is the pointer, and store.read(manifest) returns the fully assembled, decompressed content.

The store never interprets content — bytes in, bytes out. Compression and chunk boundaries are decided off-chain, by the CLI, before anything is written; the store only stitches and, where flagged, decompresses.

SSTORE2: bytes as bytecode

SSTORE2 stores a blob by deploying it as the runtime code of a tiny, immutable contract: writing costs the EVM's code-deposit gas (~200 gas/byte) rather than SSTORE's per-slot cost, and reading is EXTCODECOPY, a cheap view call. EIP-170 caps a contract's code at 24,576 bytes, so one SSTORE2 write is one chunk, bounded there.

ABX chunks at 22,000 bytes (the SDK's DEFAULT_CHUNK_SIZE), conservatively under that ceiling so a chunk that happens to grow slightly (an incompressible piece under FastLZ) still fits.

Chunk-stitching: content bigger than one write

Content over one chunk is split, each piece written as its own SSTORE2 data contract, and tied together by a manifest: an ordered list of {pointer, compressed}. Reading replays the list — AbxChunkStore.read:

function read(address pointer) external view returns (bytes memory) {
    Chunk[] memory chunks = abi.decode(SSTORE2.read(pointer), (Chunk[]));
    bytes memory out;
    for (uint256 i; i < chunks.length; ++i) {
        bytes memory part = SSTORE2.read(chunks[i].pointer);
        if (chunks[i].compressed) part = LibZip.flzDecompress(part);
        out = bytes.concat(out, part);
    }
    return out;
}

Two staging paths, chosen automatically from the byte count:

Content sizePathTransactions
Fits one transaction's gas budgetwriteContent(datas, compressed)1, atomic
LargerwriteChunk (batched via multicall) for every chunk, then writeManifestone per batch, plus 1

Both paths are ownerless writes to the shared store — the project owner only signs the set-field (or the deploy that bakes the reader value in) that references the finished manifest, never the chunk-staging transactions themselves.

Compression: two mechanisms, two guarantees

--compress <none|fastlz|gzip> on deploy --onchain-image, deploy-series, and set-field --file picks between two independent schemes:

FlagWhat it compressesRepresentationWho decompresses
nonenothingreader
fastlzeach chunk, independently, only kept if it actually shrinksreaderthe reader, on-chain, at read time
gzipthe whole content, before it's ever chunkedreader-gzipthe resolver, off-chain

FastLZ is chunk-local because Solady ships an on-chain decoder for it (LibZip.flzDecompress), so the reader can decode it during read() and the field stays on-chain-renderable. gzip has no on-chain decoder, so a gzip-compressed field can never be read by a contract, only by an off-chain resolver — the representation tag says so explicitly, and an on-chain metadata renderer can't serve it at all. inline-gzip is the same trade-off for content small enough to skip the reader path — a single field value, no chunking.

FastLZ pays off on already-text-like bytes — SVG, JSON, HTML/JS shrink meaningfully. PNG and JPEG are already entropy-coded and gain close to nothing from it.

Cost, and when on-chain is the right call

SSTORE2's code-deposit cost (~200 gas/byte) makes on-chain storage cost-effective only for small content:

  • ≲ 24 KB per file — about one chunk.
  • ≲ 256 KB total per project — past this the CLI's dry-run warns loudly on deploy --onchain-image and set-field --file alike.

Past these, off-chain storage (Arweave, IPFS, S3) is far cheaper per byte. The reader's actual edge past tiny sizes is self-resolution and permanence, not cost. A field renderer, which computes bytes at read time instead of storing them, sidesteps the size question entirely for generative output.

Reading and writing, end to end

Metadata, Field renderers, Interfaces

On this page