abx.
Protocol

Code projects

A code project is an ABX contract whose content is a program. Its output follows the determinism contract, (content, params, environment) -> output.

Two custody modes

  • Directory. The code is an off-chain build directory with an index.html entry, on IPFS, Arweave, or a URL. The on-chain surface is the code collection field, which holds a locator only, never inline bytes.
  • Template. The code is stored on-chain in chunks, through the On-Chain Script and Dependencies extensions. Dependency index 0 is the runtime, by convention. A generator assembles the dependencies in order, injects the token data, and concatenates the script chunks.

tokenData

The program receives one flat object, tokenData. It is the contract-scope and token-scope parameters merged, with token scope winning, and read-time hook entries applied last. Each value is decoded to its canonical string form. The key set comes off the chain — the parameter store enumerates its own keys — so no tool anywhere maintains a list of what a project has set.

{
  "chainId": 1,
  "contractAddress": "0x1a2b...",
  "tokenId": "42",
  "seed": "0x9f31...",
  "palette": "#0e1a40",
  "density": "0.85"
}

chainId, contractAddress, tokenId, and seed are reserved keys. seed is present only when a seed source assigned it. The serialization is canonical: JSON, keys sorted, no insignificant whitespace, UTF-8, because it is hashed as part of an effect's inputs.

tokenData reaches the program three ways: injected as a window.abxTokenData global (template, or a resolver-served directory), carried in the URL as a base64 abx query parameter (directory, budgeted at about 8 KB), or passed to a render node as input. A runtime companion, abx.js, resolves the token data from the injected global, then the query parameter, then RPC. It also exposes abx.traits({...}) to report traits and abx.done() to signal that output is complete. abx.js is a convention, not protocol.

To exercise all of this before deploying anything, abx preview --script <file> serves the same document the generator serves — same abx.js, same canonical tokenData shape, same dependency tags — on localhost, with a synthetic seed standing in for a minted one. That makes the contract above something you can drive and watch rather than reason about: shuffle seeds, set a PostParam from a real input, and see exactly what abx.traits() reported.

Seeds

A mint-time seed is configuration, through the Seed Source extension. A contract stores a seed-source address; at mint it calls IAbxSeedSource.seed(tokenId, to) and stores the result as the token's seed parameter. The canonical source is AbxSeedSource, one shared pseudorandom source per chain, deriving its value from the calling project, the token id, and block values (prevrandao, the previous block hash, the timestamp). A project can point at any contract that satisfies the interface, such as a commit-reveal or a creator-curated source.

Two properties follow from deriving a seed on-chain, and a project should know both. It is deterministic after the fact — anyone can replay it from the block, which is what makes generative output verifiable, and is the property this source exists for. It is not secret before the fact — every input is readable inside the minting transaction, so a contract executing there can compute the seed a mint would receive and choose whether to complete that mint. A buyer can decline an outcome (mint inside a wrapper that reverts unless the result is favourable, paying only gas); a builder can reorder or omit transactions. The recipient (to) is deliberately not in the preimage, because the buyer chooses it: including it made a caller-supplied field into a search space — grind candidates in a view loop, then buy once at the winner. Without it, what remains on the 721 lane is accept-or-decline rather than target. A custom source is free to use to, and inherits that grinding surface if it does.

On an edition, "choose" is the honest word, and that is intentional. A SeriesCode mint is sequential — the buyer takes the next id and has nothing to select — but an edition is bought by id, and an EditionCode id has its own seed, drawn at that id's first mint (the seed belongs to the work, not to a copy). So a buyer picking an already-minted id is choosing among settled, publicly visible works — the normal case for an edition, and the point of one — while a buyer minting an unminted id draws its seed with their chosen id in the preimage, which is a genuine selection surface bounded only by which ids have a configured sale. Do not describe the edition lane as accept-or-decline; if a single outcome must not be selectable, mint the ids yourself before selling, or use a custom source.

Not strong enough for lottery-like logic

The canonical seed is pseudorandom, generated from on-chain values, and is not strong enough to run lottery-like logic — a prize draw, a raffle, or any mint where one rare outcome is worth materially more than the mint price and transaction ordering is contestable. It suits generative work, where the seed diversifies output and the distribution, not any single outcome, is the product. If seed generation must be fully random, point seedSource at your own IAbxSeedSource backed by a commitment scheme (commit at mint, resolve from a later block) or an off-chain VRF oracle. The seed source is a per-project address, so that is a swap, not a fork of ABX — abx deploy-code --seed-source 0x... sets it at birth and abx set-seed-source <address> 0x... re-points it later (owner-only, future mints only). Both probe the candidate first — it must answer seed(uint256,address) with 32 bytes — and refuse otherwise, because a source that cannot answer reverts every mint while seedSource(), the SeedSourceSet event, and every other read surface still report the project as configured.

Settled, and settled before the sale. A seed is settled once assigned: no route rewrites or clears it, and it is never data-backed. One exemption exists — declare a seed parameter schema and the governed path may reassign the value, typically authorizing TokenOwner so a collector can set the seed of their own token.

Be precise about what that is, because the obvious word for it is the wrong one. The governed write is configureTokenParam(tokenId, "seed", value): the caller supplies the value. It is a collector-chosen (or creator-chosen) seed, not a re-roll. Nothing is re-randomized, the seed source is never consulted again, and there is no draw to be fair or unfair about — with an unbounded Uint256Range the authorized party may set literally any 32-byte value, and can keep setting it until they like the result. Say "pick your seed", never "re-roll", when you describe this to a buyer: a re-roll implies a fresh random outcome, and a buyer who expects one has been told something untrue.

That schema is a pre-sale commitment: it cannot be declared or re-pointed once any seed in the collection exists, so a project cannot sell out and then start letting seeds be rewritten on work a collector already paid for. paramSchema("seed") is the read a buyer uses to learn, before buying, whether seeds can be set at all, by whom, and within what bounds.

On-chain rendering

The canonical field renderer, AbxGenerator, produces the animation field for both custody modes:

  • Template branch. With script chunks present, it emits a full HTML document: the token data, abx.js, the dependencies in order, and the script chunks. It is chain-complete when every dependency resolves to on-chain bytes.
  • Directory branch. With a code field present, it emits a locator with the token data in the query string, served through a gateway.

onChainStatus(token) reports which branch was taken, whether the project is chain-complete, and any unresolved dependency references. A static image cannot be produced on-chain from a program, because rendering it to a PNG needs a browser — the still is rendered off-chain by an effect. (When the work is a Solidity field renderer rather than a program, the image is computed on-chain and no rendering step exists.)

Chain-complete is a claim about where the bytes come from, not about whether they change. A Registry dependency is re-fetched from the dependency registry on every read, so its library bytes can change while chainComplete stays true — including after lockDependencies, which freezes the ref and the registry pointer but cannot reach into another contract's storage. Only a OnChain dependency, which reads an immutable SSTORE2 data contract, is frozen by being resolved. See what a project owner can do.

Reading a large document piecewise

The template branch assembles the whole document on every read, at roughly 360,000–405,000 gas per KB and climbing with size (measured) — so a project carrying a registry-hosted library is comfortably past what a default node's eth_call will serve. That is expected: these are off-chain view calls, no contract reads another contract's tokenURI, and the answer is a capable RPC rather than a smaller work.

When one call won't fit, the generator serves the same document in pieces and the client assembles it:

ReadWhat it returns
document(token, tokenId)the whole document — the one call that may not fit
tokenDataJson(token, tokenId)the canonical tokenData JSON, alone
dependencyTag(token, index)one dependency's assembled <script> tag
registryScriptChunk(registry, ref, index)one registry chunk, verbatim — the innermost loop
abxJs() · gunzipScript()the baked runtime blobs, once per chain rather than per token

abxJs and gunzipScript take no token argument, so a client caches them and never pays for them again. See on-chain storage for the measured gas figures behind the thresholds.

Traits

Traits have two homes. By default they are computed at render time: the script reports them with abx.traits(...), and the render effect captures and stores them off-chain. A project can instead put traits on-chain by setting the attributes field to a renderer representation, so a field renderer computes them. When both exist, they are unioned, and the on-chain value wins on a trait_type conflict. Configured parameters are not traits and never land here: they are read completely and directly from the contract's own enumerable store, which leaves attributes the creator's curated surface. If a parameter's value should show as a marketplace trait, report it as one — from the script with abx.traits(...), or from an attributes field renderer reading the same value on-chain.

How metadata is derived

  • animation_url: an explicit animation field wins; otherwise, if code is present and not suppressed, the resolver's live-view route; otherwise it is omitted.
  • image: an explicit image field wins; otherwise the latest bound render.image effect output; otherwise a deterministic placeholder.

Until an effect produces render.image, the token shows the deterministic placeholder. When a project renders is the creator's choice.

On this page