abx.
Protocol

Field renderers

A field renderer is a contract that computes one metadata field's value from on-chain state, where a stored field just holds bytes. It is how a token's image, attributes, or any field can be produced entirely on-chain — a generative SVG derived from the token's seed and parameters, with no JavaScript, browser, bucket, resolver, or effects runner. Once the renderer is deployed and wired, the token's tokenURI — name, image, traits — resolves from the chain forever.

A field opts in by setting its metadata representation to renderer, with the value abi.encode(address). The metadata renderer staticcalls the field renderer and embeds the result — an image/svg+xml document becomes data:image/svg+xml;base64,…; an application/json array becomes the attributes. This is the zero-infrastructure lane for generative art; a small Solidity SVG is a good fit for an on-chain tokenURI, where a large JavaScript bundle is not.

The interface

interface IAbxFieldRenderer {
    /// @param token   the ABX collection contract (read its params via IAbxParams)
    /// @param tokenId the token, or type(uint256).max for the collection surface (contractURI)
    /// @param field   which field to compute (e.g. "image", "attributes")
    function render(address token, uint256 tokenId, bytes32 field)
        external
        view
        returns (string memory contentType, bytes memory data);
}

Determinism is scoped to chain state: same state → same bytes. A renderer is stateless and view-only, and is never confined to canonical deployments — any contract satisfying the interface, per field, per project.

The five invariants

The metadata renderer staticcalls render with no try/catch, so a reverting or malformed renderer bricks the entire tokenURI. A renderer must:

  1. Never revert for any (token, tokenId, field) — including the collection surface tokenId == type(uint256).max (used by contractURI). Return a neutral value instead. Missing state (no seed yet, an unset param) must fall back to a default, never throw.
  2. Return the correct contentTypeimage/svg+xml for an image, application/json for an attributes array.
  3. Guard the field — revert only a genuinely unsupported field (a deploy-time miswiring), while never reverting on the field it does render.
  4. Be view and deterministic — same chain state in, same bytes out.
  5. Stay bounded — keep output small (a few hundred bytes of SVG) so tokenURI remains a cheap single eth_call.

Reading parameters

A renderer reads live state through IAbxParams: tokenParam(tokenId, key) and contractParam(key), each returning (bytes32 value, bool valueIsHash, bool isSet). The tokenData merge rule applies — a token-scope value overrides the contract-scope one — so the common pattern is a token read with a contract-scope fallback. Reserved keys include seed; collector-set parameters (declared with a schema) are read by their key.

Two readers: pick by type

Those two functions only carry types that fit in a bytes32. The two payload types, Bytes and String, do not: their bytes32 is a keccak commitment (valueIsHash == true), and the content comes from a second pair of readers:

function tokenParamData(uint256 tokenId, bytes32 key) external view returns (bytes memory);
function contractParamData(bytes32 key) external view returns (bytes memory);

These are the two types that can carry a real payload (~24KB per key), so a renderer driven by an on-chain artwork blob wants tokenParamData, not tokenParam. An unset key returns empty bytes — the "use a default" signal — and when non-empty, keccak256(returned bytes) equals the value the scalar reader reports, so the commitment is verifiable in-contract. Reading a Bytes param through tokenParam yields a hash and renders garbage without failing anywhere, which is the mistake worth knowing about up front. All four functions are declared in the scaffold's src/interfaces/IAbxParams.sol, whose tests include a worked payload read.

A renderer that has to handle any key, rather than the ones it was written to name, can enumerate instead: tokenParamKeys(tokenId) and contractParamKeys() return the set keys per scope, paramSchemaKeys() the declared ones. That is how the canonical generator assembles token data with no maintained key list anywhere.

Example: seed + a collector PostParam → SVG

The abx scaffold-renderer command writes a ready-to-build Foundry project with a worked example: an image renderer whose geometry is derived from the token's seed and whose tint is a palette (HexColor) PostParam a collector can set, plus a matching attributes renderer that reads the same seed math so image and traits agree by construction. The same pattern ships as a forkable reference pair in the contracts package too — SeedSvgRenderer + SeedTraitsRenderer (contracts/src/renderers/examples/) — with a wider technique set: weighted-rarity trait buckets, a neutral collection-surface card, and constants kept in lockstep across both contracts on purpose, so the traits array can never drift from what the image actually draws.

function render(address token, uint256 tokenId, bytes32 field)
    external view returns (string memory, bytes memory)
{
    if (field != "image") revert UnsupportedField();
    if (tokenId == type(uint256).max) return ("image/svg+xml", bytes(_svg(keccak256("collection"), "#0e1a40")));

    bytes32 seed = _seed(token, tokenId);        // the token's seed, or a deterministic fallback
    string memory palette = _palette(token, tokenId); // "#rrggbb" from the palette PostParam, or a default
    return ("image/svg+xml", bytes(_svg(seed, palette)));
}

A collector who configures their token's palette to #ff3366 — typically through a project's own configuration site, which writes the PostParam on their behalf — re-tints the on-chain image instantly. The renderer reads the live parameter on the next tokenURI read; nothing is re-rendered or re-uploaded.

Example: one contract, many fields

A renderer dispatches on field, so one contract can compute several fields for the same project — wire the same address to image, attributes, animation_url, and description, and it serves all four:

function render(address token, uint256 tokenId, bytes32 field)
    external view returns (string memory contentType, bytes memory data)
{
    if (field == "image") { /* ... */ }       // the still: image/svg+xml
    if (field == "attributes") { /* ... */ }  // the traits: application/json
    if (field == "animation_url") { /* ... */ } // a live-view document, built with TokenDataLib
    if (field == "description") {
        // the collection surface reaches every field, not just image and attributes
        return ("text/plain", tokenId == type(uint256).max ? bytes("collection-computed") : bytes("token-computed"));
    }
    revert UnsupportedField(); // only for a field this renderer was never wired to (invariant #3)
}

TokenDataLib is the Solidity twin of the off-chain tokenData serializer: begin/finish open and close the object, scalarEntry appends a canonically-decoded parameter, decodeTagLoose applies the shared schema-less literal rule so an unfamiliar key decodes the same way on both surfaces, and augmentedEntries folds in read-time hook values last. An animation_url renderer can use it to emit a small on-chain HTML document that injects window.abxTokenData exactly like a code project does — full on-chain custody, without being one.

Building and wiring one

ABX does not compile or deploy Solidity. Scaffold, build, test, and deploy with Foundry, then hand the deployed address to the CLI, which verifies it has code:

abx scaffold-renderer ./my-renderer     # a buildable Foundry project (interfaces, examples, tests, deploy)
cd my-renderer && forge soldeer install && forge test
forge script script/Deploy.s.sol --rpc-url <rpc> --private-key <key> --broadcast

abx deploy-code \
  --image-renderer <MyRenderer address> \
  --attributes-renderer <MyTraits address> \
  --onchain-uri --schema palette:HexColor:TokenOwner --name "..." --symbol ...

forge test the never-revert cases (including the collection surface) before wiring — the scaffold ships a test that does exactly this, plus a fuzz over every seed and token id.

  • Metadata: the field-and-representation model the renderer representation plugs into.
  • On-chain storage: the reader representation — storing bytes instead of computing them.
  • Parameters: the seed and collector-set PostParams a renderer reads.
  • Code projects: the JavaScript lanes, for when the artwork is a program, not a contract.

On this page