Reproducibility & Deployment
From seed to on-chain artifact — seeding patterns, provenance, output formats, and web-runnable ports.
The reproducibility stack
You have a beautiful evolving artwork. You’ve picked a hero seed. Now you need to ship it. Four things must hold for the work to be defensible as an NFT:
- The seed is the truth. Same seed, same parameters, same bytes.
- The algorithm is published. Anyone with the seed must be able to regenerate the image. The code is the proof of authenticity.
- The provenance is on-chain. The token ID — typically derived from the seed — is in the contract. The seed itself can be on-chain (cheap, ~32 bytes) or off-chain in the contract metadata (more flexible, allows algorithm updates).
- The image is verifiable. A buyer can re-render with the seed and check
hash(rendered) == hash(tokenURI).
If any of those four doesn’t hold, the work is just a static PNG with extra steps.
Seeding patterns
Three useful patterns, in order of increasing complexity.
Pattern 1 — Integer seeds (the simple case)
A single 32-bit integer is the seed. PeterDeJong uses this — seed = 0x5EEED07D (a Tarbell constant from 2004). Reproducibility is trivial: pass the same integer, get the same output.
from evolving_art.library.peterdejong.reference import PeterDeJong
img = PeterDeJong(seed=0x5EEED07D).render()
The Art Blocks / fxhash pattern: take the token ID (an integer) as the seed. Token 1234 → seed 1234. The contract mint event calls setTokenURI(1234, render(1234)). Anyone can re-render by calling render(1234).
def render_token(token_id: int) -> bytes:
art = Sketch(seed=token_id)
art.step(STEPS)
return png_bytes(art.render())
The token ID is the seed, and the mint is the render. There is no separate “I picked this seed” step — the contract does it.
Pattern 2 — Mulberry32 (deterministic 32-bit RNG)
When you need an RNG that is guaranteed cross-language stable — Python, JavaScript, GLSL, Rust all produce the same sequence for the same seed — use Mulberry32 instead of NumPy’s default_rng. It’s 8 lines of code and has been the de-facto standard for generative-art seeding since 2012.
def mulberry32(seed):
"""Deterministic 32-bit RNG. Identical output in Python, JS, GLSL."""
a = int(seed) & 0xFFFFFFFF
def rand():
nonlocal a
a = (a + 0x6D2B79F5) & 0xFFFFFFFF
t = a
t = ((t ^ (t >> 15)) * (t | 1)) & 0xFFFFFFFF
t = t ^ (t + ((t ^ (t >> 7)) * (t | 61))) & 0xFFFFFFFF
return ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296.0
return rand
In JavaScript:
function mulberry32(seed) {
let a = seed >>> 0;
return function() {
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
This is what fxhash uses internally, and it’s the only seed pattern you can ship if you also want a browser-runnable port of your artwork (see “Web-runnable ports” below).
Pattern 3 — Hash-based seeds (image-derived)
When you want a seed that’s meaningful — say, derived from a collector’s wallet address, or from the hash of an existing image — use a cryptographic hash to map the input to a deterministic seed.
import hashlib
def seed_from_address(addr: str, salt: str = "") -> int:
"""Map an Ethereum address to a deterministic 31-bit seed."""
h = hashlib.sha256((addr + salt).encode()).digest()
# Take the first 4 bytes, mask to signed 32-bit
return int.from_bytes(h[:4], "big") & 0x7FFFFFFF
def seed_from_image_bytes(img_bytes: bytes) -> int:
h = hashlib.sha256(img_bytes).digest()
return int.from_bytes(h[:4], "big") & 0x7FFFFFFF
This is the pattern behind several “the collector’s wallet is the seed” mints. It has the property that two different addresses never collide on a seed (probabilistically, by the birthday paradox).
Storing the seed → asset hash mapping (provenance)
Once you’ve minted, you need to be able to prove that token N was rendered from seed N. The standard pattern is a JSONL provenance log:
{"token_id": 1, "seed": 1, "params": {"a": 2.01, "b": -2.53, "c": 1.61, "d": -0.33}, "image_sha256": "9f2c…", "render_steps": 200, "render_dim": 500, "sketch": "peterdejong", "git_commit": "a1b2c3d"}
{"token_id": 2, "seed": 2, "params": {"a": 2.20, "b": -2.40, "c": 1.10, "d": -0.50}, "image_sha256": "5e1a…", "render_steps": 200, "render_dim": 500, "sketch": "peterdejong", "git_commit": "a1b2c3d"}
The git_commit field is critical — it’s the commit hash of the code that produced the image. If you update the algorithm later (bug fix, performance improvement, palette change), the git commit changes, and the provenance record makes clear which version generated which image.
Build the log during the mint:
import hashlib, json
from pathlib import Path
def render_and_log(token_id, sketch_cls, out_dir, steps=200):
"""Render one token, hash the PNG, append to provenance.jsonl."""
img = render_token(token_id, sketch_cls, steps=steps)
png_path = Path(out_dir) / f"{token_id}.png"
png_bytes = png_path.read_bytes()
img_hash = hashlib.sha256(png_bytes).hexdigest()
record = {
"token_id": token_id,
"seed": token_id,
"params": sketch_cls.DEFAULTS,
"image_sha256": img_hash,
"render_steps": steps,
"sketch": sketch_cls.sketch,
"git_commit": subprocess.check_output(
["git", "rev-parse", "HEAD"]
).decode().strip(),
}
with open("provenance.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
return record
The provenance.jsonl file is the audit trail. Post-mint, it lets any collector verify their work: “give me the bytes of 1234.png, hash them with SHA-256, look up the record, confirm the hash matches.” If they don’t, the mint is broken.
Output formats
Different formats for different use cases.
PNG — the static proof
The simplest, most universally verifiable. One frame at one step count. The “this is what token N looks like at mint time” artifact.
import imageio.v2 as imageio
imageio.imwrite("out.png", sim.render())
Pros: Lossless, byte-stable, hashable. Every browser/viewer opens it. Cons: One frame. Doesn’t capture the evolution.
GIF — the time-lapse proof
128 frames at 50ms each = 6.4 seconds of evolution. Lets the viewer see what kind of artwork this is in motion.
import imageio.v2 as imageio
frames = []
for step in range(0, 1000, 8): # sample every 8th step
sim.step(8)
frames.append(sim.render())
imageio.mimsave("out.gif", frames, duration=0.05, loop=0)
Pros: Still universally viewable. Captures evolution. Cons: 256-colour palette, lossy. The GIF hash won’t match the PNG hash for the same step — they’re different artifacts.
MP4 — the high-fidelity evolution
For previews and marketing, an MP4 is what you want. Lossy compression, high frame rate, 10–30 seconds of footage.
ffmpeg -framerate 30 -i frames/frame_%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4
Pros: High quality, small file, accepted everywhere. Cons: Not byte-stable across codec versions — bad for hashing.
EXR / HDR — the high-dynamic-range master
For works that use HDR internal state — like KoganStyleTransferField or TykaDeepDreamField — the master output is EXR, not PNG. You tonemap to PNG for display.
import imageio.v2 as imageio
# sim.render() returns float32 HDR
imageio.imwrite("master.exr", sim.render())
Pros: Lossless, full dynamic range. Cons: Most browsers don’t render EXR. Use as a master archive, not as a deliverable.
What goes on-chain vs. off-chain
The trade-off is gas cost vs. flexibility.
| Data | On-chain cost | Flexibility | Recommended |
|---|---|---|---|
| Token ID (= seed) | ~30k gas | Low (set at mint) | ✅ On-chain |
| Algorithm hash | ~40k gas | Low (set at deploy) | ✅ On-chain |
| Rendered image | ~50k gas / KB | None | ❌ Off-chain |
| Seed + params | ~50k gas | Medium | ⚠️ Optional |
| Algorithm source | Prohibitive | High | ❌ Off-chain (IPFS) |
The canonical Art Blocks contract stores the token ID on-chain (as a uint256), the algorithm hash on-chain (commit-reveal pattern), and stores the script source on IPFS with the hash pinned in the contract. When a collector mints, they pass a seed (or the contract derives one), the contract calls into the script via a sandboxed interpreter, and the resulting image is the tokenURI.
A simpler pattern for small drops: store the seed on-chain, store the image bytes in IPFS or Arweave, store the algorithm source in IPFS with the hash on-chain. The tokenURI is the image. Re-rendering is a manual step the collector runs.
The 4-line mint check that catches most deployment bugs:
def verify_mint(token_id: int, contract, sketch_cls) -> bool:
"""Pull seed from chain, render locally, compare hashes."""
seed = contract.tokenIdToSeed(token_id)
img = render_token(seed, sketch_cls)
on_chain_hash = contract.tokenURI(token_id) # ipfs://…/image.png
local_hash = hashlib.sha256(img).hexdigest()
return fetch(on_chain_hash).hash == local_hash
Web-runnable ports
The final deployment question: can the collector run your artwork in their browser? If yes, the artwork is alive. If no, it’s a static asset.
Three viable targets.
p5.js port — best for algorithmic / sketch-style works
For 2D sketch-style works like lindenmayer, perlinStreamlines, CherniakSquiggles, p5.js is the easiest port. Most of the work translates line-for-line.
let axiom = "X";
const rules = { "X": "F+[[X]-X]-F[-FX]+X", "F": "FF" };
const angle = 22.5 * Math.PI / 180;
const iterations = 5;
const dim = 600;
function setup() {
createCanvas(dim, dim);
let s = axiom;
for (let i = 0; i < iterations; i++) {
s = [...s].map(c => rules[c] || c).join("");
}
let x = dim / 2, y = dim - 10, h = -Math.PI / 2;
const step = 6.0 * Math.pow(0.72, iterations);
const stack = [];
background(252, 248, 240);
stroke(30, 55, 40, 220);
for (const c of s) {
if (c === "F") {
const nx = x + step * Math.cos(h), ny = y + step * Math.sin(h);
line(x, y, nx, ny); x = nx; y = ny;
} else if (c === "+") h += angle;
else if (c === "-") h -= angle;
else if (c === "[") stack.push([x, y, h]);
else if (c === "]") [x, y, h] = stack.pop();
}
}
Direct translation, no architectural changes. Mulberry32 (above) for the seed; the token ID is the seed.
WebGL / fragment shader port — best for noise / field works
For perlinStreamlines, WorleyNoise, FBMNoise, ShaneOrganicNoise, IQDomainRepetition — anything that’s “sample a field at every pixel” — a fragment shader is the right target. The browser does the rendering in parallel on the GPU.
precision highp float;
uniform float u_seed;
uniform vec2 u_resolution;
varying vec2 v_uv;
// Mulberry32 in GLSL
float rand(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
void main() {
vec2 p = v_uv * 8.0 + vec2(u_seed);
float n = 0.0, amp = 0.5;
for (int i = 0; i < 5; i++) {
n += amp * (rand(floor(p)) - 0.5);
p *= 2.0;
amp *= 0.5;
}
gl_FragColor = vec4(vec3(n + 0.5), 1.0);
}
Pair this with a Three.js or regl scaffold that reads the seed from a URL parameter (?seed=1234), and you have a browser-runnable mint where the collector can re-render their own token just by changing the URL.
WebGPU compute port — best for particle systems and ML works
For BoidsFlock, MagnetoParticles, computeParticleSystem, latentWalk — works with thousands of agents or model inference — WebGPU compute shaders are the modern target. Same parallel-execution model as CUDA, available in all evergreen browsers since 2024.
// WebGPU compute pipeline for a particle system
const shader = `
@group(0) @binding(0) var<storage, read_write> positions: array<vec2f>;
@group(0) @binding(1) var<uniform> seed: f32;
@compute @workgroup_size(64) fn update(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
let r = hash(seed + f32(i));
positions[i] += vec2f(cos(r * 6.28), sin(r * 6.28)) * 0.01;
}
`;
The WebGPU port is more work than a p5.js port but renders particle works at 60fps with 50,000+ agents — orders of magnitude faster than the equivalent JavaScript.
Putting it all together
The canonical mint pipeline, end-to-end:
import hashlib, json, subprocess
from pathlib import Path
def mint_one(token_id, sketch_cls, steps, out_dir):
"""The full mint: seed, render, hash, log."""
Path(out_dir).mkdir(exist_ok=True)
sim = sketch_cls(seed=token_id)
sim.step(steps)
img = sim.render()
png_path = Path(out_dir) / f"{token_id}.png"
import imageio.v2 as imageio
imageio.imwrite(png_path, img)
img_hash = hashlib.sha256(png_path.read_bytes()).hexdigest()
record = {
"token_id": token_id,
"seed": token_id,
"params": sim.params(),
"image_sha256": img_hash,
"render_steps": steps,
"sketch": sketch_cls.sketch,
"git_commit": subprocess.check_output(
["git", "rev-parse", "HEAD"]
).decode().strip(),
}
with open("provenance.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
return record
def mint_collection(sketch_cls, n_tokens, steps, out_dir):
"""Mint an entire drop."""
Path("provenance.jsonl").unlink(missing_ok=True)
for tid in range(n_tokens):
mint_one(tid, sketch_cls, steps, out_dir)
That script, plus an Art Blocks–style Solidity contract, plus the algorithm source pinned to IPFS, plus a WebGPU/p5.js port of the algorithm — that’s the full deployment. Everything else is variation on the theme.
Where to go next
You now have the four-chapter pipeline. Browse the library for the 153 works you can study, modify, and mint. Each one exposes the same contract you built in Chapter 1, parameter spaces as in Chapter 2, has hero contact sheets produced via Chapter 3’s sweep pattern, and ships with the seed/provenance story from Chapter 4.
The full corpus is in this repository — start with one work you like, run it, sweep it, mint it. The fastest path to a sellable NFT is to ship one you genuinely find beautiful.