@artblocks/abx-cli 0.1.0-alpha.32 → 0.1.0-alpha.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/capabilities.d.ts +94 -0
  3. package/dist/capabilities.d.ts.map +1 -0
  4. package/dist/capabilities.js +135 -0
  5. package/dist/capabilities.js.map +1 -0
  6. package/dist/commands/deploy.d.ts.map +1 -1
  7. package/dist/commands/deploy.js +13 -17
  8. package/dist/commands/deploy.js.map +1 -1
  9. package/dist/commands/feedback.d.ts +7 -0
  10. package/dist/commands/feedback.d.ts.map +1 -0
  11. package/dist/commands/feedback.js +147 -0
  12. package/dist/commands/feedback.js.map +1 -0
  13. package/dist/commands/reads.js +1 -1
  14. package/dist/commands/reads.js.map +1 -1
  15. package/dist/commands/scaffold.d.ts +9 -1
  16. package/dist/commands/scaffold.d.ts.map +1 -1
  17. package/dist/commands/scaffold.js +60 -13
  18. package/dist/commands/scaffold.js.map +1 -1
  19. package/dist/commands/service.d.ts.map +1 -1
  20. package/dist/commands/service.js +5 -4
  21. package/dist/commands/service.js.map +1 -1
  22. package/dist/flag-allowlists.d.ts.map +1 -1
  23. package/dist/flag-allowlists.js +19 -0
  24. package/dist/flag-allowlists.js.map +1 -1
  25. package/dist/main.js +45 -11
  26. package/dist/main.js.map +1 -1
  27. package/dist/ownerops.d.ts +3 -3
  28. package/dist/ownerops.js +5 -5
  29. package/dist/ownerops.js.map +1 -1
  30. package/dist/remote.d.ts +5 -1
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +31 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/scaffold.js +1 -1
  35. package/dist/scaffold.js.map +1 -1
  36. package/dist/update-check.d.ts +6 -1
  37. package/dist/update-check.d.ts.map +1 -1
  38. package/dist/update-check.js +40 -17
  39. package/dist/update-check.js.map +1 -1
  40. package/package.json +6 -6
  41. package/skill/SKILL.md +174 -526
  42. package/skill/agents/openai.yaml +4 -0
  43. package/skill/reference/capabilities.md +171 -285
  44. package/skill/reference/code.md +210 -0
  45. package/skill/reference/creator-token.md +90 -95
  46. package/skill/reference/deploy.md +167 -0
  47. package/skill/reference/diagnose.md +165 -0
  48. package/skill/reference/hosting.md +148 -126
  49. package/skill/reference/operate.md +181 -0
  50. package/skill/reference/services.md +76 -0
  51. package/skill/reference/setup.md +108 -62
  52. package/skill/reference/code-projects.md +0 -368
  53. package/skill/reference/decisions.md +0 -182
  54. package/skill/reference/operating.md +0 -220
  55. package/skill/reference/troubleshooting.md +0 -65
@@ -1,368 +0,0 @@
1
- # Code projects — operate, resolve, render, sell
2
-
3
- [← back to SKILL.md](../SKILL.md#code-projects-generative--code-based-drops)
4
-
5
- The [SKILL Code projects](../SKILL.md#code-projects-generative--code-based-drops) section is the decision tree — inspect, pick a lane. This file is the operating depth: what to keep running, how to verify it resolves, render ops, and the lane internals. A **program is the content** (`abx deploy-code` → a `SeriesCode`): its output is a function of live on-chain state (`tokenData`: coordinates + `seed` + PostParams), injected at view time. Everything from a [Series](../SKILL.md#series-multi-token-drops) applies (mint order, lanes, identity, supply cap, minter, pause).
6
-
7
- ## Authoring the program — the abx.js runtime contract (get this right FIRST)
8
-
9
- When a creator arrives with an *idea* and you write the program, it must read its inputs and report its outputs through **one specific contract**. Guess the shape and it deploys + renders without error but is **silently broken**: the mint seed never arrives, every token renders identically, and traits come back empty. Do **not** invent globals (`window.tokenData`, `window.tokenTraits`, a bare `tokenData`) or "read defensively across variants" — there is exactly one contract, `abx inspect` recognizes only it, and you never need to read abx source to learn it (it's here):
10
-
11
- - **Read state via `abx.tokenData`** — a **flat** object. The runtime companion `abx.js` resolves it (from the injected global → URL param → RPC); template mode (`--script`) inlines `abx.js` for you, directory mode (`--code-dir`) ships its own `abx.js` copy in the build. Then:
12
- - `abx.tokenData.seed` — the mint-time seed (hex string; seed your PRNG from it — this is what makes each token unique).
13
- - `abx.tokenData.tokenId` / `.chainId` / `.contractAddress` — reserved coordinates.
14
- - `abx.tokenData.<key>` — each PostParam you declared with `--schema`, **flat** and decoded to its canonical string (e.g. `abx.tokenData.palette`). **Not** nested under `.params`.
15
- - Canonical guard: `var td = (window.abx && abx.tokenData) || {};` then `td.seed`, `td.palette`. (Reading the raw injected global `window.abxTokenData` works without abx.js, but prefer `abx.tokenData`.)
16
- - **Report traits with `abx.traits({ Key: value, … })`** (a flat object, called during render). This is the **only** thing captured into `attributes` — computing traits internally or writing them to a global does nothing. Required for the **resolver lane too**, not just on-chain (a resolver stitches `abx.traits(...)` output into the metadata; on-chain traits *additionally* need a deployed `--attributes-renderer`). No `abx.traits()` call ⇒ **no marketplace traits on any lane** — if `abx inspect` says `no traits reported`, believe it and fix the script, don't assume the resolver derives them.
17
- - **Signal `abx.done()`** when the frame is final, so the renderer captures a stable still.
18
-
19
- **A complete minimal sketch (vanilla JS, no deps) — copy this shape:**
20
-
21
- ```js
22
- // Reads state via abx.tokenData; reports traits via abx.traits(); signals abx.done().
23
- (function () {
24
- var td = (window.abx && abx.tokenData) || {}; // the flat token-data object
25
- var seed = td.seed || '0x1'; // mint-time seed (hex)
26
- var palette = td.palette || '#3355ff'; // a PostParam: --schema palette:HexColor:TokenOwner
27
-
28
- var z = 0; // seed a PRNG deterministically (same seed → same output)
29
- for (var i = 2; i < seed.length; i++) z = (z * 16 + (parseInt(seed[i], 16) || 0)) % 4294967296;
30
- function rnd() { z = (1664525 * z + 1013904223) % 4294967296; return z / 4294967296; }
31
-
32
- var rings = 3 + Math.floor(rnd() * 6); // a seed-derived value → drawn AND reported as a trait
33
- var c = document.createElement('canvas'); c.width = c.height = 1000; document.body.appendChild(c);
34
- var g = c.getContext('2d'); g.strokeStyle = palette; g.lineWidth = 6;
35
- for (var r = 0; r < rings; r++) { g.beginPath(); g.arc(500, 500, 55 * (r + 1), 0, 6.283); g.stroke(); }
36
-
37
- if (window.abx) {
38
- abx.traits({ Rings: rings, Palette: palette === '#3355ff' ? 'Default' : 'Custom' }); // ONLY these reach `attributes`
39
- abx.done(); // frame is final → capture the still
40
- }
41
- })();
42
- ```
43
-
44
- (A p5.js variant: `var td = (window.abx && abx.tokenData) || {}; randomSeed(seedInt(td.seed)); … abx.traits({…}); abx.done();` inside `draw()`. Declare `p5` with `--dep p5@1.0.0` — on-chain bytes exist on Sepolia only.)
45
-
46
- **`abx inspect <script>` is your author-time check** — iterate the script against it before picking a lane: its **PostParams** list must show every collector key you intend (if it says "none detected" but you meant `palette` to be collector-set, you're reading it the wrong way), and its **Traits** line must not say "no traits reported" if you want filterable traits. (A Solidity in-chain renderer is a *different* contract — see [In-chain Solidity SVG](#in-chain-solidity-svg--the-zero-dependency-lane); the `abx.js` contract above is for a JS `--script`/`--code-dir` program.)
47
-
48
- ## Time-based + audio projects (sound, music, generative composition)
49
-
50
- The protocol supports these: `animation_url` is an HTML document, so Web Audio works, and `abx attach`
51
- handles `.wav`/`.mp3`/`.mid` as artifacts. Five judgments the visual lanes don't need:
52
-
53
- - **Autoplay is blocked, and a marketplace iframe cannot ask.** No browser starts audio without a user
54
- gesture, and the piece will be embedded in someone else's page. Author it to render *silent and
55
- correct*, then start sound on first interaction (a click/keypress handler, or an in-piece play
56
- affordance). A piece that only makes sense with sound running is a piece most viewers see mute.
57
- - **The thumbnail is a real design decision, not a screenshot.** `image` is what every marketplace
58
- grid, wallet, and social embed shows. Decide with the creator what the still *is* — a score, a
59
- waveform, a spectrogram, a generative visual driven by the same seed — and draw it on a canvas so
60
- the render effect can capture it. "It's audio, so there's no image" ships an empty grid tile.
61
- - **`abx.done()` is the capture point, not the end of the piece.** For a duration-based work, call it
62
- once the *visual* has settled (the still is what's being captured), not when playback finishes —
63
- otherwise every capture waits out the full piece and `--shoot`/the render effect time out. A long
64
- piece with a fast-settling visual is the normal, correct shape.
65
- - **Audio libraries follow the same dependency rule as visual ones.** `Tone` is detected by
66
- `abx inspect`; declaring it on-chain (`--dep tone@<version>`) needs a dependency registry entry,
67
- which means **Sepolia, not Base Sepolia** — same constraint as `p5`. Hand-rolled Web Audio (no
68
- library) has no such constraint and goes fully on-chain on either chain.
69
- - **There is no `render/audio` output declaration.** The render effect produces the *still*; audio
70
- lives inside the document (or as an attached artifact), never as a second rendered output. Don't
71
- invent an output kind — see [the artifacts/attach lane](operating.md) for shipping the source audio
72
- alongside the piece.
73
-
74
- Everything else — seeds, traits, PostParams, the studio loop — is identical to a visual project.
75
- `--shoot`'s per-seed traits table still works: encode musical invariants (key, tempo, section count)
76
- as traits and it becomes your property check.
77
-
78
- ## Studio loop — iterate on the work before you deploy anything
79
-
80
- [← Phase 0 in SKILL.md](../SKILL.md#phase-0--make-the-work-first-skip-every-gate-below-until-its-good). When the creator is still designing, your job is to make the work **visible, interactive, and fast to change**. One command does it:
81
-
82
- ```bash
83
- abx preview --script art.js --schema "palette:HexColor:TokenOwner" # → http://localhost:8788
84
- ```
85
-
86
- **Give the creator the URL and let them drive.** This is the one place in the toolkit where handing over a link is right — the work is theirs to judge, and a browser they control is the only honest way to judge it. The studio gives them a seed shuffle, real inputs for every PostParam they declared, a live traits readout, and `/grid` for N seeds at once. `/view` is the bare document.
87
-
88
- **Why a server and not a screenshot sweep:** a still flattens every time-based piece. Plenty of generative work animates, and `abx.done()` exists *because* stills need a settle point — so a proof sheet of an animated piece is a set of arbitrary frozen frames presented as the work. The server also makes PostParams tangible (a color picker that re-renders beats any explanation of governed params), and it costs no Chromium download.
89
-
90
- **It serves the same document the generator serves** — the real `abx.js`, the real canonical tokenData shape, the real dependency tags — with a synthetic seed in place of a minted one. So what they approve is what deploys. (This is why you should not hand-roll a preview page: a stub you write yourself defines its own `abx` surface, and will happily run a sketch that reads its seed the wrong way.)
91
-
92
- **The program is re-read from disk on every render**, so the loop is: edit `art.js` → tell them to refresh → take feedback → edit again. No restart, no watcher, no rebuild.
93
-
94
- **When you need to see it yourself** — you have no browser, and "how does it look?" every round is a bad experience for them:
95
-
96
- ```bash
97
- abx preview --script art.js --shoot ./frames --count 9 # PNGs + traits.json, then exits
98
- ```
99
-
100
- Same server, same document, headless. Needs Playwright + Chromium (`npm i -D playwright && npx playwright install chromium`); the interactive lane needs neither. **Read the PNGs** — don't report on work you haven't looked at. `--shoot` also flags the two silent killers for you: no frame reporting traits (⇒ no marketplace `attributes` on any lane), and identical traits across every seed (⇒ the sketch isn't reading `abx.tokenData.seed`, so the drop mints N identical tokens).
101
-
102
- Use both: `--shoot` to check your own work between rounds, the live URL as what the creator actually looks at.
103
-
104
- **What preview is NOT.** It injects the token data itself, so it will run a sketch that reads its seed the wrong way — and a piece that only ever renders one seed correctly still looks fine here. Neither check that follows is optional:
105
-
106
- ```bash
107
- abx inspect art.js # the wiring check: are traits + PostParams actually read/reported?
108
- abx deploy-code --script art.js --onchain-uri --dry-run # the lane + surfaces check
109
- ```
110
-
111
- And a **testnet deploy remains the faithful end-to-end** (the real generator, the real assembled document, the real seed from the chain). Both come after the work is settled.
112
-
113
- ## What a code project requires you to run — and keep running (say this up front)
114
-
115
- A code project's output depends on live on-chain state (the per-token `seed`, mutable PostParams), and *something* must read that state and inject it at view time. That something is a **resolver you run** — **unless** you take the fully-on-chain lanes (`--onchain-uri` for the tokenURI+animation, `--image-base` for a deterministic off-chain thumbnail, `--attributes-renderer` for on-chain traits), which can eliminate the metadata resolver entirely. When a resolver *is* in play, it's three pieces of ongoing infrastructure — lay them out plainly before they commit:
116
-
117
- 1. **A hosted resolver (required unless the deploy takes the `--onchain-uri` lane).** Serves `tokenURI` (`/t`) and the **live view** (`/a`, where it injects the seed + current PostParams). Its **public URL is baked on-chain at deploy**, so a domain/host must be ready first — `deploy-code` **refuses a localhost/missing URL**. Stand it up with `abx deploy-resolver` ([hosting.md](hosting.md)); keep it up or the token stops resolving. **The resolver is also the protocol's one chain-watcher**: `abx serve` runs an incremental getLogs poll (~12s, `ABX_WATCH_INTERVAL_MS`, 0=off) over every registered project, so **any** on-chain change — an external mint through a minter, a param change from a foreign tool or another wallet — auto-indexes and fans out ONE coarse notification per changed project to the effects layer (`ABX_EFFECTS_URL` + `ABX_EFFECTS_TOKEN`). Effects never watch the chain themselves. (No reorg lookback — post-PoS reorgs are rare; the repair is the deterministic `abx index <addr> --full`.)
118
- 2. **An effect runner + a storage home — decide the thumbnail mode AND where renders live, up front.** A code project's marketplace still is **rendered off-chain** (there's no image file to point at); on-chain-stitched `attributes` come from the same render. Two coupled decisions to make *before* deploying: **(i) the mode** — **(a) continuous runner** that auto-renders every new mint + param change (the default for a live / for-sale drop): `abx deploy-effects --resolver-url <resolver>` HOSTS it (fly/docker) beside a hosted resolver, or `abx effects` runs it **LOCALLY, in-process** (co-located with `abx serve` — great for testing/iteration; blocks, so background it), **(b) one-shot** (`abx render <addr> --remote <resolver>` after mint, for a fixed supply — re-run for later mints / param changes), or **(c) none** (the live view still animates, but the marketplace thumbnail stays a placeholder SVG); and **(ii) the storage home** (`ABX_STORAGE_BACKEND`) — **ipfs / arweave** (durable; the runner publishes a locator the resolver redirects to) or **s3**, **NOT** the default `fs` for a hosted resolver (a laptop-local store a hosted node can't read → the placeholder never clears). The runner uploads each render to that home and **publishes** it to the resolver (the locator bridge). Verify with `abx verify <addr>` after minting token 0 (below).
119
- 3. **Pinned IPFS/Arweave behind an HTML-serving gateway (directory mode).** The build is pinned and the live view 302s to the gateway, so the gateway **must serve HTML** — the shared Pinata *public* gateway does **not** (`ERR_ID:00023`); use a **dedicated** gateway or Arweave.
120
-
121
- ### Already deployed — just operate it (the resume loop)
122
-
123
- A creator who ALREADY has a code contract (deployed here or elsewhere) doesn't re-run `deploy-code`:
124
- ```bash
125
- abx add <addr> # index it locally (auto-discovers the deploy block; --from-block to override)
126
- abx effects # START THE RUNNER FIRST — in-process, blocks, so background it (& or a second shell)
127
- ABX_EFFECTS_URL=http://localhost:<effectsPort> abx serve # resolver + chain watcher; wire it to the runner
128
- abx verify <addr> --remote http://localhost:<port> # confirm: renders up-to-date AND the watcher is live
129
- ```
130
- - **Order matters.** Start `abx effects` (or set `ABX_EFFECTS_URL` to a running runner) **before** relying on auto-render or `abx render`. A resolver whose `ABX_EFFECTS_URL` is unset still watches + indexes, but has **nowhere to send the "changed" notification → thumbnails never auto-update** (`abx serve` prints a ⚠ when it's unset). And `abx render` pointed at a dead effects URL errors instead of rendering — start the runner, or drop the URL to render inline.
131
- - **`--remote <url>` means "any resolver's HTTP API" — including your OWN local `abx serve`**, not just a cloud host. `abx verify <addr> --remote http://localhost:<port>` is the truthful check even on your laptop (plain `abx verify` only sees this machine's store, so a published render reads as a false placeholder).
132
- - **Proof the watcher is alive:** `abx serve` logs `[watch] alive — watching sepolia @ block …` on quiet stretches (~every 2 min) and `[watch] … changed → notified` on every on-chain delta. For a HOSTED node you can't tail, `GET /api/watch` (and the `watching …` line in `abx verify --remote`) reports the last poll + head — a stale `pollAt` means the watcher stopped.
133
-
134
- ### Live data (the augment hook) — the hook IS the setting
135
-
136
- Two kinds of inputs feed a piece: **settled state** (explicit PostParams, the seed — event-derived, indexed) and optional **live data** (an on-chain augment hook read fresh per view: block data, an oracle, anything a `view` returns). No hook — the overwhelming default — means the resolver makes **zero** live reads and serves pure indexed params: nothing to configure, maximum efficiency. With a hook set, the **live view** reads it per view (always current), while the **still is a snapshot of settled state only** — live data never re-addresses the render, so a volatile hook (a timestamp) animates the live view without re-rendering the thumbnail every block. Re-render triggers are settled-state changes only. `abx verify` prints the project's live-data posture; `ABX_DISABLE_AUGMENT=1` is a resolver-operator kill-switch (degrades to settled params).
137
-
138
- **Wiring the hooks — `abx set-param-hooks <addr>` (SeriesCode/EditionCode only, owner-only).** A code project has three optional param-lifecycle hook addresses, each a contract the creator deploys: **`--augment`** (the live-data hook above — read-time derivation folded into tokenData), **`--configure`** (a write-time veto/validator — a governed `configure-param` reverts if this hook reverts), and **`--transfer`** (an ownership-change call that is **also a veto**: see below). The contract has **no per-hook setter** — it writes all three at once — so the command reads the current trio and re-sends it with your change applied: **omit a role to keep it**, pass an address to set it, `none` to clear it (`--clear` clears all three). Run it bare to print the current hooks. Any signing lane; guards `--dry-run`. A 1/1 or plain Series has no configurable params, so it has no hooks (the command refuses it).
139
-
140
- **The transfer hook is a VETO — tell the creator before they arm one, and tell a buyer it exists.** The token calls it plainly, so **its revert fails the transfer**, and because a mint is a transfer from `0x0` a reverting hook also **stops minting** for that project, including through the shared minter (the same is true of a burn — `to == 0x0` — on a `--burnable` collection, which is exactly the seam burn-to-combine and redemption settle on). This is not a bug to design around: it's how a piece can react to ownership at all, and the hook is always the creator's own contract. (It differs from the 721C/1155C **transfer validator**, which never sees mint/burn precisely so a third party's policy contract cannot brick issuance.) An earlier version of the protocol swallowed the hook's revert and promised the lifecycle could never block a transfer; that promise was withdrawn rather than restated, because the receiver acceptance check runs *after* the hook, so on `safeTransferFrom` — what a marketplace fill wraps — a hook cheap at gas-estimation time and expensive at execution starved it regardless of any cap.
141
-
142
- **Writing a hook? The interface, and the one trap on an edition.** `IAbxTransferHook.onTokenTransfer(uint256 tokenId, address from, address to, address operator, uint256 amount)` — `operator` is whoever initiated the move (the holder, an approved operator, or a minter; on ERC-1155 an approved marketplace moves a holder's copies, so it is NOT redundant with `from`), and `amount` is copies moved (always 1 on ERC-721). The token refuses to call the hook at all on a **zero-amount** entry or a **self-transfer** — that closes a spoof an independent audit reproduced, where a stranger holding no copy could fire the lifecycle for any id via `safeTransferFrom(from, to, id, 0, "")`. **Think hard before a hook stores per-id state on a multi-copy edition:** params are per id and therefore SHARED, so a hook writing "the current owner" is really writing "whoever moved most recently" and it changes the work for every co-holder. Aggregate or monotonic state (transfer counts, "has ever been held by") is coherent there; a single-owner notion is not, unless the edition size is 1.
143
-
144
- **`abx lock-param-hooks <addr>` freezes all three addresses forever** (owner-only, one-way, `--dry-run`-guarded; `set-param-hooks` reverts `ParamHooksLocked` after it). It prints the exact trio it will freeze before sending. Two uses, both about what a buyer can verify rather than trust:
145
- - **No hooks set → freezing PROVES the project can never arm a transfer veto.** The strongest thing a code project can say about its own transferability. Offer it before a sale, not after.
146
- - **Hooks set → freezing pins WHICH contracts can ever run.** A hook already wired keeps its veto — freezing the set is not disarming what's in it. Say that to the creator so they don't hear "safe now".
147
- - **It freezes ADDRESSES, not behavior.** A hook is a contract, and a contract can be a proxy: a locked proxy hook can be upgraded later — to revert on every transfer, say — while `paramHooksLocked()` still reads `true`. The protocol defines locks as pointer locks and discloses it rather than attempting on-chain proxy detection. So when a creator wants the permanence claim, the hook has to be an **immutable** deployment, and the honest line to a buyer is *"these exact three addresses can never change"* — plus, if the trio is non-empty, check what's behind them.
148
-
149
- `abx state <addr>` prints the three hooks and whether they're frozen (`ParamHooksFrozen` is the on-chain proof); `abx verify` flags an armed-but-unfrozen transfer hook. Background: https://abx.docs.artblocks.io/protocol/owner-powers/
150
-
151
- **There is no release valve, and don't imply one: `setParamHooks` is owner-only forever.** No permissionless path exists in any state, including a renounced (`owner() == 0x0`) project, and a freeze holds against everyone. Do NOT carry the 721C/1155C validator's dead-man release over to hooks — a **validator** is usually a third party's purely-restrictive policy contract, so disarming it can only ever permit more, while a **hook** is the creator's own contract and is often the work itself. So the honest line for a creator planning to renounce: *"renounce with a live transfer hook that reverts and every collector's token, plus all remaining issuance, is frozen permanently — nobody can undo it, including us."* Not a reason to avoid hooks; a reason to `lock-param-hooks` deliberately and to test a hook before arming it. `paramHooks()` / `paramHooksLocked()` (both in `abx state`) are what a buyer reads.
152
-
153
- ## `--onchain-uri` — the chain-complete lane (internals)
154
-
155
- `deploy-code --onchain-uri` makes `tokenURI` resolve **on-chain** via the canonical metadata renderer, with `animation_url` **computed on-chain** by the canonical **`AbxGenerator`** (a `renderer`-representation collection field). No `--public-base-url`, no resolver base baked.
156
-
157
- **When to choose it (owner guidance from real-world experience): NOT the default for a generative drop meant to sell — lean off-chain resolver there.** A resolver keeps you maneuverable (metadata/serving can evolve without on-chain re-points) and lets marketplaces fetch a **small** `tokenURI`; a fully-on-chain code `tokenURI` carries the whole ~200KB+ document per call, and large-`tokenURI` marketplace/indexer compatibility is a real-world risk that grows with the size of the work. Reach for `--onchain-uri` **deliberately** when maximal durability / "resolves from any RPC forever" / zero always-on infra outweighs those — a legitimate, proven lane, just not the marketplace default.
158
-
159
- **What "fully on-chain" (chain-complete) means, and the one silent trap:** the `tokenURI` **and** its `animation_url` document come back entirely from on-chain bytes — no server, gateway, or CDN in the graph. It says nothing about **immutability** (a registry dep's bytes can still change — see freezing, below) and it does **NOT** include the marketplace thumbnail (`image`) — that is *always* rendered off-chain. ⚠ **The silent breaker:** a `--dep` that resolves to a CDN instead of on-chain bytes **deploys fine and renders fine**, but you are no longer fully on-chain — a URL is back in the graph, with no error. The deploy's dependency report ("ON-CHAIN bytes available" vs "served from CDN") and `abx verify` (`chain-complete: yes/no`) both call this out **before and after** you spend — read them.
160
-
161
- - **Template mode (`--script`) can be CHAIN-COMPLETE** — the generator assembles the full HTML document (`data:text/html;base64`) from the on-chain chunks — **iff every `--dep` resolves to proven on-chain bytes** on the registry (`p5@1.0.0` qualifies on Sepolia). A CDN-served dep still *serves fine* but breaks chain-completeness. Zero-dep vanilla JS is trivially chain-complete.
162
- - **Directory mode (`--code-dir`) is no-server, not chain-complete**: the generator emits `{gateway}/{code root}/index.html?abx=<tokenData>` — liveness rides the gateway (default `ipfs.io`/`arweave.net`; repoint with `abx set-gateway <addr> --ipfs <prefix>`), permanence rides the pin/endowment, params ride the URL (**8KB budget** — `abx verify` reports `urlOverBudget`; big params ⇒ prefer template mode).
163
- - **Key enumeration lives in the contract — nothing to maintain**: the params store lists its own keys on-chain, so the generator's `tokenData` always carries the full param surface, byte-aligned with the resolver's. Add a param key any way you like and it appears; there is no key list to sync and nothing that can drift. (Projects deployed before this shipped point at the older generator, which read a `params.keys` CSV — they keep working, untouched, and nothing writes one any more.)
164
- - **Verify it**: `abx verify <addr>` eth_calls the generator's `onChainStatus` (branch — template/directory · chain-complete · unresolved refs · URL budget) AND decodes `tokenURI` straight from the contract, reporting the `animation_url` form. It also reports the **script + dependency lock state** (see freezing, below). `abx tokenuri <addr>` is the quick raw read of the document. **The cheapest proof a param really landed is `abx tokens <addr>`** — it reads the param store itself (`tokenParamKeys` → `tokenParam`), no resolver and no indexer anywhere. Don't go looking for params inside `tokenURI`: they aren't projected into it (they'd be a duplicate of chain state nobody parsed back). The other place they show up is inside the `animation_url` document, as the `tokenData` the program receives.
165
- - **Freezing a code project — the program, not just the metadata.** `lock-field`/`lock-uri` freeze *metadata*; they do **not** touch the on-chain program. The work is the script chunks, and the owner can keep rewriting them (`setScriptChunk`/`removeLastScriptChunk`) until you run **`abx lock-script <addr>`**. There is **no `abx replace-script`**: until that lock, the contract still accepts the ABI write, but the supported CLI does not expose post-deploy rewrite (only `deploy-code --resume` for an *incomplete* setup). To change a live program, deploy a new contract. The full set is **`abx lock-script`** (the program) + **`abx lock-dependencies`** (the library set) + `lock-field`/`lock-uri` (the metadata) + `set-schema … lock=now` for any param whose value should also freeze. Lock last — deploy unlocked, confirm it resolves, *then* freeze; `abx verify` shows what's still mutable. (SeriesCode/EditionCode expose `scriptLocked()`/`dependenciesLocked()` — the locks are plain owner-only function calls.)
166
- - **With ALL of those engaged, the output can still change — say so instead of promising a freeze.** Four routes, and none of them is a bug: **(1) an *ungoverned* param has no lock**, so the owner keeps writing it and the generator keeps injecting it as `tokenData`, which is exactly how a piece stays responsive to its holder — and even a *welded* param (`:lock=now`, below) can still be overridden at read time by an `--augment` hook until `lock-param-hooks` freezes the hook set; **(2) a `name@version` (Registry) `--dep` is re-fetched from the registry contract on every read** — `lock-dependencies` freezes the ref and the registry pointer, not the bytes the registry returns, so the library can change under a locked project. Only `--dep 0x…` (an immutable SSTORE2 data contract) is frozen by being resolved; **(3) a welded param's *contract-scope default* can still be cleared.** The weld closes token-scope writes and further `set-schema` on that key, but `clearContractParam` sits outside the schema guard on purpose (it is the only exit from a value poisoned before the schema existed), so an owner who left a collection-wide default in place can delete it after the lock and every token that merely inherited it changes. Writing the value **per token** under the governed path is what actually welds it; **(4) every lock freezes a POINTER, not the code behind it** — a locked hook, renderer or reader is a contract address, and if that contract is an upgradeable proxy its behavior can be replaced while `paramHooksLocked()` still reads `true`. The protocol defines locks as pointer locks and discloses the limit rather than attempting on-chain proxy detection, so a permanence claim requires **immutable** hook/renderer deployments. You cannot verify that from the address alone and there is no tool that does it for you — so the honest move is not to investigate, it is to scope the claim: say "these exact addresses can never change", which is true and verifiable, rather than "the output can never change", which you do not know. If the creator deployed the hook themselves and it has no upgrade path, they can say the stronger thing. `abx verify` says `chain-complete` in all of these cases, because that flag is about *where* bytes come from. **The line to give a creator: "your metadata is locked, and these exact addresses can never change" — and, if they want the stronger claim, the route is on-chain `0x…` deps, immutable hook/renderer contracts, per-token governed values, and locked fields.** A token that deliberately live-adapts is a good thing to build; just don't describe one as immutable. Background: https://abx.docs.artblocks.io/protocol/owner-powers/
167
-
168
- ### PostParam schema — the Type + Auth catalog
169
-
170
- A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:TokenOwner --schema 'speed:Uint256Range[0..100]:Creator'`). Quote any spec that contains `[` or `|` — zsh glob-expands them. The Type token carries an optional **bracket suffix**: a **`Select` MUST list its options** (`'mood:Select[Spring|Summer|Autumn|Winter]:TokenOwner'`, pipe-delimited — a Select with no options is rejected, because the on-chain schema requires them), and a **Range MAY carry bounds** (`'density:Uint256Range[0..100]:TokenOwner'` — omit for unbounded).
171
-
172
- **See what a live project already has: `abx state <addr>`** lists every governed PostParam — type, auth, bounds/options, an upcoming lock date, and a `retired` marker. Read it BEFORE `set-schema` on an existing key: the write is a full-row upsert, so you need the current shape to avoid clobbering a field you didn't mean to touch.
173
-
174
- **Both halves are plain chain reads.** Declared schemas enumerate on-chain (`paramSchemaKeys()` — every governed key, including one nobody has written yet), and every *set* value enumerates too (`contractParamKeys()` / `tokenParamKeys(id)`, then `contractParam`/`tokenParam` for the value; `paramSchema(key)` for its type). So a project's configure UI can be built from the chain alone, and a collector's write is publicly readable the moment it lands — no resolver, no indexer, no key list to keep in sync. `abx state <addr>` reads the schema half; `abx tokens <addr>` reads the value half. **Params are chain state, not a metadata projection** — they are deliberately not copied into `tokenURI` (a second serialization of something already enumerable, which nothing parsed back, on a document marketplaces need to stay cheap). Where they *do* appear is `tokenData`, injected into the program at render time; anything meant for a marketplace's trait display belongs in `attributes` (`abx.traits()` or an `--attributes-renderer`).
175
-
176
- **⚠ On an edition (`--copies`), a holder-writable param is SHARED — resolve this with the creator before the schema is committed, because it is on-chain from deploy.** A param belongs to the **id**, and an id's copies are all the same id, so:
177
-
178
- - **One value, many holders, last writer wins.** `TokenOwner` on an edition means *any* holder of that id. A `name:String:TokenOwner` schema does not let each collector name their copy — it lets whoever wrote most recently name the work for everyone. If per-collector configuration is the point, it needs **one id per copy** (a 721 `deploy-code`, or single-copy ids). Aggregate or monotonic state — a transfer counter, a communal mood, "has ever been held by" — is exactly what shared params are *good* at, and reads as intended there.
179
- - **A holder-writable `String`/`Bytes` key has no on-chain size budget.** The contract accepts any non-empty valid value (one SSTORE2 blob per key), so a single holder can fill every declared data key and push that id's `tokenURI`/generator document past what common RPCs will serve — for every co-holder at once, and **permanently** if a `:lock=` deadline then bites. This is a deliberate protocol choice: on-chain byte accounting would tax every project to police a configuration almost nobody should use, so the mitigation is the schema you choose. Keep holder-writable keys to **scalar** types (`HexColor`, `Select`, `Bool`, a bounded Range) unless a large holder-authored payload is genuinely the work; if it is, reach for `Address(0x…)` auth and a controller contract that applies your own size policy before forwarding the write.
180
- - The CLI says the same thing at the moment of decision — `deploy-code --copies` with a holder-writable `--schema` prints an advisory naming the keys. **Relay it to the creator; don't step past it.**
181
-
182
- Both are fine when *intended*: a 1-copy edition behaves like a 721, and the shared-value shape is the whole appeal of a communal piece. What must not happen is a creator discovering it after the mint.
183
-
184
- **The param surface is NOT frozen at deploy.** `abx set-schema <addr> --schema key:Type:Auth` attaches or replaces one key's schema on a live contract, so a piece that turns out to need another dial does **not** need a redeploy (which would cost the address, the mints, and the collectors). Two things to hold onto when you use it: it is a **full-row upsert**, so replacing a schema rewrites every field — restate anything you want to keep, including an existing `lock=`; and the chain does **not** re-validate values already stored under the key, so narrowing a bound, dropping a `Select` option, or changing the Type strands them (the CLI refuses that unless you pass `--force`). Tell the creator plainly before forcing one.
185
-
186
- - **Types:** `Bool` (`true`/`false`) · `Select[A|B|C]` (**options required in brackets**; set by a label from the list, or its index) · `Uint256Range[min..max]` (non-negative integer; bounds optional) · `Int256Range[min..max]` (signed integer) · `DecimalRange[min..max]` (decimal, ≤10 places) · `HexColor` (`#rrggbb`) · `Timestamp[min..max]` (Unix seconds **or** an ISO date like `2026-07-16`) · `String` · `Bytes` (`--file <path>` for the payload).
187
- - **Auth — who may set the param:** `Creator` (the contract owner) · `TokenOwner` (the token's current holder; **delegate.xyz honored**) · `Address(0x…)` (a specific named writer — **name it inline**, e.g. `board:Bytes:Address(0xabc…)`) · and the `Or` combinations `CreatorOrTokenOwner` · `CreatorOrAddress` · `TokenOwnerOrAddress` · `CreatorOrTokenOwnerOrAddress`. The chain enforces it — a wrong signer reverts. There is **no "anyone" leg**, but the `Address` leg is a plain `msg.sender` check with no EOA restriction, so **a contract may hold it** — that is how open / multi-party participation is built (a controller contract applies its own rules and forwards the write). If a creator wants a communal canvas or open entry, that is the shape to describe, not a missing feature.
188
- - **`:lock=<when>` — an optional 4th field** that freezes the param after a time (`palette:HexColor:TokenOwner:lock=2026-12-31`; ISO date, unix seconds, or `now`). A lock already in the past is permanent, which is the supported way to **retire** a param: `abx retire-param <addr> <key>`. Past the deadline the chain welds **both halves**: every `configure-param` reverts `ParamLockExpired`, and so does any further `set-schema` on that key — so the Type, Auth, bounds and `Select` options are frozen too (otherwise a locked Select's options could be swapped and a collector's chosen "Ember" would re-render as "Frost"). The deadline is **monotonic** — a later `set-schema` may only move it *earlier*, never later and never back to open (`ParamLockNotExtendable`), so a weld can't be undone. It does **not** remove the key (a governed key stays governed) and does **not** erase a value already stored — that value keeps serving. Never describe retiring as deleting. **The one thing the weld does not cover: a contract-scope DEFAULT on that key can still be cleared.** `clearContractParam` is deliberately outside the schema guard (it is the only recovery path from a value poisoned before the schema existed), so an owner can leave a collection-wide default in place, weld the key, sell, and later delete the default — changing every token that never wrote its own value. Clearing can only *remove* a fallback: it cannot forge a value, bypass an auth rule, or touch a token-scope value already written. If a collection-wide value must be frozen, write it **per token** through the governed path rather than relying on the inherited default.
189
- - **⚠ A zero-length `String`/`Bytes` write is REFUSED on chain** (`InvalidParamValue`, before any hook runs, whatever the auth) — the blob path's `dataLength` is a hook's scalar-vs-blob discriminator, so zero has to be impossible there. This bites any list-like or optional payload: "unequip everything", "clear my inscription", an empty selection. Design the empty state in: a **sentinel byte** the renderer recognizes (`0x00` = empty), or a scalar companion key holding the count. A key that was *never* written reads as unset — that is the only genuinely empty state, and it is not reachable again once written.
190
- - Examples: a collector-tunable color → `palette:HexColor:TokenOwner`; a collector-chosen mood → `mood:Select[Calm|Wild|Chaotic]:TokenOwner`; a creator-only bounded dial → `speed:Uint256Range[1..10]:Creator`; an on/off toggle → `invert:Bool:TokenOwner`.
191
- - **Cost, so a minter that configures params can budget gas:** a **cold** scalar write (first ever for that key on that token) ≈ **153k**; a **warm** overwrite ≈ **17k**. A `Bytes`/`String` blob: cold 32 B ≈ **209k**, warm 32 B ≈ **57k**, ~**203 gas per payload byte** on top. Cold-vs-warm dominates, not type — a mint that configures three fresh params pays ~3 cold writes (≈ 426k for two scalars + a 64 B blob), and the same three writes later cost a tenth of that. Budget for cold. Details: https://abx.docs.artblocks.io/protocol/params#what-a-write-costs
192
-
193
- ## `--image-base` — deterministic S3/CDN thumbnail URLs (no metadata resolver)
194
-
195
- `deploy-code --image-base https://cdn.you/orbit` bakes the on-chain `image` as a **`url-template`** (`https://cdn.you/orbit/{id}.png`) — a stable per-token URL the chain names — and the effect runner writes each token's still to that exact object (overwrite in place) when you render it. Marketplaces read the on-chain `tokenURI` → the image URL → the bytes the runner PUT; **no resolver serves the image.**
196
-
197
- - **⚠ Thumbnail freshness — the honest tradeoff.** With **no resolver there is no chain-watcher**, so on this lane the still is **backfill / manual**: run `abx render <addr> [ids]` after minting, and **re-run it after any PostParam change**. A param change updates the on-chain **animation instantly** (it reads the param live), but the **S3 still stays stale until you re-render**. It's a **pick-one at the baseline: on-chain-URI durability with *manual* thumbnails, OR an off-chain resolver with *continuous/live* thumbnails** (the resolver is the watcher). Don't promise "fully on-chain AND auto-updating thumbnails."
198
- - **Needs a mutable, path-addressed host** — S3 / R2 / a CDN (`--backend cloud`), **not ipfs/arweave** (content-addressed: the URL changes with the bytes, defeating a fixed per-token address). `deploy-code --dry-run` validates this combo and prints `render/storage ✓|✗ <reason>`; a real run refuses a bad one outright.
199
- - **Provisioning:** the upload side (`ABX_S3_ENDPOINT`/`BUCKET`/`ACCESS_KEY_ID`/`SECRET_ACCESS_KEY`) and the public serve side (`ABX_S3_PUBLIC_BASE`, which must equal `--image-base`) are DIFFERENT hosts (API endpoint vs public read URL — R2/S3 both have this split) — verify both are live and agree with `abx storage show --check` (detail: [hosting.md](hosting.md#storage-backends-byte-custody)).
200
- - **How a render writes:** the runner keys the still by the URL the on-chain template names (token N → `{key}/N.png`), plus a `…N.png.abxhash` sidecar for idempotency (skip when unchanged; overwrite when the inputsHash advances). It needs a live view to screenshot — a local `abx serve` render aid (run it while rendering, kill it after; never baked on-chain) or a resolver.
201
- - **Pairs with `--onchain-uri`** for the "no metadata server" drop: tokenURI + animation on-chain, thumbnail at a deterministic S3 URL on-chain, and — if the traits port — `--attributes-renderer` for on-chain traits.
202
-
203
- ## In-chain Solidity SVG — the zero-dependency lane
204
-
205
- The purest form: the work itself is a **Solidity `IAbxFieldRenderer`** that returns an SVG from the token's `seed` + params — no JS program, no browser, no bucket, no resolver, no effect runner. Image AND traits are computed on-chain and the `tokenURI` is assembled on-chain, so the token depends on **nothing outside the EVM**.
206
-
207
- ```bash
208
- abx deploy-code --image-renderer 0x<svgRenderer> [--attributes-renderer 0x<traitsRenderer>] \
209
- --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol … [--dep none]
210
- ```
211
-
212
- - **No `--script`/`--code-dir` is ALLOWED, not required.** `deploy-code` accepts a renderer-only project (neither program mode) as long as `--image-renderer` (and/or `--attributes-renderer`) is set. Renderer-only has no `animation_url` — the SVG `image` *is* the work; the metadata renderer omits an unset animation field, and the CLI does **not** wire the generator (wiring an animation leg at a zero generator would revert every `tokenURI`).
213
- - **But a script AND renderers together is the both-worlds shape — verified, and the most-missed option.** `--script f.js --image-renderer 0x… --attributes-renderer 0x… --onchain-uri` gives `animation_url` assembled on-chain from the script chunks *and* `image`/`attributes` computed by Solidity: every marketplace surface has an on-chain home, and there is **nothing to render, host, or refresh** (no runner, no bucket, no resolver). The fields are independent in the metadata renderer, so they compose. Prefer this over renderer-only whenever the piece is actually a program.
214
- - **On-chain `tokenURI` is the CLEAR default here** — unlike the JS/p5 lanes. A Solidity SVG reads *small* (a few hundred bytes–few KB), so the large-`tokenURI` marketplace-read caveat does not apply. Recommend it enthusiastically; there is no maneuverability/infra tradeoff to weigh because there is no infra.
215
- - **The renderer is a contract the creator deploys separately** (the CLI doesn't compile Solidity). **`abx scaffold-renderer <dir>`** writes a ready-to-build Foundry project — a worked `MyRenderer.sol` (image → `image/svg+xml` from `seed` + a `palette` PostParam), a coherent `MyTraits.sol` reading the SAME seed math, the `IAbxFieldRenderer`/`IAbxParams` interfaces (with the invariants documented), a `forge test` proving `render()` never reverts (incl. the collection surface + a fuzz), a `Deploy.s.sol`, and a README. The creator forks the sample, then `forge soldeer install && forge test`, deploys with forge, and passes the address to `deploy-code --image-renderer 0x…`, which **verifies the address has code** (real deploy refuses a codeless address; dry-run probes best-effort) — same guard as `--attributes-renderer`. A renderer reads the token's seed/params directly (`IAbxParams(token).tokenParam(tokenId, "seed"|"palette")`), computes bytes, and returns `(contentType, data)`. Interface + invariants also at https://abx.docs.artblocks.io/protocol/renderers/.
216
- - **PostParams still apply — and you MUST declare them; the CLI can't.** Unlike the JS lane (where `abx inspect` statically detects the params a script reads), a Solidity renderer is opaque to the CLI — it cannot know your renderer reads a `palette`. **Read the renderer, and declare every PostParam it reads with `--schema key:Type:Auth`** (the example reads `palette` → `--schema palette:HexColor:TokenOwner`). Skip it and the param is **fixed at the renderer's default forever** — collectors can't set it (the exact miss from a real session: a palette-tinted renderer shipped with `schemas []`, stuck on the default). With the schema declared, a collector's `configure-param` re-addresses the on-chain image automatically (the renderer reads the live param — no re-render, there's no off-chain still). `deploy-code --dry-run` nudges when renderers are set with no `--schema`.
217
- - **`abx render`/`abx effects` are irrelevant** (there's no off-chain still to produce), and `isCodeProject` is false for a renderer-only project — both are expected, not errors.
218
- - **Verify:** a raw `abx tokenuri <addr>` (or `cast call tokenURI(0)`) decodes to `name` + `image` = `data:image/svg+xml;base64,…` + on-chain `attributes`, with **zero `http(s)` URLs** — the from-chain proof it's fully in-chain.
219
-
220
- ### Authoring / reviewing an `IAbxFieldRenderer` — the invariants to CHECK before you wire it
221
-
222
- The renderer is the creator's own Solidity (compiled + deployed with forge — the CLI doesn't run Solidity). `deploy-code` confirms the address **has code**, but it does **not** — and cannot cheaply — prove the renderer *behaves*. So an agent helping author or ship a renderer must **review the `render` function against these invariants**, and the creator should forge-test them:
223
-
224
- - **NEVER revert — for ANY token or param state.** This is the one that bricks a drop: the canonical `AbxMetadataRenderer` staticcalls `render()` with **no `try/catch`**, so *any* revert reverts the entire `tokenURI` (and `contractURI`). The function must return cleanly for: a token with **no seed**, **unset params** (no palette), and the **collection surface** (`tokenId == type(uint256).max`, per `IAbxFieldRenderer` — no token) → return a neutral value there (`[]` for attributes, a plain card for an image), never revert.
225
- - **Correct content-type + shape.** `image` → `image/svg+xml` (or another image MIME) returning a valid document; `attributes` → `application/json` whose bytes are a JSON **array** `[{"trait_type":…,"value":…},…]` (numbers unquoted, strings escaped). A wrong type or malformed array is a broken/blank marketplace field.
226
- - **Guard the field, wire the right one.** Revert `UnsupportedField` for a field it doesn't serve (a wiring mistake fails loud), and make sure `--image-renderer`/`--attributes-renderer` point at the renderer that actually serves that field.
227
- - **`view` + deterministic.** Same chain state → same bytes. No unseeded randomness; read block/oracle state only if you *intend* live data (it re-reads per view).
228
- - **Read params LIVE via `IAbxParams(token)`** (`tokenParam`/`contractParam`, token scope wins; `tokenParamKeys`/`contractParamKeys` if the renderer must handle keys it wasn't written to name). **`Bytes`/`String` params need the OTHER reader:** their `bytes32` is a keccak commitment (`valueIsHash == true`) and the content comes from `tokenParamData(tokenId, key)` / `contractParamData(key)` (~24KB per key — the two types that can carry an actual payload). Reading a `Bytes` param through `tokenParam` hands the renderer a hash and draws garbage with nothing failing anywhere; empty returned bytes are the "use a default" signal — so a collector's `configure-param` shows up on the next read with no redeploy. Never bake a param value in at deploy.
229
- - **Keep the output bounded.** It assembles into `tokenURI` per call; a very large SVG/HTML can strain the `eth_call` gas on unauthenticated public reads.
230
- - **A `Select` param stores an INDEX — read its label from the chain, never hardcode the list.** `IAbxConfigurableParams(token).selectOption(key, index) → string` returns one declared option (`paramSchema(key)` returns the whole `string[]`). A renderer that keeps its own parallel `string[]` of names is a second copy with nothing keeping the two in step, so a later `set-schema --force` that reorders or renames an option silently changes what every token *means* without changing what it says. Both getters are on every deployed ABX code project — check before you write the constant.
231
- - **An augment hook does NOT reach your renderer.** `render(token, tokenId, field)` takes no `tokenData`, so a hook wired on the project never fires for it. The hook is a compute seam over `tokenData`, and the two things that assemble `tokenData` are the canonical `AbxGenerator` (on-chain, for `animation_url`) and the off-chain resolver — a renderer you wrote assembles nothing. Do NOT tell a creator "augment hooks are a resolver concept"; that is wrong (the generator calls one on-chain). The accurate line: **whoever builds tokenData calls the hook, and in your renderer that is you** — so read the live state directly, or read `paramHooks()` and call `augmentTokenParams` yourself (wrapped, so a broken hook can't make `render()` revert).
232
-
233
- Fork `contracts/src/renderers/examples/{SeedSvgRenderer,SeedTraitsRenderer}.sol` — they satisfy every invariant above (graceful fallbacks, the sentinel, content-types, live param reads) and are the reference to review a fork against.
234
-
235
- ## Traits on-chain vs off-chain — is your script's trait logic reproducible in Solidity?
236
-
237
- A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](https://github.com/ArtBlocks/abx/blob/main/docs/research/onchain-traits-feasibility.md). Give a **graded** answer, never a flat "not feasible":
238
-
239
- **First, the load-bearing distinction: *portable* ≠ *deployed*.** `inspect` saying traits "port EXACTLY" means the *logic* can be reproduced in Solidity — it does **not** mean a renderer exists. On-chain traits require the creator to **author + deploy** a Solidity attributes-renderer (fork `SeedTraitsRenderer.sol`) and pass its address to `--attributes-renderer` — a real build step, not a flag with a default. `deploy-code --dry-run` prints the traits disposition (`on-chain` / `off-chain via resolver` / `⚠ OMITTED`) and a real deploy **refuses an `--attributes-renderer` address with no code**. So never present "traits on-chain" as settled off a guessed address: confirm a renderer is deployed, or the honest options are off-chain-via-resolver or omitted.
240
-
241
- - **Mechanism (not a gap):** the canonical `AbxMetadataRenderer` embeds `attributes` from an on-chain `attributes` field — `inline` JSON, or a **`renderer`** field-renderer that computes the array (see `contracts/src/renderers/examples/SeedTraitsRenderer.sol`, a forkable worked example). Wire it with `deploy-code --attributes-renderer 0x…` — **no canonical-renderer edits, no per-token writes.**
242
- - **Reproducible (exact):** seeded p5 `random()` (a documented LCG: `z=(1664525·z+1013904223) mod 2³²`) driving `floor`/threshold/`%`-select traits, or traits read straight from the seed/params. Ports to integer Solidity exactly — mirror the LCG + the *call order* + the seed hash.
243
- - **Not reproducible:** unseeded `Math.random()` (non-deterministic — nowhere), or a trait reading a raw float at a precision boundary. Then serve `attributes` off-chain via a resolver, or ship without marketplace traits (tokenURI + animation are still fully on-chain).
244
- - **Best practice:** author traits **Solidity-first** (seed/param integer functions, `SeedTraitsRenderer`-style) and mirror them in JS — on-chain and rendered traits then agree by construction, no PRNG archaeology.
245
-
246
- ## The `deploy-code` command
247
-
248
- ```bash
249
- abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
250
- (--public-base-url https://your.resolver.domain | --onchain-uri) \
251
- [--image-base https://cdn/…] [--attributes-renderer 0x…] \
252
- [--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed] \
253
- [--dep <name@version|0x…>[,…]] [--dep-registry 0x…] \
254
- [--description "…"] [--external-url <url>] [--backend ipfs|arweave] [--dry-run] [--confirm]
255
- ```
256
- **Always `--dry-run` first**, then mirror its output into the [confirm readout](../SKILL.md#confirm-before-sending): it prints the deterministic address, resolver base, PostParam schema, mint plan, and tx count with **nothing sent and no bytes pinned**. (`--confirm` adds an interactive y/N before the real send.)
257
-
258
- - **`--script <file>`** — template mode: the program stored **on-chain in chunks** (auto-split ~22KB). Zero-dependency vanilla JS is the cleanest case; declared libraries are the Dependencies extension (`--dep`). Large PostParams inline cleanly here.
259
- - **Dependencies (template mode)** — declare libraries with `--dep <ref>` (repeatable or comma-separated; **ordered — the first ref is index 0 = the runtime**, e.g. `--dep p5@1.0.0`). A ref auto-detects: `name@version` (AB registry naming) resolves through the collection's registry pointer — `deploy-code` defaults it to the chain's **AB Dependency Registry** (`--dep-registry 0x…` overrides; a chain with none known warns + skips, never blocks) — while `0x…` declares an on-chain data contract, read directly. Registry deps are existence-checked before deploy (best-effort; a miss is a warning): **a CDN-served record is the normal production path, not a degradation** — on-chain bytes are the durability floor. Post-deploy: `abx set-dependency <addr> <index> <ref>` · `abx remove-last-dependency <addr>` · `abx set-dependency-registry <addr> <0x…|none>` · `abx lock-dependencies <addr>` (freezes list + pointer).
260
- - **`--code-dir <dir>`** — directory mode: a build folder (must contain `index.html` **and its own `abx.js` copy** — the build must read `abx.tokenData` + call `abx.traits({…})`, see [Authoring the program](#authoring-the-program--the-abxjs-runtime-contract-get-this-right-first)) uploaded via `putDirectory` (ipfs/arweave); its root becomes the on-chain `code` field. The live view 302s to the gateway with `?abx=<canonical tokenData>` — **so the gateway must serve HTML** (dedicated Pinata gateway or Arweave, never the shared public one).
261
- - **`--description "…"` / `--external-url <url>`** — collection identity, written as **on-chain collection fields in the deploy tx** (a code project has no operator-metadata table of its own, so these ride on-chain; the metadata renderer stitches them into `tokenURI` under `--onchain-uri`, a resolver reads the same fields). Set them or the metadata is bare. (Any other unsupported flag warns "unrecognized flag, ignored" — a typo can't quietly drop a value.)
262
- - **Seeds** — drawn at mint from the canonical `AbxSeedSource` (settled once assigned; `--no-seed` opts out; curated pre-set seeds win). **Pseudorandom, NOT lottery-grade** — see [Seeds](#seeds--pseudorandom-not-lottery-grade) below before a creator prices scarcity off it.
263
- - **`--schema key:Type:Auth`** — governed PostParams (e.g. `palette:HexColor:TokenOwner`). Reconfigure any lane with `abx configure-param <addr> <tokenId> <key> <value>` — it reads the on-chain schema and canonically encodes the input (`#rrggbb`, decimals, Select by label). **Payload types are explicit: `String` takes literal text, `Bytes` takes `0x`-prefixed hex or `--file <path>` — a bare string on a `Bytes` key is REFUSED**, because storing those characters as bytes can't be told apart from meaning them literally; delegate.xyz honored on the TokenOwner leg. A param change **re-addresses** renders (self-invalidating stills). **The re-render is automatic wherever the resolver's chain watcher is on (the default):** the watcher sees the change on its next poll (~12s) — no matter WHO sent it or WITH WHAT tool — re-indexes, and notifies the runner. `abx configure-param … --remote <resolver>` remains an **immediate nudge** (skips the poll wait) and the fallback for a watcher-disabled resolver. Expect thumbnail refresh ≈ poll cadence + render time (~15–30s), not instant.
264
- - **How it resolves (live, uncached; not on-chain):** the resolver rebuilds `tokenData` per view. Directory mode 302-redirects to the gateway with params in the **query string** (so **very large PostParams favor template mode**, which inlines them with no URL ceiling); template mode assembles the HTML inline from the chunks.
265
- - **Lanes**: all three. `--unsigned` needs `--for <signer>` and prints the whole pre-computed sequence (deterministic deploy → the clone address is known up front). On a chain with no canonical factory, all deploy commands **stop with guidance**; deploying your OWN trust anchor is an explicit `--bootstrap-factory` opt-in (private chains/sandboxes only). For a code project that anchor is a short sequence — the write-path libraries (`AbxParamsLib`, `AbxCodeLib`, plus `AbxEditionLib` with `--copies`), then the factory linked against them — and note any `--copies` deploy, code project or not, bootstraps `AbxParamsLib` + `AbxEditionLib`, since all three ERC-1155 token types link that library — all CREATE2 at canonical salts, so it lands at the same addresses a `forge` bootstrap would and **re-running finishes a partial bootstrap** (an already-deployed library is reused, never duplicated) rather than failing.
266
-
267
- ### Seeds — pseudorandom, not lottery-grade
268
-
269
- Each mint calls the project's seed source and stores the result as the token's `seed` param, which is what `abx.tokenData.seed` delivers to the program. The canonical `AbxSeedSource` hashes the project address, the token id, and block values (`prevrandao`, the previous block hash, the timestamp) — **not** the recipient, deliberately: a buyer names the recipient (`purchaseTo`), so hashing it turned a buyer-chosen field into a search space (grind candidates in a view loop, buy once at the winner).
270
-
271
- **Say this plainly to any creator whose pitch leans on rarity.** The seed is pseudorandom, generated from on-chain params, and is **not strong enough to run lottery-like logic**. Both of these are true at once:
272
- - **Deterministic after the fact** — anyone replays it from the block. That is the property it exists for: it's what makes the generative output verifiable.
273
- - **Not secret before the fact** — every input is readable *inside* the minting transaction, so a contract minting there can compute the seed it would receive and revert unless it likes the result (a buyer declines outcomes for the price of gas), and a block builder can reorder or omit transactions. Dropping the recipient removed cheap *targeting*, not accept-or-decline — **on the 721 lane.**
274
-
275
- **On an edition (`--copies`) the buyer CHOOSES, and that's the product.** Sales are keyed `(token, id)`, so an edition buyer names the `id` — which work they want — and an `EditionCode` id's `seed` is drawn at that id's **FIRST** mint (the seed belongs to the work, not to a copy). So: buying an already-minted id is **choosing among settled, publicly readable works**, exactly as on secondary; buying an unminted id **draws its seed with the buyer's chosen `id` in the preimage**, which is a real selection surface (grind ids in a view loop, buy the best) bounded only by which ids have a configured sale and by `maxInvocations`. Never describe the edition lane as "decline, not choose" — that's the 721 story. `id` can't be dropped from the preimage the way the recipient was: it *is* the work. A creator who doesn't want selection has two clean answers: **mint the ids themselves first, then list on secondary** (a first-class way to run an edition — the owner may always mint, paused or not, and every seed settles under their own transactions), or use a custom `IAbxSeedSource`.
276
-
277
- So it fits work where the seed diversifies output and the **distribution** is the product. It does **not** fit a prize draw, a raffle, or any mint where one rare outcome is worth materially more than the mint price and ordering is contestable.
278
-
279
- **If seed generation must be fully random, the creator supplies their own `IAbxSeedSource`** — commit-reveal (commit at mint, resolve from a later block), an off-chain VRF oracle, a curated queue. `seedSource` is a **per-project address**, so that is a swap, not a fork of ABX, and both ends of it are first-class CLI surface:
280
-
281
- | | |
282
- |---|---|
283
- | at deploy | `abx deploy-code --seed-source 0xTheirSource …` (omit it, or `--seed-source canonical`, for the shared `AbxSeedSource`; `--no-seed` for none at all — passing both `--no-seed` and `--seed-source` is refused) |
284
- | after deploy | `abx set-seed-source <addr> <0x…\|canonical\|none>` — owner-only, **future mints only** |
285
-
286
- Both **probe the address before using it**: an `eth_call` to `seed(uint256,address)` that must return 32 bytes, else the command refuses. That check is not ceremony. A seed source is the one setting whose misconfiguration is completely silent — the write succeeds, `seedSource()` reads back what you set, `abx state` prints it, and then *every mint of the collection reverts* in the token's `bytes32` decode. `code.length > 0` does not catch it: a Safe, an uninitialised proxy, and a 7702-delegated EOA all have code and all answer an unknown selector with empty success. Pasting the creator's own wallet or Safe here is the common way in, so expect the refusal and read it.
287
-
288
- Two things the CLI still does **not** do: it does not write or audit the source contract (its randomness properties are the creator's to state to buyers — ABX makes no claim about them), and it cannot verify a source that isn't answering yet. A commit-reveal source that reverts until it's armed will be refused — arm it first, then `abx set-seed-source`. A custom source is free to hash `to`, and inherits the grinding surface above if it does.
289
-
290
- **Re-pointing mid-sale is legitimate but visible-only-if-you-look.** Assigned seeds are settled, so a change applies to future mints and a part-sold collection ends up spanning two sources. `SeedSourceSet` puts it on the event spine and `abx state <addr>` prints the current source (flagged CUSTOM when it isn't the canonical one), but an early buyer is not notified. If a drop is live, pause and say so.
291
-
292
- **A collector-chosen seed is a pre-sale commitment — and it is not a "re-roll".** A settled seed is terminal by every route — nothing rewrites or clears it — *unless* the project declared a `seed` PostParam schema, typically with a `TokenOwner` auth leg so a collector can set the seed of their own token.
293
-
294
- Be exact about what that gives a collector, because the intuitive word for it is wrong. The governed path is `configureTokenParam(tokenId, "seed", value)`: **the caller supplies the value.** Nothing is re-randomized and the seed source is never called again — with an unbounded `Uint256Range` the authorized party may set literally any 32-byte value, and set it again until they like the output. So describe it as "pick your seed" / "set your own seed", never as a re-roll: a re-roll implies a fresh random draw a collector could reasonably expect to be fair, and there is no draw.
295
-
296
- The schema can only be declared **before the collection's first seed exists**; afterwards the contract reverts `SeedSettled`. That boundary is the first seed, not the sale opening, and the gap between them is owner-reachable: declaring the schema is an ordinary transaction, so an owner can land it ahead of a buyer's already-signed, already-broadcast first-mint transaction, and the mint still succeeds normally — after which every token the collection ever mints (not just that one) is reassignable. So decide this with the creator, one way or the other, **before opening any sale or granting any minter** — not merely "before minting anything" — and never tell a creator or a buyer that `paramSchema("seed")` reading empty is a forward guarantee once a sale might be live: it only describes the block it was read in. Never promise reassignment as something you'd add later, either. Treat it as advanced and prove it on testnet first: the type has to be a **literal**/scalar one (a `String`/`Bytes` schema routes writes to the data path, which a seed refuses outright). A buyer reads the answer on-chain with `paramSchema("seed")` — or `abx state <addr>`, whose **PostParams** block lists every declared schema: a `seed` row there means the value can be set (and says by whom), no row means final as of that read.
297
-
298
- ## `--copies` — a generative drop sold as an EDITION (EditionCode), and what v1 gives up
299
-
300
- `abx deploy-code --copies <n|open>` swaps the 721 **SeriesCode** for its ERC-1155 twin **EditionCode**: N ids, each a distinct work/seed, **× `--copies` copies of each**. `--mint-count`/`--mint-all` keep their meaning (which ids premint) and `--mint-amount <n>` sets how many copies of each. Per-id sales, per-id supply caps → [operating.md](operating.md#edition-owner-ops).
301
-
302
- **Source is `--script` or a Solidity renderer; on-chain dependencies DO work. The CLI refuses the rest rather than silently dropping them:**
303
-
304
- | Flag | With `--copies` |
305
- |---|---|
306
- | `--script <file>` | ✅ a JS program stored on-chain in chunks |
307
- | `--image-renderer` / `--attributes-renderer` | ✅ **works** — the in-chain Solidity lane, same as the 721 twin |
308
- | `--dep <ref>` / `--dep-registry` | ✅ **works** — same ordered refs as the 721 lane (index 0 = the runtime) |
309
- | `--code-dir <dir>` | ❌ refused — *"use `--script` (on-chain chunks) instead"* |
310
- | `--image-base` | ❌ refused — it needs the effect runner to write a still **per id**, which this lane doesn't drive |
311
-
312
- **A TRUE fully-on-chain ERC-1155 exists — say yes to it.** `--copies open --image-renderer 0x… [--attributes-renderer 0x…] --onchain-uri` is a real, permissionless, ongoing 1155 whose image and traits are computed in Solidity and whose `uri(id)` assembles on-chain: **no resolver, no render runner, no bucket, nothing to keep running.** `--script` is *optional* here — a renderer-only edition stores no program and has no `animation_url` (the same shape the 721 lane calls renderer-only), and script + renderers together is the both-worlds composition. This was refused by the CLI until 2026-08-24 and a field report read the refusal as a protocol limit, shipping a 721 "signature + prints" workaround for what should have been an edition. Don't repeat that: the answer to *"can I get a real on-chain-rendered 1155 with no server?"* is **yes**.
313
-
314
- So **"an edition of my p5 sketch, with p5 coming from the chain" is a real lane**: `abx deploy-code --script art.js --copies 100 --dep p5@1.0.0 --onchain-uri`, on **Sepolia** (`ABX_CHAIN=sepolia` — the AB dependency registry does not exist on Base Sepolia, where the pointer leg is skipped with a warning and the drop is NOT chain-complete). Confirm it with `abx verify <addr>`, which should say *"chain-complete — every dependency resolves to on-chain bytes; no server, gateway, or CDN in the graph"* (a claim about provenance, not immutability — see freezing, above).
315
-
316
- Two things still to watch on this lane:
317
-
318
- 1. **A self-contained script needs no `--dep` at all** — but if your sketch calls p5 globals (`createCanvas`, `randomSeed`) and you *don't* declare the dependency, it **deploys fine and renders blank**. `abx inspect <script>` reports the libraries it detects; the edition dry-run does not cross-check that for you, so check it yourself before shipping.
319
- 2. **On the `--script` lane a thumbnail is still rendered off-chain** by the effect runner, so a JS code edition needs a public home for its stills even when `uri()` is fully on-chain, and `abx verify` says the image is a placeholder until you render one. **This does not apply with `--image-renderer`** — there the image IS the on-chain SVG, there is no still, and nothing to render or host.
320
-
321
- Everything else about a code project is unchanged by `--copies`.
322
-
323
- ## Stills + traits — the render effect
324
-
325
- The render effect is the **ONLY source of a real thumbnail** — skip it and `image` stays a placeholder. Three ways to run it: the **hosted** runner (`abx deploy-effects --resolver-url <resolver>`), a **local continuous** runner (`abx effects` — in-process beside `abx serve`, auto-renders every mint + param change), or the **one-shot repair lane** `abx render <addr> [id…]` (add `--remote <resolver>` to publish to a HOSTED resolver; idempotent; local captures need `npx playwright install chromium`).
326
-
327
- - **`abx render <addr> <id> --force`** re-renders a still that already exists — the fix for a **bad / blank / timed-out capture**. The output is deterministic (fixed seed + params), so a plain `render` idempotent-skips an existing still and `--force` re-captures the *same* pixels; it is **not** a way to change how correct output looks (to change appearance, change an input — the `palette`/etc. PostParam via `abx configure-param`, which re-addresses the render).
328
- - A render that hits an unavailable/erroring live view **fails loudly and stores nothing** (no garbage thumbnail).
329
- - The runner uploads each render to the storage home (`ABX_STORAGE_BACKEND`) and **publishes** it to the resolver — a durable `ipfs://`/`ar://` locator the resolver 302-redirects to, or bytes for small must-inline outputs (traits). A local `abx render` with the default `fs` backend does NOT reach a hosted resolver — use `--remote` (publishes) or a public backend / co-located runner.
330
- - Script-reported traits (`abx.traits({...})`) stitch into `attributes` (on-chain wins > render > operator).
331
-
332
- ## Verify it actually resolves — before you tell the creator it's live
333
-
334
- **This testnet drop IS the preview / e2e** — it's the *real* wiring (renderers, generator, on-chain assembly), so inspecting it here is how a creator gains confidence before any mainnet launch; there's no local approximation to trust. A code project has the most that can silently break. Confirm the whole chain through the **baked** URL; don't announce success off a deploy receipt alone:
335
- 1. `abx tokenuri <addr>` → shows the **resolver base**, never `localhost` (or, on `--onchain-uri`, decodes straight from the contract). Etherscan's "Read Contract" reverting on a big on-chain doc is a client gas cap, not indexing lag or a broken token → [troubleshooting.md](troubleshooting.md#abx-tokenuri--etherscan-reverts-on-a-fully-on-chain-code-project).
336
- - **The rendered thumbnail is only real if the on-chain `image` had a destination at deploy** (`--image-base <bucket>` or a resolver). If you deployed `--onchain-uri` with neither, `abx render` writes to a local store the tokenURI never points at → **orphaned**; the marketplace still is the placeholder forever. There is no fix without a re-point tx (`set-field image <public url>` then re-render) — which is why it's a deploy-time decision.
337
- 2. the resolver serves `/t/<chainId>/<addr>/0` (real JSON, **not** `{"error":"unknown project"}`). If it errors, it's still backfilling or scanning from block 0 ([setup.md](setup.md)) — fix the **hosted** resolver; a local `abx serve` does **not** fix a hosted-baked token.
338
- 3. the live view `/a/<chainId>/<addr>/0` loads. A **`503`** ("its on-chain code has not been folded into the projection yet") is the retry-able answer — the resolver knows it's a code project but hasn't read the code yet; wait a beat and re-ask. A **`404`** is the terminal one, and the split is the diagnosis: **`404 {"error":"no live view — not a code project"}` on a project that IS a code drop is a scan-floor bug, NOT a version/compat problem** — the resolver indexed *above* the deploy block, so it missed the `code` field written at deploy (`collectionFields` comes back `[]`). Check `GET /api/project/<addr>` → `fromBlock` should equal the deploy block and `collectionFields` should contain `code`. Fix: `abx add <addr> --remote <url> --from-block <deployBlock>` (a *changed* floor forces a full replay). Do **not** conclude "the resolver doesn't support code projects" or redeploy as a static NFT. Directory mode then 302s to the gateway — the redirect must have **exactly one** gateway prefix (a doubled `https://arweave.net/https://arweave.net/…` is a stored-locator bug, fixed in-toolkit).
339
- 4. `abx tokens <addr>` → **every** token's owner, seed, and params in one read, straight from the contract (no indexer, no resolver, no server). This is the "what did the seeds actually deal?" answer, and for a generative collection **the seed list IS the collection** — the natural input to any distribution check before a real launch. `--json` for the machine form (`{tokenId, owner, seed, params}` per token). Note what it is *not*: traits come from running the script against the seed, so a trait spread comes from `abx render` / the effects runner, never from this command. Do **not** hand-roll the old workaround (serve → GET the project API → base64-decode `tokenURI` → base64-decode the `animation_url` inside it → regex the seed out of the HTML); every value is a plain contract read.
340
- 5. `abx verify <addr>` → per-minted-token render presence + live-data posture. **For a HOSTED drop use `abx verify <addr> --remote <resolver>`** — it reads the resolver's effect-status API and reports the real **4-state** per token: `up to date` · `rendering` · `failed` (with the actual error + attempt count — fix, then `abx render <addr> <id> --force --remote`) · `stale` (the next notify/sweep picks it up). Plain `abx verify` only checks THIS machine's store, so a render **published** to a hosted resolver reads as a false placeholder locally. A big batch drains through the runner's queue in ascending token order — `stale → rendering → up to date` is normal.
341
-
342
- **⚠ Arweave (`--backend arweave`) directory mode propagates with a DELAY — set this expectation.** Unlike IPFS/Pinata (a pin is servable almost immediately), a Turbo/Arweave upload settles over **minutes (sometimes longer)** before the gateway serves it. So right after an arweave directory deploy: the live view `/a` 302s to `arweave.net`, which **404s until it propagates**, and a render run *now* will (correctly) **fail with "content isn't servable yet" and store nothing** — expected, not a bug, and it will NOT leave a garbage 404-page thumbnail. **Re-run the render (or let the effects service sweep) once the content is live** — check with **`abx storage status ar://<manifestTxid>/index.html`** (`ready` vs `propagating` vs `unreachable`; exits non-zero until it serves, so `until abx storage status <loc>; do sleep 10; done` is the whole wait). Do NOT reach for `curl` here — the command also probes other gateways, which is what distinguishes "yours is behind" from "the locator is wrong", and it tells you plainly not to re-upload. IPFS directory content is renderable right after the pin.
343
-
344
- ## Mint order, timing, pause, supply
345
-
346
- **Metadata is the token id.** Tokens mint **in order** (`0,1,2,…`); token 3 shows work 3. No token-id ↔ metadata-id decoupling — to shuffle or sell specific tokens, pre-mint then trade.
347
-
348
- **Mint timing** — same three paths as the 1/1:
349
- | Path | Flag / command |
350
- |---|---|
351
- | **Mint all at deploy** (to yourself) | `--mint-all` |
352
- | **Mint some now, defer the rest** | `--mint-count N` |
353
- | **Deferred** *(default)* | `--no-mint`, then `abx mint <addr>` (next in order) · `--count N` · `--to <buyer>` |
354
-
355
- Pre-mint warming covers every unminted id within the cap, so `--no-mint` → `serve`/`add --remote` → `mint` keeps marketplaces from caching blanks.
356
-
357
- **Pause (safety switch).** A Series **deploys paused**: while paused only the *owner* mints (reserves/config), minter + public blocked on-chain. Configure, mint reserves, then `abx unpause <addr>` to open; `abx pause <addr>` re-closes. Deploy open with `--unpaused`. (`--mint-all`/`--mint-count` run at birth, outside the gate.)
358
- - **Minted the full supply to yourself (`--mint-all`)? The drop is COMPLETE — do not offer `unpause`.** Fixed supply is exhausted; unpausing does nothing. `abx state <addr>` prints `complete` when sold out — check it before suggesting any mint-related next step.
359
-
360
- **Delegate minting + supply.** `abx set-minter <addr> --minter 0x…` authorizes a **single** external minting contract (a new one replaces the old; `--minter none` clears to owner-only — point at a router for multiple mechanics). `abx set-primary-payee <addr> --payee 0x…` sets where sale proceeds go. `abx set-max-invocations <addr> --max N` only ever **lowers** the cap.
361
-
362
- ## Selling + a buyer page
363
-
364
- Both already documented in [operating.md](operating.md) — a code project sells exactly like a Series:
365
- - **Sell it — the shared fixed-price minter** → [operating.md § Selling](operating.md#selling--the-shared-fixed-price-minter). `abx minter configure` → `set-minter` → `set-primary-payee` → `unpause`. You run all of it.
366
- - **Give buyers a page** → [operating.md § mint-page](operating.md#a-mint-website-for-buyers--abx-mint-page). `abx mint-page <addr>` scaffolds a self-contained Next.js mint site; deploy to Vercel.
367
-
368
- Never bake a `localhost` resolver URL on-chain as a "real" deploy — it resolves for no one; the CLI refuses it. Use `--onchain-uri`/`--image-base` for no-server, or a real public resolver domain.