@artblocks/abx-cli 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (137) hide show
  1. package/CHANGELOG.md +1991 -0
  2. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +17 -0
  3. package/assets/renderer-scaffold/test/MyRenderer.t.sol +51 -2
  4. package/dist/commands/deploy.d.ts +206 -0
  5. package/dist/commands/deploy.d.ts.map +1 -0
  6. package/dist/commands/deploy.js +4273 -0
  7. package/dist/commands/deploy.js.map +1 -0
  8. package/dist/commands/project.d.ts +241 -0
  9. package/dist/commands/project.d.ts.map +1 -0
  10. package/dist/commands/project.js +1060 -0
  11. package/dist/commands/project.js.map +1 -0
  12. package/dist/commands/reads.d.ts +64 -0
  13. package/dist/commands/reads.d.ts.map +1 -0
  14. package/dist/commands/reads.js +697 -0
  15. package/dist/commands/reads.js.map +1 -0
  16. package/dist/commands/scaffold.d.ts +71 -0
  17. package/dist/commands/scaffold.d.ts.map +1 -0
  18. package/dist/commands/scaffold.js +552 -0
  19. package/dist/commands/scaffold.js.map +1 -0
  20. package/dist/commands/service.d.ts +41 -0
  21. package/dist/commands/service.d.ts.map +1 -0
  22. package/dist/commands/service.js +618 -0
  23. package/dist/commands/service.js.map +1 -0
  24. package/dist/commands/storage.d.ts +37 -0
  25. package/dist/commands/storage.d.ts.map +1 -0
  26. package/dist/commands/storage.js +343 -0
  27. package/dist/commands/storage.js.map +1 -0
  28. package/dist/config.d.ts +91 -2
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +244 -11
  31. package/dist/config.js.map +1 -1
  32. package/dist/conformance.d.ts +31 -0
  33. package/dist/conformance.d.ts.map +1 -0
  34. package/dist/conformance.js +371 -0
  35. package/dist/conformance.js.map +1 -0
  36. package/dist/deps.d.ts +6 -39
  37. package/dist/deps.d.ts.map +1 -1
  38. package/dist/deps.js +4 -68
  39. package/dist/deps.js.map +1 -1
  40. package/dist/errors.d.ts +20 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/errors.js +25 -0
  43. package/dist/errors.js.map +1 -0
  44. package/dist/flag-allowlists.d.ts +53 -0
  45. package/dist/flag-allowlists.d.ts.map +1 -0
  46. package/dist/flag-allowlists.js +148 -0
  47. package/dist/flag-allowlists.js.map +1 -0
  48. package/dist/flags.d.ts +41 -0
  49. package/dist/flags.d.ts.map +1 -1
  50. package/dist/flags.js +94 -1
  51. package/dist/flags.js.map +1 -1
  52. package/dist/jsonout.d.ts +37 -0
  53. package/dist/jsonout.d.ts.map +1 -0
  54. package/dist/jsonout.js +68 -0
  55. package/dist/jsonout.js.map +1 -0
  56. package/dist/kind.d.ts +53 -0
  57. package/dist/kind.d.ts.map +1 -0
  58. package/dist/kind.js +94 -0
  59. package/dist/kind.js.map +1 -0
  60. package/dist/main.js +534 -4708
  61. package/dist/main.js.map +1 -1
  62. package/dist/mintpage.d.ts +17 -2
  63. package/dist/mintpage.d.ts.map +1 -1
  64. package/dist/mintpage.js +214 -52
  65. package/dist/mintpage.js.map +1 -1
  66. package/dist/output.d.ts +148 -0
  67. package/dist/output.d.ts.map +1 -0
  68. package/dist/output.js +634 -0
  69. package/dist/output.js.map +1 -0
  70. package/dist/ownerops.d.ts +132 -44
  71. package/dist/ownerops.d.ts.map +1 -1
  72. package/dist/ownerops.js +1151 -329
  73. package/dist/ownerops.js.map +1 -1
  74. package/dist/preview.d.ts +81 -0
  75. package/dist/preview.d.ts.map +1 -0
  76. package/dist/preview.js +535 -0
  77. package/dist/preview.js.map +1 -0
  78. package/dist/prompt.d.ts +17 -0
  79. package/dist/prompt.d.ts.map +1 -0
  80. package/dist/prompt.js +19 -0
  81. package/dist/prompt.js.map +1 -0
  82. package/dist/provision.d.ts.map +1 -1
  83. package/dist/provision.js +10 -7
  84. package/dist/provision.js.map +1 -1
  85. package/dist/remote.d.ts +134 -53
  86. package/dist/remote.d.ts.map +1 -1
  87. package/dist/remote.js +358 -46
  88. package/dist/remote.js.map +1 -1
  89. package/dist/riskgate.d.ts +29 -0
  90. package/dist/riskgate.d.ts.map +1 -0
  91. package/dist/riskgate.js +91 -0
  92. package/dist/riskgate.js.map +1 -0
  93. package/dist/scaffold.d.ts +10 -0
  94. package/dist/scaffold.d.ts.map +1 -0
  95. package/dist/scaffold.js +52 -0
  96. package/dist/scaffold.js.map +1 -0
  97. package/dist/schema.d.ts +18 -1
  98. package/dist/schema.d.ts.map +1 -1
  99. package/dist/schema.js +74 -23
  100. package/dist/schema.js.map +1 -1
  101. package/dist/served.d.ts +30 -0
  102. package/dist/served.d.ts.map +1 -0
  103. package/dist/served.js +112 -0
  104. package/dist/served.js.map +1 -0
  105. package/dist/signer.d.ts +13 -0
  106. package/dist/signer.d.ts.map +1 -1
  107. package/dist/signer.js +80 -14
  108. package/dist/signer.js.map +1 -1
  109. package/dist/update-check.d.ts +83 -4
  110. package/dist/update-check.d.ts.map +1 -1
  111. package/dist/update-check.js +132 -17
  112. package/dist/update-check.js.map +1 -1
  113. package/package.json +10 -6
  114. package/skill/SKILL.md +108 -62
  115. package/skill/reference/code-projects.md +109 -13
  116. package/skill/reference/creator-token.md +86 -0
  117. package/skill/reference/decisions.md +87 -0
  118. package/skill/reference/hosting.md +48 -15
  119. package/skill/reference/operating.md +32 -8
  120. package/skill/reference/setup.md +10 -3
  121. package/skill/reference/troubleshooting.md +31 -4
  122. package/dist/inspect.d.ts +0 -48
  123. package/dist/inspect.d.ts.map +0 -1
  124. package/dist/inspect.js +0 -184
  125. package/dist/inspect.js.map +0 -1
  126. package/dist/migrate.d.ts +0 -65
  127. package/dist/migrate.d.ts.map +0 -1
  128. package/dist/migrate.js +0 -180
  129. package/dist/migrate.js.map +0 -1
  130. package/dist/onchain-uri.d.ts +0 -97
  131. package/dist/onchain-uri.d.ts.map +0 -1
  132. package/dist/onchain-uri.js +0 -243
  133. package/dist/onchain-uri.js.map +0 -1
  134. package/dist/upload.d.ts +0 -28
  135. package/dist/upload.d.ts.map +0 -1
  136. package/dist/upload.js +0 -41
  137. package/dist/upload.js.map +0 -1
@@ -45,6 +45,71 @@ 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 art 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 art 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 art 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 art is settled.
112
+
48
113
  ## What a code project requires you to run — and keep running (say this up front)
49
114
 
50
115
  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:
@@ -82,15 +147,22 @@ Two kinds of inputs feed a piece: **settled state** (explicit PostParams, the se
82
147
 
83
148
  - **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
149
  - **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.
150
+ - **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.)
151
+ - **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 — and the cheapest proof a param really landed on-chain: look for its key under **`abx_params`** in that decoded JSON, no resolver anywhere.
87
152
 
88
153
  ### PostParam schema — the Type + Auth catalog
89
154
 
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.
155
+ 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).
156
+
157
+ **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.
158
+
159
+ **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 shows up in the token's `tokenURI` under `abx_params`. So a project's configure UI can be built from the chain alone, and a collector's write is visible metadata the moment it lands — no resolver, no indexer.
160
+
161
+ **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
162
 
92
163
  - **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.)
164
+ - **Auth — who may set the param:** `Artist` (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 `ArtistOrTokenOwner` · `ArtistOrAddress` · `TokenOwnerOrAddress` · `ArtistOrTokenOwnerOrAddress`. 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.
165
+ - **`:lock=<when>` — an optional 4th field** that freezes the VALUE 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>`. It stops all further writes forever; 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.
94
166
  - 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`.
95
167
 
96
168
  ## `--image-base` — deterministic S3/CDN thumbnail URLs (no metadata resolver)
@@ -98,8 +170,8 @@ A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:
98
170
  `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
171
 
100
172
  - **⚠ 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.)
173
+ - **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.
174
+ - **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
175
  - **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
176
  - **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
177
 
@@ -112,7 +184,8 @@ abx deploy-code --image-renderer 0x<svgRenderer> [--attributes-renderer 0x<trait
112
184
  --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol … [--dep none]
113
185
  ```
114
186
 
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`).
187
+ - **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 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`).
188
+ - **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
189
  - **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
190
  - **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/.
118
191
  - **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`.
@@ -127,7 +200,7 @@ The renderer is the creator's own Solidity (compiled + deployed with forge — t
127
200
  - **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
201
  - **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
202
  - **`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.
203
+ - **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
204
  - **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.
132
205
 
133
206
  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.
@@ -160,10 +233,32 @@ abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
160
233
  - **`--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
234
  - **`--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
235
  - **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.
236
+ - **`--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
237
  - **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
238
  - **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).
166
239
 
240
+ ## `--copies` — a generative drop sold as an EDITION (EditionCode), and what v1 gives up
241
+
242
+ `abx deploy-code --copies <n|open>` swaps the 721 **SeriesCode** for its ERC-1155 twin **EditionCode**: N ids, each a distinct artwork/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).
243
+
244
+ **Source is `--script` only; on-chain dependencies DO work. The CLI refuses the rest rather than silently dropping them:**
245
+
246
+ | Flag | With `--copies` |
247
+ |---|---|
248
+ | `--script <file>` | ✅ the only supported source |
249
+ | `--dep <ref>` / `--dep-registry` | ✅ **works** — same ordered refs as the 721 lane (index 0 = the runtime) |
250
+ | `--code-dir <dir>` | ❌ refused — *"use `--script` (on-chain chunks) instead"* |
251
+ | `--image-renderer` / `--attributes-renderer` / `--image-base` | ❌ all three refused together |
252
+
253
+ 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 proven on-chain bytes; no server, gateway, or CDN in the graph"*.
254
+
255
+ Two things still to watch on this lane:
256
+
257
+ 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.
258
+ 2. **A thumbnail is still rendered off-chain** by the effect runner, so a code edition still needs a public home for its stills even when `tokenURI` is fully on-chain. `abx verify` will say the image is a placeholder until you render one.
259
+
260
+ Everything else about a code project is unchanged by `--copies`.
261
+
167
262
  ## Stills + traits — the render effect
168
263
 
169
264
  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`).
@@ -176,13 +271,14 @@ The render effect is the **ONLY source of a real thumbnail** — skip it and `im
176
271
  ## Verify it actually resolves — before you tell the creator it's live
177
272
 
178
273
  **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.**
274
+ 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
275
  - **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
276
  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.
277
+ 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).
278
+ 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.
279
+ 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
280
 
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.
281
+ **⚠ 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
282
 
187
283
  ## Mint order, timing, pause, supply
188
284
 
@@ -0,0 +1,86 @@
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
+ ## Refusals the CLI enforces (don't fight them; relay the reason)
76
+
77
+ - **`--721c none`/zero at deploy** — refused: a zero validator never enrolls, and "enrolled but
78
+ suspended" is not a deploy-time state. Plain (721 or 1155) is already the default; drop the flag.
79
+ - **`--721c recommended` on a chain with no known validator** — refused, naming the chains that have
80
+ one. Pass an explicit deployed validator address instead; never guess one.
81
+ - **A codeless validator address** — refused before any gas at deploy AND at re-point (the contract
82
+ would revert `InvalidTransferValidator()`); a mixed-case address must pass its EIP-55 checksum.
83
+ A real deploy also refuses to enroll blind when the RPC can't verify code (a dry-run defers it).
84
+ - **`set-transfer-validator` on a plain, unenrolled collection** — refused up front (721 or 1155):
85
+ enrollment is a deploy-time decision and this collection didn't enroll. Do not retry with different
86
+ flags; if the creator truly needs enforcement, that's a redeploy conversation.
@@ -0,0 +1,87 @@
1
+ # Decisions — configuring a real launch
2
+
3
+ [← back to SKILL.md](../SKILL.md#decisions-real-launch)
4
+
5
+ The six decisions a real launch actually turns on. SKILL.md carries the router and the rules that
6
+ prevent an irreversible mistake; this is the depth — read it once you are configuring a real deploy,
7
+ not while you are still working out what the creator wants.
8
+
9
+ Master call is **custody × mutability**:
10
+
11
+ | | **Mutable** (name/desc/traits may change) | **Immutable** (never changes) |
12
+ |---|---|---|
13
+ | **Tiny static** (≲ 24 KB/file, ≲ 256 KB total) | **on-chain renderer** — `--onchain-image --compress fastlz`. No host, mutable via `set-field`, permanent. | on-chain renderer + `lock-field` + `lock-uri` once it resolves. |
14
+ | **Bigger / dynamic** (most PNG/JPEG) | **image off-chain, JSON on-chain, no server** — `--onchain-uri --backend arweave` (or `ipfs`). Renderer assembles JSON pointing at the bytes; many files → one `url-template` (O(1)). For metadata you edit often, a **resolver** instead — a managed provider or your own (`abx deploy-resolver`), [hosting.md](reference/hosting.md). Not fully on-chain (~200 gas/byte). | image off-chain (Arweave = permanent) + on-chain renderer + `lock-field`/`lock-uri`. Or a frozen `ipfs://` override + `lock-uri`. |
15
+
16
+ **Four patterns, by where bytes live × how `tokenURI` resolves:**
17
+ 1. **Fully on-chain** (`--onchain-image`) — bytes + JSON on-chain. Tiny art only.
18
+ 2. **Image off-chain, JSON on-chain, no server** (`--onchain-uri --backend arweave|ipfs|cloud`) — the sweet spot for static art. Arweave/IPFS (permanent, content-addressed) or your S3/CDN (`--backend cloud --public-base <url>`; centralized, mutable). Many files → one `url-template`.
19
+ 3. **Remote resolver** (`--public-base-url` + a node) — for mutable/dynamic metadata; **self-hosted** (`abx deploy-resolver`, you run it) or a **managed provider** (an API key, they run it). Same interface — swap with one re-point.
20
+ 4. **Inline SVG on-chain** — self-contained vector art inlined into `tokenURI`. For a **1/1** that's `abx deploy … --onchain-uri`; for a **Series** of tiny SVGs use `abx deploy-series … --onchain-image --compress fastlz` (bare `--onchain-uri` on a folder does NOT inline the images — it's the image-custody flag `--onchain-image` that puts SVG bytes on-chain per token).
21
+
22
+ **All three deploy commands can now do pattern 2.** `deploy` (1/1), `deploy-series` and `deploy-code`
23
+ each upload to the chosen backend under `--onchain-uri` and bake the resulting public URL into the
24
+ on-chain JSON. (Until 2026-08-03 the 1/1 silently could not — it committed a hash and rendered a
25
+ placeholder. If you remember that limitation, it is gone.)
26
+
27
+ ## `--copies` (ERC-1155 editions) — custody is NOT orthogonal to the shape
28
+
29
+ Adding `--copies <n|open>` does not leave the four patterns above intact. Three combinations are
30
+ **refused** today, and each one is something you would reach for first. Check this BEFORE you promise a
31
+ creator a lane — the refusal lands at deploy time, and two of these have already pushed agents into the
32
+ wrong architecture:
33
+
34
+ | Want | `deploy --copies` (1/1-edition) | `deploy-series --copies` (EditionImage) | `deploy-code --copies` (EditionCode) |
35
+ |---|---|---|---|
36
+ | **Pattern 1** — bytes fully on-chain (`--onchain-image`) | ✅ hot + `--sign` | ✅ hot + `--sign` | n/a |
37
+ | **Pattern 2** — image off-chain, JSON on-chain, **no server** (`--onchain-uri --backend arweave\|ipfs`) | ✅ | ✅ (uniform extensions → ONE `url-template` collection field, O(1)) | n/a |
38
+ | **Pattern 4** — inline SVG on-chain (`--onchain-uri`, SVG art) | ✅ | ✅ (every id must be SVG) | n/a |
39
+ | **Pattern 3** — resolver (`--public-base-url`) | ✅ | ✅ | ✅ |
40
+ | on-chain library dep (`--dep p5@…`) | n/a | n/a | ✅ **Sepolia only** (no registry on Base Sepolia) |
41
+ | build directory (`--code-dir`) · field renderers | n/a | n/a | ❌ refused — `--script` only |
42
+
43
+ **One lane genuinely can't be signed offline:** `--onchain-image` is refused on the **cold**
44
+ (`--unsigned`) lane everywhere, 721 and edition alike — staging is a sequence where each chunk tx's
45
+ receipt feeds the next, so use the hot lane or `--sign`.
46
+
47
+ Otherwise custody is now fully symmetric with the 721 side, including the case that used to have no
48
+ answer: **"N artworks × M copies each" with raster art and no server** is
49
+ `abx deploy-series --dir <folder> --copies <n> --onchain-uri --backend arweave` — a uniform file
50
+ extension uploads the folder as ONE directory and bakes a single `{id}`-substituting collection field,
51
+ so the whole collection costs one field rather than N.
52
+
53
+ **Picking IPFS (or Arweave) does NOT mean running a server.** The `--onchain-uri --backend ipfs|arweave` path (pattern 2) bakes the image's public **gateway** URL into on-chain JSON — a pinning service's read endpoint (a *dedicated* Pinata gateway for IPFS), not a resolver you host. So when a creator chooses IPFS, **default to this no-server path** — image on IPFS, JSON on-chain, nothing to keep running (just keep the pin alive). You only need a **resolver** (pattern 3 — managed or self-hosted) if they want *freely editable* metadata. Never present IPFS as blocked on "a public URL" or "a server always online": the gateway belongs to the pinning service and the JSON lives on-chain. (The one real input IPFS needs is `PINATA_JWT` in `.env` for pinning — that's an API upload, not a host.)
54
+
55
+ **No-server tradeoff (patterns 1, 2, 4):** with the on-chain renderer only the *image* is off-chain — any **description / traits / animation_url, and every configured PostParam (served as `abx_params`), live on-chain** (gas to write, permanent, lockable), vs a hosted resolver where they're free to edit. Cheap (a shared value is **one collection-scope field**, not one per token — the renderer falls back token→collection), but the creator should choose "no server" knowing their text metadata is on-chain.
56
+
57
+ Get decisions 1–2 right before deploy (image commitment + resolver URL are written then; re-pointable, but):
58
+
59
+ **1. Storage permanence** — where bytes live. Not irreversible: bytes are content-addressed by their on-chain keccak, so start on one backend and move later (`abx verify` confirms the hash). Don't let it block a first deploy.
60
+ - `arweave` = pay-once permanent, no recurring fee. `cloud` (S3/R2) = durable, you maintain it. `ipfs` = decentralized, you pin it. `fs` = zero-config start, dies with the disk → move before it matters.
61
+ - **`arweave` is nearly as easy as `fs` for small art** — Turbo default: **under 100 KB free, no setup** (a managed `.abx-self-host/arweave-key.json` minted on first upload; back it up with `abx storage backup-key`). Choose per command with `--backend` (stateless, no config file). **A backend missing its secret does NOT fall back to `fs`** — it fails: `cloud` refuses up front naming the missing values, and `ipfs` without `PINATA_JWT` resolves to **kubo mode against a local node** (`http://127.0.0.1:5001`), which fails at upload time if you aren't running one. Check with `abx storage show` (it prints the resolved backend *and* mode) before a real upload.
62
+ - **Who pays is a lane (`--storage-signer`)** — Turbo credits attach to an identity (managed key · `.env` key · browser wallet). **Before any top-up, check BOTH balances** (`abx storage balance --backend arweave` shows the managed key AND the wallet — spend the wallet's credits if present). On an upload error surface it verbatim — `…already been uploaded…` is *success* (dedup); don't reflexively top-up or switch to IPFS. Full lanes + failure playbook → [hosting.md](reference/hosting.md#arweave-via-turbo--the-easy-permanent-path-read-before-quoting-setup).
63
+
64
+ **2. Public host URL — and who runs the resolver** (**off-chain custody only**). Baked into `tokenURI` at deploy, so the CLI **refuses an off-chain deploy without a public URL** (`ABX_PUBLIC_BASE_URL` or `--public-base-url https://…`) and **never bakes localhost** (that token resolves for no one). No exceptions.
65
+ - **First ask whether you need a host at all** — tiny art is cheaper and more durable on-chain (no host). For bigger art, Arweave (no host to run) beats a resolver unless you need mutability or serve many files.
66
+ - **A named remote is already configured (`ABX_REMOTE_<NAME>_URL` in `.env`)? Use it.** The creator already chose a provider — don't stand up new infrastructure beside it. **Run `abx remote <name>` FIRST, before registering anything**: it prints the provider's chain coverage + whether rendering is managed, and it *validates the key* (`401` = the token in `ABX_REMOTE_<NAME>_TOKEN` is stale/wrong → they replace the value in `.env`; `403` = the key is fine but not authorized for this contract/chain → provider-side scoping, don't touch the key). Then register: `abx add <addr> --remote <name>`. Testing a replacement key without editing `.env` first: `abx remote <name> --remote-token <new-key>`.
67
+ - **Otherwise, two equal ways to have a resolver, one config change apart.** A **managed provider** — one base URL + one API key, no cloud account, nothing to keep alive; often **managed rendering** too, so a code drop needs no effects runner (**lead with this when the creator doesn't already run infrastructure or doesn't want to** — [hosting.md → Managed providers](reference/hosting.md#managed-providers--a-resolver-someone-else-runs---remote-name)). Or **self-host** (`abx deploy-resolver`, [hosting.md](reference/hosting.md)) — the creator owns the node and the cloud account. Same interface, same commands; a project moves between them with one re-point + re-register. **No provider key in hand and none to get? Self-host is the fully-supported path today** — the provider market is only starting to form; never invent or recommend a provider that isn't in front of you.
68
+ - **Tunnels (ngrok/cloudflared) are preview-only — never bake one on-chain** (dies on sleep, rotates on restart). A real launch puts the resolver on a host you control under your own domain (move = a DNS re-point), or behind a provider.
69
+
70
+ **3. Identity** — `--name`, `--symbol`, `--royalty-bps` (default 500 = 5%), `--description "…"`, `--external-url <url>` (both served in the metadata — set them or the description is boilerplate). Owner + royalty receiver = the deploying wallet. These default to off-chain operator metadata (editable via `abx add <addr> --description "…"`). For a description that should outlast any node, add `--description-onchain` (or later `abx set-field <addr> --field description --text "…"`) → on-chain, freezable via `lock-field`; the resolver prefers the on-chain value. This is the per-field on-chain model — any field on-chain or off, one active `representation` (inline · reader · keccak256 · arweave · ipfs · url). Background: [metadata model](https://abx.docs.artblocks.io/protocol/metadata/).
71
+ - **Credit + license** — deploy flags `--artist "…"` · `--license "…"` (also `--display-notes`, `--artist-links`) bake authorship + rights ON-CHAIN in the deploy tx (all three deploy commands); or set/change them later with `abx set-field <addr> --collection --field artist|license --text "…"`. Reserved collection fields served in `contractURI`, on any type (1/1 · Series · code). Detail: [operating.md → Authorship + rights](reference/operating.md#authorship--rights-credit--license).
72
+ - **Propose a real name/symbol and confirm — never silently bake a generic folder-name guess.** A folder called `series`/`images`/`photos` infers junk ("Series" / "SRS"), and on all three deploy commands the CLI *refuses* a real send that would bake its own placeholder identity (`--name`/`--symbol` missing) — because on-chain identity is effectively permanent. **In `--dry-run` the same check only warns** (so a preview still runs before you have the creator's title); don't read that warning as "the CLI allows it" — the real deploy stops. Suggest a specific title + a short ticker-style symbol drawn from the actual work, and get an explicit yes before deploying. Inference is a suggestion to confirm, not a default to ship — if the folder name is generic, say so and ask rather than proposing it.
73
+
74
+ **4. Image placement** — `--image <path>` (png · jpg · gif · svg · webp). The on-chain keccak256 (`image` field) anchors integrity; size is bounded by the backend, not the chain.
75
+ - *Off-chain:* the served `image` is the backend's **gateway HTTPS URL** (`https://<gateway>/ipfs/<cid>`), not raw `ipfs://` (wallets/marketplaces can't render that). So off-chain needs a pinning service + a **public** gateway — with Pinata use a **dedicated** gateway (`--gateway https://<you>.mypinata.cloud`); a local kubo gateway is preview-only. The keccak stays the anchor → move gateways without a tx.
76
+ - *Fully on-chain:* `abx set-field <addr> --field image --file <path> [--compress fastlz]` splits into SSTORE2 chunks behind the shared reader; or bake it in with `abx deploy --image <path> --onchain-image [--compress fastlz]`.
77
+
78
+ **Inline vs reader — default to the reader for real artwork.** `--onchain-uri` alone inlines the SVG (1 tx, ~700 gas/byte); `--onchain-image --compress fastlz` stages via SSTORE2 + a small `reader` pointer (~200 gas/byte, +1 tx) — **cheaper above ~0.5 KB** and widening with size. So: tiny (<~0.5 KB, a one-line SVG/short text) → `--onchain-uri` inline; real artwork (a few KB+) → `--onchain-image --compress fastlz`. **Never `--compress gzip` for an on-chain-rendered token** — gzip decodes off-chain only, breaking `--onchain-uri`; use fastlz (it decodes *in* the reader).
79
+
80
+ **5. On-chain vs off-chain resolution** — by default `tokenURI`/`contractURI` point at your resolver. `--onchain-uri` = JSON assembled *on-chain* by the shared `AbxMetadataRenderer`, self-resolving forever — so it pairs with on-chain content, cost-effective only for tiny art (thresholds above).
81
+ - **Fully on-chain = no server.** Don't stand one up; never cite a localhost URL. **Prove it with `abx tokenuri <addr>`** (reads `tokenURI(0)` over RPC, no `serve`). `abx serve` is only for off-chain-resolving tokens.
82
+ - **The off-chain `tokenURI` is a base, not a per-token URL** — the contract stores a base and derives `{base}/{chainId}/{address}/{tokenId}`. Set via `--public-base-url` or `set-token-uri --uri <base>` later.
83
+ - **Don't default to a frozen `ipfs://` tokenURI** — every edit then = re-pin + on-chain re-point, and the event spine stops driving the token (exiting the spec). Right only for true immutability, then lock it (`set-token-uri --override ipfs://<cid>` then `lock-uri`).
84
+ - **Store ≠ lock; lock last.** On-chain ≠ frozen. Deploy unlocked, confirm it resolves in production, *then* freeze. Two locks: `lock-field <addr> --field <name>` (a value) + `lock-uri <addr>` (how it resolves). Both = provably immutable. A deliberate follow-up, not the first deploy.
85
+
86
+ **6. When to mint** → [Deploy strategy](#deploy-strategy--when-to-mint).
87
+
@@ -29,7 +29,7 @@ All backends verify via the on-chain keccak commitment wherever the bytes live.
29
29
 
30
30
  - **Identity is a lane (`--storage-signer`) — Turbo is multi-chain, so credits attach to whichever identity signs the upload.** Three choices, mirroring the tx signing lanes:
31
31
  - **`arweave`** *(default)* — a CLI-managed Arweave key auto-generated at `.abx-self-host/arweave-key.json` on first upload (address printed). Reused across every upload/project; it signs *and* holds credits. Zero-config. Bring-your-own via `ARWEAVE_JWK` (inline) / `ABX_ARWEAVE_KEY_FILE` (path). **Back it up:** `abx storage backup-key --out <path>` (copies the file — the key is never printed to the terminal/log).
32
- - **`--storage-signer eth`** — reuse the **`.env` EVM signing key** as the Turbo identity (`ABX_DEPLOYER_PK`/`SEPOLIA_FUNDED_PK`/`SEPOLIA_WALLET_PK`). Its ETH address holds the credits, so credits funded on that wallet's Turbo balance are spendable. Hot lane (key in `.env`).
32
+ - **`--storage-signer eth`** — reuse the **`.env` EVM signing key** (`ABX_DEPLOYER_PK`) as the Turbo identity. Its ETH address holds the credits, so credits funded on that wallet's Turbo balance are spendable. Hot lane (key in `.env`).
33
33
  - **`--storage-signer eth` + `--sign`** — the **browser wallet** signs each upload data-item via `personal_sign` (no gas, no funds move; the upload is paid from the wallet's Turbo credits), in the same sign session as the deploy tx. The key never leaves the wallet. *(New; the mechanism matches arbundles' `InjectedEthereumSigner` and is unit-tested at the CLI↔page contract, but the live MetaMask↔Turbo path wants a manual smoke test. If a live upload misbehaves, fall back to `--storage-signer eth` with a key, or fund the managed key.)*
34
34
  - **Under 100 KB → free**, permanent, zero setup. The CLI prints this from the file size (in `--dry-run` too).
35
35
  - **Over 100 KB → prepaid credits, one-time.** `abx storage balance` shows the funded address + credits; `abx storage topup --usd <n>` returns a Stripe checkout link. A **pre-upload balance guard** stops *before* the deploy if credits are short — printing the address + fund options — so a shortfall never fails mid-deploy after txs already landed. One-time payment, no recurring fee, nothing to re-pin (contrast `cloud`/`ipfs`). Credits are non-refundable.
@@ -54,19 +54,30 @@ A failed upload is the #1 place an agent goes off the rails: it invents a cause
54
54
 
55
55
  - **Uniform extension → O(1) directory.** Files are renamed to the token-id convention (`0.png`, `1.png`, …), uploaded as one IPFS directory pin (Pinata) / Arweave path manifest (Turbo), and a single collection-scope `image` field gets the **`url-template`** representation — `<gatewayBase>/{id}.png`. The renderer substitutes each id → one field renders the whole collection, any size. No per-token on-chain data.
56
56
  - **Mixed extensions → per-token `url`** fields (O(N)) — still no server. A uniform extension unlocks the single-template path; flag it if a folder is mixed.
57
- - **`cloud` (S3/R2/CDN)** works the same but needs a **public read base** (`--public-base` / `ABX_S3_PUBLIC_BASE`, also what `deploy-code --image-base` takes), **distinct from the signed-API `--endpoint`/`ABX_S3_ENDPOINT`** they are different hosts. ⚠ **R2:** `https://<acct>.r2.cloudflarestorage.com` is the *API endpoint* (auth-only, never public → 403 for marketplaces); the public read URL is a `https://pub-<hash>.r2.dev` you ENABLE in the dashboard, or a custom domain. AWS: a public-read bucket or a CloudFront domain. The full set: `ABX_S3_ENDPOINT`/`BUCKET`/`REGION`/`ACCESS_KEY_ID`/`SECRET_ACCESS_KEY` + `ABX_S3_PUBLIC_BASE` (`abx storage show` prints them). Files upload under a content-derived prefix → `<publicBase>/<prefix>/{id}.png`. **Caveat:** S3 is centralized, mutable, not content-addressed (no CID/txid root). Bake a domain/CDN you control. Good if the creator already runs a bucket/CDN; IPFS/Arweave are the trustless/permanent options.
57
+ - **`cloud` (S3/R2/CDN)** works the same, but the signed-API host (`--endpoint`/`ABX_S3_ENDPOINT`, where uploads go) and the public read host (`--public-base`/`ABX_S3_PUBLIC_BASE`, also what `deploy-code --image-base` takes) are **different hosts by design**one auth-only, one public (R2's own API endpoint is never public; its public URL is a separate `pub-<hash>.r2.dev` or custom domain you enable). **`abx storage show --check` proves the two agree** a real PUT through the API + an unsigned GET through the public base (`abx doctor` runs the same probe). Files upload under a content-derived prefix → `<publicBase>/<prefix>/{id}.png`. **Caveat:** S3 is centralized, mutable, not content-addressed (no CID/txid root). Good if the creator already runs a bucket/CDN; IPFS/Arweave are the trustless/permanent options.
58
58
  - **Integrity** (IPFS/Arweave) comes from the content-addressed root (CID / manifest txid), not a per-token keccak. The gateway host is baked on-chain → moving gateways is a `set-field` (bytes stay put). Prefer a **dedicated** gateway.
59
59
  - **`url-template`** is a first-class representation ([spec](../../../../specs/protocol/onchain-metadata.md)); set by hand with `abx set-field <addr> --collection --field image --representation url-template --text "<gateway>/ipfs/<cid>/{id}.png"` then `abx set-renderer <addr>`.
60
60
 
61
- ## Hosted resolver`abx deploy-resolver`
61
+ ## Managed providersa resolver someone else runs (`--remote <name>`)
62
62
 
63
- For the large/mutable default. `abx deploy-resolver --provider <fly|render|vps> --domain <meta.you.xyz>` scaffolds the artifact and prints the exact next steps, the DNS record, and the bake reminder. Providers: **fly.io** / **render** (Docker PaaS, free tier, custom domain) and a **VPS** (compose + Caddy auto-TLS). The host is **read-only** — serves + accepts admin index-control, no signing key on it (writes are signed locally), so a compromised host can at worst serve wrong bytes (the keccak catches it). Prefer **a domain you control** (move = DNS, not a tx) but without one the scaffold now bakes the **real platform hostname** (`<app>.fly.dev` / `<app>.onrender.com`) so the resolver works out of the box (add a custom domain later). It NO LONGER bakes a dead `<app>.example` placeholder, and the resolver **refuses to serve** an `.example`/placeholder base (or a localhost base in a hosted image, `ABX_HOSTED=1`) — a loud fail beats silently serving dead image/animation links. The one sub-decision is *which provider* (ask + recommend); you scaffold, the human owns the cloud account + domain.
63
+ The other way to have a resolver: be a **customer** of a hosted provider instead of running a node one base URL + one API key, no cloud account, no Dockerfile, nothing to keep alive. Same interface, same commands as self-hosting ([spec](../../../../specs/self-host-toolkit/remote-services.md)); a project moves between a provider and your own node with one re-point + re-register ([operating.md Moving your hosting](operating.md#moving-your-hosting--two-cases-dont-conflate)).
64
+
65
+ - **Named remotes.** Put the provider in `.env`: `ABX_REMOTE_<NAME>_URL=<base>` + `ABX_REMOTE_<NAME>_TOKEN=<api key>`. **`<NAME>` is YOUR label for the provider, not their legal name** — you pick it, you pass the same word to `--remote`. Normalization matches `ABX_RPC_URLS_<CHAIN>`: uppercase, runs of non-alphanumerics → `_`. So `--remote my-provider` reads `ABX_REMOTE_MY_PROVIDER_*`. For a multi-word brand, pick ONE short word and use it consistently — "Meridian Metadata" → `--remote meridian` + `ABX_REMOTE_MERIDIAN_URL`/`_TOKEN` (using `meridian metadata` would work too, as `ABX_REMOTE_MERIDIAN_METADATA_*`, but short is kinder to type). Then every remote command takes the name: `abx add <addr> --remote <name>`, ditto `index` / `forget` / `render` / `verify`, `abx migrate --from/--to <name>`, and owner-op nudges. Ad-hoc: `--remote <url> --remote-token <key>`. **Bare `--remote` stays the self-host default** (`ABX_PUBLIC_BASE_URL` + `ABX_REMOTE_SELF_TOKEN` — "self" is just another named remote) — nothing changes for a node you deployed. A named remote never falls back to `ABX_REMOTE_SELF_TOKEN` (or the resolver's own server-side `ABX_RESOLVER_ADMIN_TOKEN`), so neither secret can leak to a provider.
66
+ - **Ask up front whether they serve YOUR domain.** Whatever base URL is baked on-chain is what a marketplace fetches. If that's a domain the creator controls (pointed at the provider), leaving later is a DNS change — no tx, no gas. If it's the provider's own hostname, leaving costs an **owner-signed `set-token-uri`/`set-contract-uri` tx**. Same exit either way, different price; worth knowing before you bake it, not after.
67
+ - **Read the descriptor BEFORE registering — match the project to the provider.** `abx remote <name>` (or `curl <base>/.well-known/abx-service`, public, no key) prints what the service supports: **`chains`** must cover the project's chain (else registration is refused with `unsupported_chain`); **`render.attached`** means thumbnails/traits for code drops are rendered behind the provider — **skip `abx deploy-effects` entirely** (without it, renders are still yours: `abx render <addr> --remote <name>` or your own runner); **`auth.signupUrl`** is where a human gets a key.
68
+ - **The API key is the human's step — a membrane ask, never a chat paste.** Say: *"This provider covers your chain and manages rendering. You'll need an API key from `<signupUrl>` — put it in `.env` as `ABX_REMOTE_<NAME>_TOKEN` (never paste it in chat) and I'll do the rest."* Then verify with `abx remote <name>`: it lists the projects visible to the key. **401** = the key is missing/wrong (check the var, rotate at the provider). **403** = the key is valid but not authorized for this contract/chain — provider-side scoping, not a typo; don't retry-loop, check the provider dashboard.
69
+ - **The exit is guaranteed by the interface**, not by the provider's goodwill: registration is never load-bearing for resolution, and `abx migrate --from <provider> --to <anywhere>` reads only the provider's *public* endpoints — leaving is one config change plus (for a provider-hostname base) one on-chain re-point.
70
+ - **Honesty:** no default provider is baked into the CLI, the SDK, or this skill, and the provider market is only starting to form. A creator with a provider key (or a named remote already in `.env`) should use it; otherwise **self-hosting (next section) is the fully-supported path today** and what the rest of this file assumes.
71
+
72
+ ## Self-hosted resolver — `abx deploy-resolver`
73
+
74
+ For the large/mutable default, when the creator runs the node themselves (the alternative to a managed provider above — same interface, they own the cloud account). `abx deploy-resolver --provider <fly|render|vps> --domain <meta.you.xyz>` scaffolds the artifact and prints the exact next steps, the DNS record, and the bake reminder. Providers: **fly.io** / **render** (Docker PaaS, free tier, custom domain) and a **VPS** (compose + Caddy auto-TLS). The host is **read-only** — serves + accepts admin index-control, no signing key on it (writes are signed locally), so a compromised host can at worst serve wrong bytes (the keccak catches it). Prefer **a domain you control** (move = DNS, not a tx) — but without one the scaffold now bakes the **real platform hostname** (`<app>.fly.dev` / `<app>.onrender.com`) so the resolver works out of the box (add a custom domain later). It NO LONGER bakes a dead `<app>.example` placeholder, and the resolver **refuses to serve** an `.example`/placeholder base (or a localhost base in a hosted image, `ABX_HOSTED=1`) — a loud fail beats silently serving dead image/animation links. The one sub-decision is *which provider* (ask + recommend); you scaffold, the human owns the cloud account + domain.
64
75
 
65
76
  **The artifact is fully self-contained.** `deploy-resolver` writes `deploy/<provider>/` with its OWN `Dockerfile` + `.dockerignore` + config (a production image installs the published CLI: `npm i -g --no-optional @artblocks/abx-cli`). **Run every next step from that dir.** You never supply, copy, or hand-edit a Dockerfile. **If a step seems to need a file from elsewhere (`../Dockerfile`, a `packages/` dir, the repo), STOP — that's a scaffold bug, not something to work around.** Report it; don't MacGyver it. (Local from-source/contributor dev sets `ABX_RESOLVER_SOURCE=1` → a build-from-source artifact instead, still self-contained; you don't set this.)
66
77
 
67
78
  **Never inline a secret into a command.** The generated steps **source from `.env`** (`set -a; . ../../.env; set +a` then `fly secrets set ABX_RPC_URLS="$ABX_RPC_URLS"`) — keep that shape. Never substitute a literal secret into command text; it must never appear in output or shell history.
68
79
 
69
- `deploy-resolver` also **generates `ABX_RESOLVER_ADMIN_TOKEN`** → your local `.env` (a secret — never printed, env-only); the printed steps set the **same** token on the host. That token is what later lets `abx add <clone> --remote` tell the hosted node which contracts to index — the only new secret the hosted path adds, and it authorizes indexing control, not signing.
80
+ `deploy-resolver` also **generates `ABX_REMOTE_SELF_TOKEN`** → your local `.env` (a secret — never printed, env-only; "self" is just the named remote for a node you run); the printed steps set that same value on the host under **its own** var name, `ABX_RESOLVER_ADMIN_TOKEN` (server-side config, unrelated to how any client authenticates). That token is what later lets `abx add <clone> --remote` tell the hosted node which contracts to index — the only new secret the hosted path adds, and it authorizes indexing control, not signing.
70
81
 
71
82
  **Gather the decisions in one pass.** The hosted path has a few small choices — provider, custom domain (or the provider-hostname tradeoff), app name, plus identity. Collect them together, then show the full plan; don't drip one question per turn. Once the human says "you run it," run the steps — don't flip between "here are commands" and "I'll do it."
72
83
 
@@ -74,18 +85,37 @@ For the large/mutable default. `abx deploy-resolver --provider <fly|render|vps>
74
85
 
75
86
  ## Render runner — `abx deploy-effects`
76
87
 
77
- A code project's marketplace still is rendered off-chain, so a **runner** (Playwright + Chromium — the resolver image stays browserless) must render each token's live view and hand the resolver the result. `abx deploy-effects --resolver-url <resolver>` scaffolds it for Fly (a self-contained `Dockerfile.effects` + `fly.toml` + vendored source), wired to the resolver and your storage home. **Decide where renders live up front** (`ABX_STORAGE_BACKEND`): **ipfs / arweave** (the runner uploads and publishes a durable `ipfs://`/`ar://` locator the resolver 302-redirects to) or **s3** **NOT** the default `fs` for a hosted setup (a laptop-local store a hosted resolver can't read, so the placeholder never clears). The runner **publishes** each render to the resolver's admin control plane (`POST /admin/render-artifacts`, gated by the SAME `ABX_RESOLVER_ADMIN_TOKEN` — index-control only, never signing), so it does NOT need to share the resolver's disk. Two topologies:
88
+ A code project's marketplace still is rendered off-chain, so a **runner** (Playwright + Chromium — the resolver image stays browserless) must render each token's live view and hand the resolver the result. **A managed provider with `render.attached` in its descriptor does all of this for you — skip this section.** Self-hosting it: `abx deploy-effects --resolver-url <resolver>` scaffolds it for Fly (a self-contained `Dockerfile.effects` + `fly.toml` + vendored source), wired to the resolver and your storage home. **Decide where renders live up front** (`ABX_STORAGE_BACKEND`): **cloud (S3/R2 + a public base) · ipfs · arweave** equal options, pick on cost/ops. **NOT** the default `fs` for a hosted setup, and this is now refused rather than warned: the runner HOLDS its render bytes and hands the resolver a URL, so a backend that can't name one has no lane (`abx deploy-effects` errors out, and the runner refuses to start). The runner **registers** each render with the resolver's control plane (`POST /v1/effect-artifacts`, gated by the same token — your local `ABX_REMOTE_SELF_TOKEN`, the resolver's own `ABX_RESOLVER_ADMIN_TOKEN` — index-control only, never signing), so it does NOT need to share the resolver's disk. What travels: a **locator** for the still (the resolver 302-redirects; it never proxies or fetches it), and the **content** for `traits` (≤64KB — traits stitch into the token JSON, so a pointer there could never work). It does not matter which scheme the URL uses: an `https://` gateway or bucket URL is a peer of `ipfs://`/`ar://`, since a lost render is a re-render. It only has to be fetchable by someone other than you — a loopback gateway or a presigned expiring URL is refused. Two topologies:
78
89
 
79
- - **Locator bridge (default for a hosted drop):** runner on its own host + a public storage home; it publishes locators, the resolver redirects. `deploy-effects` sets this up.
80
- - **Co-located:** runner beside the resolver sharing one backend/volume (no admin token needed — both read/write the same store).
90
+ - **Locator bridge (default for a hosted drop):** runner on its own host + a public storage home; it registers locators, the resolver redirects. `deploy-effects` sets this up.
91
+ - **Co-located:** runner beside the resolver sharing one backend/volume (no admin token needed — both read/write the same store). This is the escape hatch when there's no public storage home: `fs` is fine here.
81
92
 
82
- One-shot without a service: **`abx render <addr> --remote <resolver>`** renders locally and publishes to the hosted resolver (re-run to restore a resolver that lost its volume the publish is idempotent). Optional immediacy: set `ABX_EFFECTS_URL` on the resolver so it pings the runner on re-index; the periodic sweep is the eventual floor regardless.
93
+ One-shot without a service: **`abx render <addr> --remote <resolver>`** renders locally and registers with the hosted resolver — same requirement, so pass `--backend cloud|ipfs|arweave` (the default `fs` is refused before Chromium launches, naming the fix). Re-run any time: registration is idempotent and repairs a resolver that lost its rows without re-rendering. Optional immediacy: set `ABX_EFFECTS_URL` on the resolver so it pings the runner on re-index; the periodic sweep is the eventual floor regardless.
83
94
 
84
- ## Local vs remote — two SEPARATE projection stores (read before deploying to a hosted resolver)
95
+ ## Local vs remote — two SEPARATE projection stores (read before deploying to a remote resolver)
85
96
 
86
- A project is served only if it's in **the resolver's own** store. `abx deploy`/`abx add` (no `--remote`) index into **this machine's** store; a hosted resolver is a **different store** and doesn't learn about a contract just because you deployed it. A local deploy pointing its `tokenURI` at a remote resolver returns `{"error":"unknown project"}` for everyone — "works on my `serve`, broken for the world." **Bridge it:**
97
+ A project is served only if it's in **the resolver's own** store. `abx deploy`/`abx add` (no `--remote`) index into **this machine's** store; a remote resolver — self-hosted or a managed provider — is a **different store** and doesn't learn about a contract just because you deployed it. A local deploy pointing its `tokenURI` at a remote resolver returns `{"error":"unknown project"}` for everyone — "works on my `serve`, broken for the world." **Bridge it:**
87
98
  - **Local resolver** (`abx serve` here): a local `deploy`/`add` already indexed it — done.
88
- - **Remote resolver** (baked URL is hosted): after deploy run **`abx add <clone> --remote [url]`** to register + index it on the node (url defaults to `ABX_PUBLIC_BASE_URL`; needs `ABX_RESOLVER_ADMIN_TOKEN` matching the resolver). Also **bridges** what the node can't derive: the durable `ipfs://`/`ar://` locator and off-chain traits. Post-deploy nudge (after a deferred mint) is **`abx index <clone> --remote`** (idempotent). Remove with **`abx forget <clone> --remote`**. Every remote command prints `REMOTE → <url>`.
99
+ - **Remote resolver** (baked URL is remote): after deploy run **`abx add <clone> --remote [name|url]`** to register + index it on the node (bare `--remote` defaults to `ABX_PUBLIC_BASE_URL` + `ABX_REMOTE_SELF_TOKEN`; a named remote uses its own `ABX_REMOTE_<NAME>_TOKEN`). Also **bridges** what the node can't derive: the durable `ipfs://`/`ar://` locator and off-chain traits. Post-deploy nudge (after a deferred mint) is **`abx index <clone> --remote`** (idempotent). Remove with **`abx forget <clone> --remote`**. Every remote command prints `REMOTE → <url>`.
100
+
101
+ ### Indexing status — `backfilling` is not a failure
102
+
103
+ A service may accept a registration and catch up **asynchronously** (a cold replay on a rate-limited RPC takes minutes). `abx add --remote` handles it: prints `registered — backfilling…`, polls, then prints the normal summary once the project is **`live`**. So a slow add is *slow*, not broken — don't re-run `add` to "retry" it (a re-POST never starts a second replay, but you'll just wait again). `--no-wait` returns immediately when you don't want to block.
104
+
105
+ **Check anytime — same five words locally and remotely:** `abx status <address>` (this node) · `abx status <address> --remote <name> [--watch]` (a service) · `abx remote <name>` (roll-up: *"3 live, 1 backfilling, 1 failed (rpc_rate_limited)"*).
106
+
107
+ **On a node YOU run**, two knobs shape this (both optional): `ABX_REGISTER_DEADLINE_MS` (default 8000) is how long a register may hold the HTTP request before answering "accepted, still catching up" and finishing in the background — the registration is durable either way, so this only decides who waits; and `ABX_STALE_LAG_BLOCKS` (default 5000) is how far behind chain head the watcher may fall before it reports its projects `stale`. A backfill interrupted by a restart is re-queued automatically on the next `abx serve` — you don't have to re-add it.
108
+
109
+ | status | what it means | what to do |
110
+ | --- | --- | --- |
111
+ | `queued` / `backfilling` | accepted, catching up (registration is already durable) | **wait** — poll, don't re-add |
112
+ | `live` | caught up, tracking head | done |
113
+ | `stale` | was live, now lagging — **still serving** old state | usually the service's RPC; investigate if it sticks |
114
+ | `failed` | catch-up errored; retried with backoff | read the **class** (below); the registration survived |
115
+
116
+ `failed` names a cause: **`rpc_rate_limited`/`rpc_unavailable`** = the *service's* RPC, not your key and not your address — it retries, so wait or tell the operator; **`not_abx_contract`** = wrong address/chain (or a provider that validates clones refusing a non-clone); **`internal`** = the operator's logs have it. Report the class verbatim to the user rather than guessing a cause.
117
+
118
+ **A `live` project with 0 events is a red flag, not a success** — a real ABX clone always emits a spine, so 0 events means the service scanned the wrong chain or its RPC didn't serve the logs (check `abx remote <name>` covers your chain, and that the deploy block was forwarded). The CLI warns instead of printing ✓ here.
89
119
 
90
120
  ### Deploying to a hosted resolver — pre-warm flow (preferred)
91
121
 
@@ -117,12 +147,15 @@ Drop `--no-mint` (steps 5–6 collapse) for the express path. Add `--sign` to st
117
147
 
118
148
  ## Token API (the resolver)
119
149
 
150
+ ⚠ **Read this list; never infer a route from another one — and prefer not needing it at all.** The contract commits its own metadata URL on-chain, so **`abx tokenuri <addr>`** and **`abx contracturi <addr>`** (ERC-7572 collection metadata: reads `contractURI()`, follows it, decodes the JSON) answer "where does this resolve, and what does it say" without you assembling a URL. Guessing has a specific trap: **`/t/<chainId>/<address>` is NOT collection metadata** — collection metadata is `/c/<chainId>/<address>`. A miss now names which of three things happened, in a machine `code`: **400 `invalid_request`** (your path shape — the body names the right template and may carry `didYouMean`) · **404 `unknown_route`** (this node serves no such path; body lists what it does serve) · **404 `not_registered`** (path fine, this node doesn't index that contract → `abx add <addr> --remote`) · **400 `unsupported_chain`** (body carries the `chains` it serves). None of those mean the service is down — don't report an outage off a URL you built.
151
+
120
152
  Default `http://localhost:8787` (or `ABX_PUBLIC_BASE_URL`). Routes carry the **chainId** (the protocol path grammar) so one host can serve many chains:
121
- - `GET /` — read-only index of served contracts · `GET /t/<chainId>/<address>/0` — ERC-721 metadata · `…/0/image` — the image
153
+ - `GET /` — read-only index of served contracts · `GET /t/<chainId>/<address>/0` — token metadata (ERC-721 `tokenURI` on a 1/1/Series/code project, ERC-1155 `uri` on an edition — auto-detected, same route) · `…/0/image` — the image
122
154
  - `GET /c/<chainId>/<address>` — ERC-7572 collection metadata · `GET /api/project/<address>` — full reconstructed state · `GET /d/<chainId>/<address>` — per-contract read-only dashboard (namespaced so one host serves many contracts). No public action buttons anywhere.
123
155
  - For IPFS/Arweave the served `image` is the **gateway HTTPS URL** (`https://<gateway>/ipfs/<cid>` / `<gateway>/<txid>`), the form wallets/marketplaces render (raw `ipfs://` doesn't). The on-chain commitment is the **keccak256** (backend-neutral anchor, survives a gateway migration); the CID/txid is just the locator. The locator lives in the **deployer's** local index, so a **remote** resolver emits the gateway URL only once it's bridged (`abx add <clone> --remote`), else `image` falls back to the resolver's own `/…/image` route.
124
- - `POST /api/project/<address>/reindex` (full replay) · `GET /api/project/<address>/verify`both **admin-only** (bearer `ABX_RESOLVER_ADMIN_TOKEN`), never exposed as public actions. Run from the CLI: `abx index <addr> --remote` / `abx verify <addr>`.
125
- - `POST /admin/projects` `{address, fromBlock?, factory?, description?, externalUrl?, attributes?, contentLocators?}` + `DELETE /admin/projects/<address>` — the **admin control plane** (register/forget which contracts the node indexes, bridge off-chain traits + locators; a remote `add`/`forget`). Bearer-gated, disabled when the var is unset. Indexing control only — never signing.
156
+ - `GET /.well-known/abx-service` — the **service descriptor** (public): what the node supports `interfaces`, `chains`, `auth` (incl. `signupUrl` for humans), `render.attached` (managed rendering). Read it before registering with any remote service.
157
+ - `POST /v1/projects` `{chainId, address, fromBlock?, factory?, description?, externalUrl?, attributes?, contentLocators?}` · `GET /v1/projects` (the projects visible to the token, each with `status`) · `DELETE /v1/projects/<chainId>/<address>` · `POST /v1/projects/<chainId>/<address>/reindex` · `GET /v1/projects/<chainId>/<address>/status` (`status` + `fromBlock`/`toBlock`/`headBlock` + `error.class`) — the **control plane** ([spec](../../../../specs/self-host-toolkit/remote-services.md)): register/forget which contracts the node indexes, bridge off-chain traits + locators; a remote `add`/`forget`. Bearer-gated (`Authorization: Bearer <token>`), 404 code `disabled` when no token is configured. Errors carry a machine `code` (`unauthorized` 401 · `forbidden` 403 · `unsupported_chain` · `not_registered`). Register answers **200** (caught up, with counts) or **202** (accepted, still catching up — poll status); the registration is durable either way. Indexing control only — never signing.
158
+ - `GET /api/project/<address>/verify` — bearer-gated too (it triggers outbound fetches). Run from the CLI: `abx verify <addr>` / `abx index <addr> --remote`.
126
159
 
127
160
  `/api/project/<address>` key fields: `isCanonical` (factory-verified — note: `isCanonical`, **not** `canonical`), `owner`, `royalty`, `collectionFields[]`/`lockedCollectionFields[]`, `extensions[]`, `tokens[]` each `{minted, owner, tokenURI, fields[] ({field, representation, value}), lockedFields[]}`. The JSON also carries **`abx_provenance`** — per-field `source` + `status` (`on-chain` · `verified` · `mismatch` · `anchored` · `off-chain` · `n/a`), with an `anchor` for off-chain bytes that carry an on-chain hash (`off-chain` = plain operator value, benign; `anchored` = verifiable via `abx verify`). Confirm a deploy with **`abx verify <addr>`**, not curl.
128
161