@artblocks/abx-cli 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (148) hide show
  1. package/CHANGELOG.md +3663 -0
  2. package/assets/renderer-scaffold/README.md +2 -2
  3. package/assets/renderer-scaffold/src/MyRenderer.sol +2 -2
  4. package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +1 -1
  5. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +17 -0
  6. package/assets/renderer-scaffold/test/MyRenderer.t.sol +51 -2
  7. package/dist/commands/deploy.d.ts +242 -0
  8. package/dist/commands/deploy.d.ts.map +1 -0
  9. package/dist/commands/deploy.js +4764 -0
  10. package/dist/commands/deploy.js.map +1 -0
  11. package/dist/commands/project.d.ts +257 -0
  12. package/dist/commands/project.d.ts.map +1 -0
  13. package/dist/commands/project.js +1299 -0
  14. package/dist/commands/project.js.map +1 -0
  15. package/dist/commands/reads.d.ts +64 -0
  16. package/dist/commands/reads.d.ts.map +1 -0
  17. package/dist/commands/reads.js +701 -0
  18. package/dist/commands/reads.js.map +1 -0
  19. package/dist/commands/scaffold.d.ts +73 -0
  20. package/dist/commands/scaffold.d.ts.map +1 -0
  21. package/dist/commands/scaffold.js +674 -0
  22. package/dist/commands/scaffold.js.map +1 -0
  23. package/dist/commands/service.d.ts +41 -0
  24. package/dist/commands/service.d.ts.map +1 -0
  25. package/dist/commands/service.js +618 -0
  26. package/dist/commands/service.js.map +1 -0
  27. package/dist/commands/storage.d.ts +51 -0
  28. package/dist/commands/storage.d.ts.map +1 -0
  29. package/dist/commands/storage.js +370 -0
  30. package/dist/commands/storage.js.map +1 -0
  31. package/dist/commands/submit-app.d.ts +58 -0
  32. package/dist/commands/submit-app.d.ts.map +1 -0
  33. package/dist/commands/submit-app.js +512 -0
  34. package/dist/commands/submit-app.js.map +1 -0
  35. package/dist/config.d.ts +90 -2
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +285 -11
  38. package/dist/config.js.map +1 -1
  39. package/dist/conformance.d.ts +31 -0
  40. package/dist/conformance.d.ts.map +1 -0
  41. package/dist/conformance.js +393 -0
  42. package/dist/conformance.js.map +1 -0
  43. package/dist/deps.d.ts +6 -39
  44. package/dist/deps.d.ts.map +1 -1
  45. package/dist/deps.js +4 -68
  46. package/dist/deps.js.map +1 -1
  47. package/dist/errors.d.ts +20 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +25 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/flag-allowlists.d.ts +53 -0
  52. package/dist/flag-allowlists.d.ts.map +1 -0
  53. package/dist/flag-allowlists.js +157 -0
  54. package/dist/flag-allowlists.js.map +1 -0
  55. package/dist/flags.d.ts +45 -0
  56. package/dist/flags.d.ts.map +1 -1
  57. package/dist/flags.js +117 -1
  58. package/dist/flags.js.map +1 -1
  59. package/dist/jsonout.d.ts +37 -0
  60. package/dist/jsonout.d.ts.map +1 -0
  61. package/dist/jsonout.js +68 -0
  62. package/dist/jsonout.js.map +1 -0
  63. package/dist/kind.d.ts +61 -0
  64. package/dist/kind.d.ts.map +1 -0
  65. package/dist/kind.js +111 -0
  66. package/dist/kind.js.map +1 -0
  67. package/dist/main.js +699 -4736
  68. package/dist/main.js.map +1 -1
  69. package/dist/mintpage.d.ts +17 -2
  70. package/dist/mintpage.d.ts.map +1 -1
  71. package/dist/mintpage.js +241 -54
  72. package/dist/mintpage.js.map +1 -1
  73. package/dist/output.d.ts +179 -0
  74. package/dist/output.d.ts.map +1 -0
  75. package/dist/output.js +756 -0
  76. package/dist/output.js.map +1 -0
  77. package/dist/ownerops.d.ts +302 -51
  78. package/dist/ownerops.d.ts.map +1 -1
  79. package/dist/ownerops.js +1780 -357
  80. package/dist/ownerops.js.map +1 -1
  81. package/dist/preview.d.ts +81 -0
  82. package/dist/preview.d.ts.map +1 -0
  83. package/dist/preview.js +535 -0
  84. package/dist/preview.js.map +1 -0
  85. package/dist/prompt.d.ts +17 -0
  86. package/dist/prompt.d.ts.map +1 -0
  87. package/dist/prompt.js +19 -0
  88. package/dist/prompt.js.map +1 -0
  89. package/dist/provision.d.ts.map +1 -1
  90. package/dist/provision.js +16 -8
  91. package/dist/provision.js.map +1 -1
  92. package/dist/remote.d.ts +153 -52
  93. package/dist/remote.d.ts.map +1 -1
  94. package/dist/remote.js +406 -46
  95. package/dist/remote.js.map +1 -1
  96. package/dist/riskgate.d.ts +58 -0
  97. package/dist/riskgate.d.ts.map +1 -0
  98. package/dist/riskgate.js +212 -0
  99. package/dist/riskgate.js.map +1 -0
  100. package/dist/scaffold.d.ts +10 -0
  101. package/dist/scaffold.d.ts.map +1 -0
  102. package/dist/scaffold.js +52 -0
  103. package/dist/scaffold.js.map +1 -0
  104. package/dist/schema.d.ts +36 -1
  105. package/dist/schema.d.ts.map +1 -1
  106. package/dist/schema.js +121 -26
  107. package/dist/schema.js.map +1 -1
  108. package/dist/script-chunks.d.ts +8 -0
  109. package/dist/script-chunks.d.ts.map +1 -0
  110. package/dist/script-chunks.js +35 -0
  111. package/dist/script-chunks.js.map +1 -0
  112. package/dist/served.d.ts +30 -0
  113. package/dist/served.d.ts.map +1 -0
  114. package/dist/served.js +112 -0
  115. package/dist/served.js.map +1 -0
  116. package/dist/signer.d.ts +13 -0
  117. package/dist/signer.d.ts.map +1 -1
  118. package/dist/signer.js +84 -15
  119. package/dist/signer.js.map +1 -1
  120. package/dist/update-check.d.ts +83 -4
  121. package/dist/update-check.d.ts.map +1 -1
  122. package/dist/update-check.js +140 -19
  123. package/dist/update-check.js.map +1 -1
  124. package/package.json +10 -6
  125. package/skill/SKILL.md +350 -150
  126. package/skill/reference/code-projects.md +183 -26
  127. package/skill/reference/creator-token.md +99 -0
  128. package/skill/reference/decisions.md +174 -0
  129. package/skill/reference/hosting.md +63 -19
  130. package/skill/reference/operating.md +116 -12
  131. package/skill/reference/setup.md +55 -6
  132. package/skill/reference/troubleshooting.md +42 -5
  133. package/dist/inspect.d.ts +0 -48
  134. package/dist/inspect.d.ts.map +0 -1
  135. package/dist/inspect.js +0 -184
  136. package/dist/inspect.js.map +0 -1
  137. package/dist/migrate.d.ts +0 -65
  138. package/dist/migrate.d.ts.map +0 -1
  139. package/dist/migrate.js +0 -180
  140. package/dist/migrate.js.map +0 -1
  141. package/dist/onchain-uri.d.ts +0 -97
  142. package/dist/onchain-uri.d.ts.map +0 -1
  143. package/dist/onchain-uri.js +0 -243
  144. package/dist/onchain-uri.js.map +0 -1
  145. package/dist/upload.d.ts +0 -28
  146. package/dist/upload.d.ts.map +0 -1
  147. package/dist/upload.js +0 -41
  148. package/dist/upload.js.map +0 -1
@@ -25,7 +25,7 @@ When a creator arrives with an *idea* and you write the program, it must read it
25
25
  var seed = td.seed || '0x1'; // mint-time seed (hex)
26
26
  var palette = td.palette || '#3355ff'; // a PostParam: --schema palette:HexColor:TokenOwner
27
27
 
28
- var z = 0; // seed a PRNG deterministically (same seed → same art)
28
+ var z = 0; // seed a PRNG deterministically (same seed → same output)
29
29
  for (var i = 2; i < seed.length; i++) z = (z * 16 + (parseInt(seed[i], 16) || 0)) % 4294967296;
30
30
  function rnd() { z = (1664525 * z + 1013904223) % 4294967296; return z / 4294967296; }
31
31
 
@@ -45,9 +45,74 @@ When a creator arrives with an *idea* and you write the program, it must read it
45
45
 
46
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
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
+
48
113
  ## What a code project requires you to run — and keep running (say this up front)
49
114
 
50
- A code project's art 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:
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:
51
116
 
52
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`.)
53
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).
@@ -70,51 +135,84 @@ abx verify <addr> --remote http://localhost:<port> # confirm: renders up
70
135
 
71
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).
72
137
 
73
- **Wiring the hooks — `abx set-param-hooks <addr>` (SeriesCode only, owner-only).** A SeriesCode 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 lifecycle call, best-effort a revert there never blocks the transfer). 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).
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.
74
152
 
75
153
  ## `--onchain-uri` — the chain-complete lane (internals)
76
154
 
77
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.
78
156
 
79
- **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 art. 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.
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.
80
158
 
81
- **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 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.
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.
82
160
 
83
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.
84
- - **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 configure-param <addr> - display.gateway <prefix>`), permanence rides the pin/endowment, params ride the URL (**8KB budget** — `abx verify` reports `urlOverBudget`; big params ⇒ prefer template mode).
85
- - **`params.keys` is auto-managed**: the deploy writes this contract param (schema keys itself, sorted CSV) so the generator's on-chain `tokenData` carries the full param surface, byte-aligned with the resolver's. Add a param key another way and `abx configure-param` prints the exact fix when the CSV drifts.
86
- - **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. `abx tokenuri <addr>` is the quick raw read.
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/
87
167
 
88
168
  ### PostParam schema — the Type + Auth catalog
89
169
 
90
- A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:TokenOwner,speed:Uint256Range[0..100]:Artist`). The Type token carries an optional **bracket suffix**: a **`Select` MUST list its options** (`Select[Spring|Summer|Autumn|Winter]`, pipe-delimited — a Select with no options is rejected, because the on-chain schema requires them), and a **Range MAY carry bounds** (`Uint256Range[0..100]`, `Int256Range[-50..50]`, `DecimalRange[0..1]`, `Timestamp[2026-01-01..2026-12-31]` — omit for unbounded). The delimiters never collide (params `,` · fields `:` · options `|` · bounds `..`, all inside `[…]`). Declared at **deploy** (`--schema`); set later per token with `abx configure-param <addr> <id> <key> <value>` (value canonically encoded per Type — you pass the human form; a Select takes a label or its index). Adding a schema to an already-deployed contract isn't a CLI command today; declare params at deploy.
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.
91
185
 
92
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).
93
- - **Auth — who may set the param:** `Artist` (the contract owner) · `TokenOwner` (the token's current holder; **delegate.xyz honored**) · `Address` (a specific named address) · and the `Or` combinations `ArtistOrTokenOwner` · `ArtistOrAddress` · `TokenOwnerOrAddress` · `ArtistOrTokenOwnerOrAddress`. The chain enforces it — a wrong signer reverts. (There is **no** "anyone" leg a param is always artist / token-owner / a named address.)
94
- - Examples: a collector-tunable color `palette:HexColor:TokenOwner`; a collector-chosen mood `mood:Select[Calm|Wild|Chaotic]:TokenOwner`; an artist-only bounded dial `speed:Uint256Range[1..10]:Artist`; an on/off toggle `invert:Bool:TokenOwner`.
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
95
192
 
96
193
  ## `--image-base` — deterministic S3/CDN thumbnail URLs (no metadata resolver)
97
194
 
98
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.**
99
196
 
100
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."
101
- - **Use a mutable, path-addressed host** — S3 / R2 / a CDN (`ABX_STORAGE_BACKEND=cloud`). **NOT ipfs/arweave** for this lane: content-addressed the URL changes with the bytes you'd rewrite chain state on every re-render. The whole point is a URL that never changes while the pixels do.
102
- - **Provisioning the EXACT vars (don't guess these; a real agent invented `R2_*` names abx never reads and broke the upload):** the runner uploads via `ABX_S3_ENDPOINT` · `ABX_S3_BUCKET` · `ABX_S3_ACCESS_KEY_ID` · `ABX_S3_SECRET_ACCESS_KEY` (region `ABX_S3_REGION`, default `auto`), and serves the still from `ABX_S3_PUBLIC_BASE` — **which must equal your `--image-base`.** `abx storage show` prints what's resolved. **The `--image-base`/`ABX_S3_PUBLIC_BASE` is the PUBLIC READ url, NOT the S3 API endpoint** — the two are different hosts. **R2 gotcha:** `https://<acct>.r2.cloudflarestorage.com` is the *API endpoint* (auth-only, goes in `ABX_S3_ENDPOINT`); the public URL is a **`https://pub-<hash>.r2.dev`** you enable in the R2 dashboard, or a custom domain. (AWS S3: a public-read bucket or a CloudFront domain the bare `s3….amazonaws.com` host is public only if you made the bucket public.)
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)).
103
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.
104
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.
105
202
 
106
203
  ## In-chain Solidity SVG — the zero-dependency lane
107
204
 
108
- The purest form: the artwork 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**.
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**.
109
206
 
110
207
  ```bash
111
208
  abx deploy-code --image-renderer 0x<svgRenderer> [--attributes-renderer 0x<traitsRenderer>] \
112
209
  --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol … [--dep none]
113
210
  ```
114
211
 
115
- - **No `--script`/`--code-dir`.** `deploy-code` allows a renderer-only project (neither program mode) as long as `--image-renderer` (and/or `--attributes-renderer`) is set. There is no `animation_url` — the SVG `image` *is* the artwork; 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`).
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.
116
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.
117
- - **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 art, 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/.
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/.
118
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`.
119
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.
120
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.
@@ -127,8 +225,10 @@ The renderer is the creator's own Solidity (compiled + deployed with forge — t
127
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.
128
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.
129
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).
130
- - **Read params LIVE via `IAbxParams(token)`** (`tokenParam`/`contractParam`, token scope wins) — so a collector's `configure-param` shows up on the next read with no redeploy. Never bake a param value in at deploy.
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.
131
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).
132
232
 
133
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.
134
234
 
@@ -159,16 +259,72 @@ abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
159
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).
160
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).
161
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.)
162
- - **Seeds** — drawn at mint from the canonical `AbxSeedSource` (settled once assigned; `--no-seed` opts out; curated pre-set seeds win).
163
- - **`--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; `String`/`Bytes` via `--file`); 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.
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.
164
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.
165
- - **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).
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`.
166
322
 
167
323
  ## Stills + traits — the render effect
168
324
 
169
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`).
170
326
 
171
- - **`abx render <addr> <id> --force`** re-renders a still that already exists — the fix for a **bad / blank / timed-out capture**. The art 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 art looks (to change appearance, change an input — the `palette`/etc. PostParam via `abx configure-param`, which re-addresses the render).
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).
172
328
  - A render that hits an unavailable/erroring live view **fails loudly and stores nothing** (no garbage thumbnail).
173
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.
174
330
  - Script-reported traits (`abx.traits({...})`) stitch into `attributes` (on-chain wins > render > operator).
@@ -176,17 +332,18 @@ The render effect is the **ONLY source of a real thumbnail** — skip it and `im
176
332
  ## Verify it actually resolves — before you tell the creator it's live
177
333
 
178
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:
179
- 1. `abx tokenuri <addr>` → shows the **resolver base**, never `localhost` (or, on `--onchain-uri`, decodes straight from the contract). **⚠ On `--onchain-uri`, `tokenURI` reverting on Etherscan's "Read Contract" is usually NOT a broken token — an unauthenticated public read has a low gas cap, and a large on-chain document assembles per call and exceeds it. The user hits this constantly. Correct diagnosis: it's a client gas cap, NOT indexing lag. It works via `abx tokenuri`, a wallet-connected read, or a high-gas RPC. Do NOT blame the RPC's index, do NOT run `abx index --full` (there is no index in the on-chain path), do NOT redeploy.**
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).
180
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.
181
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.
182
- 3. the live view `/a/<chainId>/<addr>/0` loads. **`{"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).
183
- 4. `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.
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.
184
341
 
185
- **⚠ 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 `curl -sIL https://arweave.net/<manifestTxid>/index.html` (200 = ready). IPFS directory content is renderable right after the pin.
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.
186
343
 
187
344
  ## Mint order, timing, pause, supply
188
345
 
189
- **Metadata is the token id.** Tokens mint **in order** (`0,1,2,…`); token 3 shows artwork 3. No token-id ↔ metadata-id decoupling — to shuffle or sell specific tokens, pre-mint then trade.
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.
190
347
 
191
348
  **Mint timing** — same three paths as the 1/1:
192
349
  | Path | Flag / command |
@@ -0,0 +1,99 @@
1
+ # Royalty enforcement — opt-in creator token (ERC-721C / ERC-1155C)
2
+
3
+ [← back to SKILL.md](../SKILL.md)
4
+
5
+ **Read this ONLY because the creator brought it up** (royalty enforcement, "make OpenSea pay my
6
+ royalties", ERC-721C/ERC-1155C, transfer restrictions, a transfer validator). If they didn't, this file
7
+ has no business in the conversation: **plain ERC-721 (or plain ERC-1155, for an edition) is the default
8
+ and the recommendation**, every deploy flow already does the right thing with zero `--721c` flags, and
9
+ offering enforcement unprompted reads as a nudge toward a restricted asset. Never present `--721c` as a
10
+ checklist item, a "should we also…", or a row in the deploy readout for a creator who never asked.
11
+
12
+ **Same flag, both standards.** `--721c` is the one flag for creator-token enrollment everywhere — on a
13
+ 721 deploy (`deploy`/`deploy-series`/`deploy-code` with no `--copies`) it enrolls **ERC-721C**; on the
14
+ same commands with `--copies` (an edition) it enrolls **ERC-1155C** instead. Same validator registry,
15
+ same recommended validator, same permanence rule, same UX — only the ERC-165 discovery ids and the
16
+ per-transfer call differ, both invisible to a creator (below).
17
+
18
+ ## The stance (how to frame it when asked)
19
+
20
+ - **Plain ERC-721 — or plain ERC-1155 for an edition — is the more sovereign asset (the default)**:
21
+ no third-party contract on the transfer path, tradeable everywhere, nothing anyone — creator
22
+ included — can later switch off. ABX royalties are already on-chain via ERC-2981
23
+ (`abx set-royalty`); marketplaces *choose* whether to honor them.
24
+ - **721C/1155C is enforcement for creators who specifically want it**: transfers are checked by a
25
+ validator contract, and sales that don't route through an authorized venue revert. It is a real
26
+ trade-off, decided at deploy, **permanently** — present it transparently, recommend it only when
27
+ the creator's stated goal is enforcement, and get an explicit yes before enrolling.
28
+ - **The honest market picture (2026) — say it, don't oversell**: enforcement via 721C effectively
29
+ means **OpenSea** plus venues on Limit Break's Payment Processor. OpenSea's own help docs say
30
+ enforcement limits sales to those venues; Magic Eden's EVM marketplace shut down March 2026, and
31
+ Blur never honored 721C. The trade: enforced royalties on OpenSea, in exchange for not trading on
32
+ venues the validator doesn't authorize.
33
+
34
+ ## What enrolling does (the facts to relay)
35
+
36
+ - **Deploy-time, permanent, both directions.** A plain ABX token (721 or 1155) can **never** grow
37
+ transfer restrictions later (that's a feature — collectors of unenrolled tokens keep that promise
38
+ forever), and an enrolled one never sheds the standard. No retrofit exists; the only path to
39
+ enrollment for a live plain collection is a redeploy.
40
+ - **Suspend ≠ un-enroll.** The owner can re-point the validator or set it to zero (enforcement off,
41
+ token **stays** enrolled — the owner can re-enable any time). Collectors can see enrollment via
42
+ ERC-165, suspended or not.
43
+ - **Mints and burns are NEVER validated** — no validator policy can brick minting, the shared
44
+ fixed-price minter (721 or edition), or a delegated minter.
45
+ - **The recommended validator** (`--721c recommended`, or bare `--721c`) is OpenSea's
46
+ StrictAuthorizedTransferSecurityRegistry — the same registry, same address, for both standards.
47
+ Under it, owner-initiated wallet-to-wallet transfers pass; non-authorized operators revert. One
48
+ decision, done.
49
+ - **On an edition, validation is per `(id, amount)`, not per token.** ERC-1155C's validator call
50
+ carries the transferred amount and isn't a view (unlike 721C's), and a batch transfer loops one
51
+ validator call per id — invisible to a creator, but the reason a large batch transfer on an
52
+ enrolled edition costs more gas than an unenrolled one.
53
+
54
+ ## Commands
55
+
56
+ ```bash
57
+ # Enroll at deploy — all three deploy commands take the flag (absent = plain, forever):
58
+ abx deploy|deploy-series|deploy-code … --721c [recommended|0x<validator>]
59
+ # On an edition deploy (--copies), the SAME flag enrolls ERC-1155C instead — nothing else changes.
60
+ abx deploy --copies <n|open> … --721c [recommended|0x<validator>]
61
+
62
+ # Operate an ENROLLED collection (owner-only, any signing lane, --dry-run works):
63
+ abx set-transfer-validator <addr> 0x<validator> # re-point enforcement
64
+ abx set-transfer-validator <addr> none # suspend (stays enrolled)
65
+ abx set-transfer-validator <addr> recommended # the chain's recommended validator
66
+
67
+ abx state <addr> # shows a "721C validator" row ONLY when enrolled ("suspended" when zero);
68
+ # an enrolled EDITION shows the same row labeled "1155C validator" — same
69
+ # ERC-165 id + getter on both standards, so detection is identical
70
+ ```
71
+
72
+ Enrolling prints one plain statement of what enforcement means — relay it, don't embellish. An
73
+ un-enrolled deploy prints nothing about 721C/1155C.
74
+
75
+ **One exception to "owner-only", worth knowing before you tell a collector they're stuck:** once a
76
+ collection's `owner()` is the zero address (renounced), **anyone** may send the `none` (suspend) form,
77
+ and **nobody** may ever arm a validator again. It's the release valve for an abandoned collection whose
78
+ validator reverts every transfer — the only thing a stranger can do is make the tokens transferable, never
79
+ the reverse. So a creator who plans to renounce should hear the trade: "renounced **and** still enforcing
80
+ royalties forever" is not a state this protocol offers.
81
+
82
+ ## Refusals the CLI enforces (don't fight them; relay the reason)
83
+
84
+ - **`--721c none`/zero at deploy** — refused: a zero validator never enrolls, and "enrolled but
85
+ suspended" is not a deploy-time state. Plain (721 or 1155) is already the default; drop the flag.
86
+ - **`--721c recommended` on a chain with no known validator** — refused, naming the chains that have
87
+ one. Pass an explicit deployed validator address instead; never guess one.
88
+ - **A validator address that isn't a validator** — refused before any gas at deploy AND at re-point
89
+ (the contract reverts `InvalidTransferValidator()`); a mixed-case address must pass its EIP-55
90
+ checksum. **Two shapes are refused, not one:** no code at all, and an address that answers *any*
91
+ call successfully — a **Safe**, an uninitialised proxy, or a 7702-delegated EOA. `validateTransfer`
92
+ returns nothing, so there's no decode to fail and such an address would enforce nothing while
93
+ `getTransferValidator()` and ERC-165 both reported enforcement as ON. If a creator offers their Safe
94
+ address, that's the refusal you'll get — relay it as "that's a wallet, not a validator", and point at
95
+ `recommended`. A real deploy also refuses to enroll blind when the RPC can't answer (a dry-run
96
+ defers it).
97
+ - **`set-transfer-validator` on a plain, unenrolled collection** — refused up front (721 or 1155):
98
+ enrollment is a deploy-time decision and this collection didn't enroll. Do not retry with different
99
+ flags; if the creator truly needs enforcement, that's a redeploy conversation.