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.htmlentry, on IPFS, Arweave, or a URL. The on-chain surface is thecodecollection 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
codefield 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:
| Read | What 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 explicitanimationfield wins; otherwise, if code is present and not suppressed, the resolver's live-view route; otherwise it is omitted.image: an explicitimagefield wins; otherwise the latest boundrender.imageeffect 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.