@artblocks/abx-cli 0.1.0-alpha.0

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/assets/renderer-scaffold/README.md +59 -0
  3. package/assets/renderer-scaffold/foundry.toml +15 -0
  4. package/assets/renderer-scaffold/remappings.txt +3 -0
  5. package/assets/renderer-scaffold/script/Deploy.s.sol +23 -0
  6. package/assets/renderer-scaffold/src/MyRenderer.sol +123 -0
  7. package/assets/renderer-scaffold/src/MyTraits.sol +75 -0
  8. package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +32 -0
  9. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +26 -0
  10. package/assets/renderer-scaffold/test/MyRenderer.t.sol +110 -0
  11. package/dist/config.d.ts +69 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +184 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/deps.d.ts +46 -0
  16. package/dist/deps.d.ts.map +1 -0
  17. package/dist/deps.js +90 -0
  18. package/dist/deps.js.map +1 -0
  19. package/dist/flags.d.ts +25 -0
  20. package/dist/flags.d.ts.map +1 -0
  21. package/dist/flags.js +34 -0
  22. package/dist/flags.js.map +1 -0
  23. package/dist/inspect.d.ts +48 -0
  24. package/dist/inspect.d.ts.map +1 -0
  25. package/dist/inspect.js +184 -0
  26. package/dist/inspect.js.map +1 -0
  27. package/dist/main.d.ts +3 -0
  28. package/dist/main.d.ts.map +1 -0
  29. package/dist/main.js +5102 -0
  30. package/dist/main.js.map +1 -0
  31. package/dist/migrate.d.ts +65 -0
  32. package/dist/migrate.d.ts.map +1 -0
  33. package/dist/migrate.js +180 -0
  34. package/dist/migrate.js.map +1 -0
  35. package/dist/mintpage.d.ts +46 -0
  36. package/dist/mintpage.d.ts.map +1 -0
  37. package/dist/mintpage.js +461 -0
  38. package/dist/mintpage.js.map +1 -0
  39. package/dist/onchain-uri.d.ts +97 -0
  40. package/dist/onchain-uri.d.ts.map +1 -0
  41. package/dist/onchain-uri.js +243 -0
  42. package/dist/onchain-uri.js.map +1 -0
  43. package/dist/ownerops.d.ts +195 -0
  44. package/dist/ownerops.d.ts.map +1 -0
  45. package/dist/ownerops.js +1270 -0
  46. package/dist/ownerops.js.map +1 -0
  47. package/dist/provision.d.ts +86 -0
  48. package/dist/provision.d.ts.map +1 -0
  49. package/dist/provision.js +372 -0
  50. package/dist/provision.js.map +1 -0
  51. package/dist/remote.d.ts +58 -0
  52. package/dist/remote.d.ts.map +1 -0
  53. package/dist/remote.js +54 -0
  54. package/dist/remote.js.map +1 -0
  55. package/dist/schema.d.ts +15 -0
  56. package/dist/schema.d.ts.map +1 -0
  57. package/dist/schema.js +125 -0
  58. package/dist/schema.js.map +1 -0
  59. package/dist/series-traits.d.ts +30 -0
  60. package/dist/series-traits.d.ts.map +1 -0
  61. package/dist/series-traits.js +103 -0
  62. package/dist/series-traits.js.map +1 -0
  63. package/dist/signer.d.ts +80 -0
  64. package/dist/signer.d.ts.map +1 -0
  65. package/dist/signer.js +520 -0
  66. package/dist/signer.js.map +1 -0
  67. package/dist/upload.d.ts +28 -0
  68. package/dist/upload.d.ts.map +1 -0
  69. package/dist/upload.js +41 -0
  70. package/dist/upload.js.map +1 -0
  71. package/package.json +55 -0
  72. package/skill/SKILL.md +304 -0
  73. package/skill/reference/code-projects.md +211 -0
  74. package/skill/reference/hosting.md +138 -0
  75. package/skill/reference/operating.md +116 -0
  76. package/skill/reference/setup.md +36 -0
  77. package/skill/reference/troubleshooting.md +28 -0
@@ -0,0 +1,211 @@
1
+ # Code projects — operate, resolve, render, sell
2
+
3
+ [← back to SKILL.md](../SKILL.md#code-projects-generative--code-based-drops)
4
+
5
+ The [SKILL Code projects](../SKILL.md#code-projects-generative--code-based-drops) section is the decision tree — inspect, pick a lane. This file is the operating depth: what to keep running, how to verify it resolves, render ops, and the lane internals. A **program is the content** (`abx deploy-code` → a `SeriesCode`): its output is a function of live on-chain state (`tokenData`: coordinates + `seed` + PostParams), injected at view time. Everything from a [Series](../SKILL.md#series-multi-token-drops) applies (mint order, lanes, identity, supply cap, minter, pause).
6
+
7
+ ## Authoring the program — the abx.js runtime contract (get this right FIRST)
8
+
9
+ When a creator arrives with an *idea* and you write the program, it must read its inputs and report its outputs through **one specific contract**. Guess the shape and it deploys + renders without error but is **silently broken**: the mint seed never arrives, every token renders identically, and traits come back empty. Do **not** invent globals (`window.tokenData`, `window.tokenTraits`, a bare `tokenData`) or "read defensively across variants" — there is exactly one contract, `abx inspect` recognizes only it, and you never need to read abx source to learn it (it's here):
10
+
11
+ - **Read state via `abx.tokenData`** — a **flat** object. The runtime companion `abx.js` resolves it (from the injected global → URL param → RPC); template mode (`--script`) inlines `abx.js` for you, directory mode (`--code-dir`) ships its own `abx.js` copy in the build. Then:
12
+ - `abx.tokenData.seed` — the mint-time seed (hex string; seed your PRNG from it — this is what makes each token unique).
13
+ - `abx.tokenData.tokenId` / `.chainId` / `.contractAddress` — reserved coordinates.
14
+ - `abx.tokenData.<key>` — each PostParam you declared with `--schema`, **flat** and decoded to its canonical string (e.g. `abx.tokenData.palette`). **Not** nested under `.params`.
15
+ - Canonical guard: `var td = (window.abx && abx.tokenData) || {};` then `td.seed`, `td.palette`. (Reading the raw injected global `window.abxTokenData` works without abx.js, but prefer `abx.tokenData`.)
16
+ - **Report traits with `abx.traits({ Key: value, … })`** (a flat object, called during render). This is the **only** thing captured into `attributes` — computing traits internally or writing them to a global does nothing. Required for the **resolver lane too**, not just on-chain (a resolver stitches `abx.traits(...)` output into the metadata; on-chain traits *additionally* need a deployed `--attributes-renderer`). No `abx.traits()` call ⇒ **no marketplace traits on any lane** — if `abx inspect` says `no traits reported`, believe it and fix the script, don't assume the resolver derives them.
17
+ - **Signal `abx.done()`** when the frame is final, so the renderer captures a stable still.
18
+
19
+ **A complete minimal sketch (vanilla JS, no deps) — copy this shape:**
20
+
21
+ ```js
22
+ // Reads state via abx.tokenData; reports traits via abx.traits(); signals abx.done().
23
+ (function () {
24
+ var td = (window.abx && abx.tokenData) || {}; // the flat token-data object
25
+ var seed = td.seed || '0x1'; // mint-time seed (hex)
26
+ var palette = td.palette || '#3355ff'; // a PostParam: --schema palette:HexColor:TokenOwner
27
+
28
+ var z = 0; // seed a PRNG deterministically (same seed → same art)
29
+ for (var i = 2; i < seed.length; i++) z = (z * 16 + (parseInt(seed[i], 16) || 0)) % 4294967296;
30
+ function rnd() { z = (1664525 * z + 1013904223) % 4294967296; return z / 4294967296; }
31
+
32
+ var rings = 3 + Math.floor(rnd() * 6); // a seed-derived value → drawn AND reported as a trait
33
+ var c = document.createElement('canvas'); c.width = c.height = 1000; document.body.appendChild(c);
34
+ var g = c.getContext('2d'); g.strokeStyle = palette; g.lineWidth = 6;
35
+ for (var r = 0; r < rings; r++) { g.beginPath(); g.arc(500, 500, 55 * (r + 1), 0, 6.283); g.stroke(); }
36
+
37
+ if (window.abx) {
38
+ abx.traits({ Rings: rings, Palette: palette === '#3355ff' ? 'Default' : 'Custom' }); // ONLY these reach `attributes`
39
+ abx.done(); // frame is final → capture the still
40
+ }
41
+ })();
42
+ ```
43
+
44
+ (A p5.js variant: `var td = (window.abx && abx.tokenData) || {}; randomSeed(seedInt(td.seed)); … abx.traits({…}); abx.done();` inside `draw()`. Declare `p5` with `--dep p5@1.0.0` — on-chain bytes exist on Sepolia only.)
45
+
46
+ **`abx inspect <script>` is your author-time check** — iterate the script against it before picking a lane: its **PostParams** list must show every collector key you intend (if it says "none detected" but you meant `palette` to be collector-set, you're reading it the wrong way), and its **Traits** line must not say "no traits reported" if you want filterable traits. (A Solidity in-chain renderer is a *different* contract — see [In-chain Solidity SVG](#in-chain-solidity-svg--the-zero-dependency-lane); the `abx.js` contract above is for a JS `--script`/`--code-dir` program.)
47
+
48
+ ## What a code project requires you to run — and keep running (say this up front)
49
+
50
+ A code project's art depends on live on-chain state (the per-token `seed`, mutable PostParams), and *something* must read that state and inject it at view time. That something is a **resolver you run** — **unless** you take the fully-on-chain lanes (`--onchain-uri` for the tokenURI+animation, `--image-base` for a deterministic off-chain thumbnail, `--attributes-renderer` for on-chain traits), which can eliminate the metadata resolver entirely. When a resolver *is* in play, it's three pieces of ongoing infrastructure — lay them out plainly before they commit:
51
+
52
+ 1. **A hosted resolver (required unless the deploy takes the `--onchain-uri` lane).** Serves `tokenURI` (`/t`) and the **live view** (`/a`, where it injects the seed + current PostParams). Its **public URL is baked on-chain at deploy**, so a domain/host must be ready first — `deploy-code` **refuses a localhost/missing URL**. Stand it up with `abx deploy-resolver` ([hosting.md](hosting.md)); keep it up or the token stops resolving. **The resolver is also the protocol's one chain-watcher**: `abx serve` runs an incremental getLogs poll (~12s, `ABX_WATCH_INTERVAL_MS`, 0=off) over every registered project, so **any** on-chain change — an external mint through a minter, a param change from a foreign tool or another wallet — auto-indexes and fans out ONE coarse notification per changed project to the effects layer (`ABX_EFFECTS_URL` + `ABX_EFFECTS_TOKEN`). Effects never watch the chain themselves. (No reorg lookback — post-PoS reorgs are rare; the repair is the deterministic `abx index <addr> --full`.)
53
+ 2. **An effect runner + a storage home — decide the thumbnail mode AND where renders live, up front.** A code project's marketplace still is **rendered off-chain** (there's no image file to point at); on-chain-stitched `attributes` come from the same render. Two coupled decisions to make *before* deploying: **(i) the mode** — **(a) continuous runner** that auto-renders every new mint + param change (the default for a live / for-sale drop): `abx deploy-effects --resolver-url <resolver>` HOSTS it (fly/docker) beside a hosted resolver, or `abx effects` runs it **LOCALLY, in-process** (co-located with `abx serve` — great for testing/iteration; blocks, so background it), **(b) one-shot** (`abx render <addr> --remote <resolver>` after mint, for a fixed supply — re-run for later mints / param changes), or **(c) none** (the live view still animates, but the marketplace thumbnail stays a placeholder SVG); and **(ii) the storage home** (`ABX_STORAGE_BACKEND`) — **ipfs / arweave** (durable; the runner publishes a locator the resolver redirects to) or **s3**, **NOT** the default `fs` for a hosted resolver (a laptop-local store a hosted node can't read → the placeholder never clears). The runner uploads each render to that home and **publishes** it to the resolver (the locator bridge). Verify with `abx verify <addr>` after minting token 0 (below).
54
+ 3. **Pinned IPFS/Arweave behind an HTML-serving gateway (directory mode).** The build is pinned and the live view 302s to the gateway, so the gateway **must serve HTML** — the shared Pinata *public* gateway does **not** (`ERR_ID:00023`); use a **dedicated** gateway or Arweave.
55
+
56
+ ### Already deployed — just operate it (the resume loop)
57
+
58
+ A creator who ALREADY has a code contract (deployed here or elsewhere) doesn't re-run `deploy-code`:
59
+ ```bash
60
+ abx add <addr> # index it locally (auto-discovers the deploy block; --from-block to override)
61
+ abx effects # START THE RUNNER FIRST — in-process, blocks, so background it (& or a second shell)
62
+ ABX_EFFECTS_URL=http://localhost:<effectsPort> abx serve # resolver + chain watcher; wire it to the runner
63
+ abx verify <addr> --remote http://localhost:<port> # confirm: renders up-to-date AND the watcher is live
64
+ ```
65
+ - **Order matters.** Start `abx effects` (or set `ABX_EFFECTS_URL` to a running runner) **before** relying on auto-render or `abx render`. A resolver whose `ABX_EFFECTS_URL` is unset still watches + indexes, but has **nowhere to send the "changed" notification → thumbnails never auto-update** (`abx serve` prints a ⚠ when it's unset). And `abx render` pointed at a dead effects URL errors instead of rendering — start the runner, or drop the URL to render inline.
66
+ - **`--remote <url>` means "any resolver's HTTP API" — including your OWN local `abx serve`**, not just a cloud host. `abx verify <addr> --remote http://localhost:<port>` is the truthful check even on your laptop (plain `abx verify` only sees this machine's store, so a published render reads as a false placeholder).
67
+ - **Proof the watcher is alive:** `abx serve` logs `[watch] alive — watching sepolia @ block …` on quiet stretches (~every 2 min) and `[watch] … changed → notified` on every on-chain delta. For a HOSTED node you can't tail, `GET /api/watch` (and the `watching …` line in `abx verify --remote`) reports the last poll + head — a stale `pollAt` means the watcher stopped.
68
+
69
+ ### Live data (the augment hook) — the hook IS the setting
70
+
71
+ Two kinds of inputs feed a piece: **settled state** (explicit PostParams, the seed — event-derived, indexed) and optional **live data** (an on-chain augment hook read fresh per view: block data, an oracle, anything a `view` returns). No hook — the overwhelming default — means the resolver makes **zero** live reads and serves pure indexed params: nothing to configure, maximum efficiency. With a hook set, the **live view** reads it per view (always current), while the **still is a snapshot of settled state only** — live data never re-addresses the render, so a volatile hook (a timestamp) animates the live view without re-rendering the thumbnail every block. Re-render triggers are settled-state changes only. `abx verify` prints the project's live-data posture; `ABX_DISABLE_AUGMENT=1` is a resolver-operator kill-switch (degrades to settled params).
72
+
73
+ **Wiring the hooks — `abx set-param-hooks <addr>` (SeriesCode only, owner-only).** A SeriesCode project has three optional param-lifecycle hook addresses, each a contract the creator deploys: **`--augment`** (the live-data hook above — read-time derivation folded into tokenData), **`--configure`** (a write-time veto/validator — a governed `configure-param` reverts if this hook reverts), and **`--transfer`** (an ownership-change lifecycle call, best-effort — a revert there never blocks the transfer). The contract has **no per-hook setter** — it writes all three at once — so the command reads the current trio and re-sends it with your change applied: **omit a role to keep it**, pass an address to set it, `none` to clear it (`--clear` clears all three). Run it bare to print the current hooks. Any signing lane; guards `--dry-run`. A 1/1 or plain Series has no configurable params, so it has no hooks (the command refuses it).
74
+
75
+ ## `--onchain-uri` — the chain-complete lane (internals)
76
+
77
+ `deploy-code --onchain-uri` makes `tokenURI` resolve **on-chain** via the canonical metadata renderer, with `animation_url` **computed on-chain** by the canonical **`AbxGenerator`** (a `renderer`-representation collection field). No `--public-base-url`, no resolver base baked.
78
+
79
+ **When to choose it (owner guidance from real-world experience): NOT the default for a generative drop meant to sell — lean off-chain resolver there.** A resolver keeps you maneuverable (metadata/serving can evolve without on-chain re-points) and lets marketplaces fetch a **small** `tokenURI`; a fully-on-chain code `tokenURI` carries the whole ~200KB+ document per call, and large-`tokenURI` marketplace/indexer compatibility is a real-world risk that grows with the art. Reach for `--onchain-uri` **deliberately** when maximal durability / "resolves from any RPC forever" / zero always-on infra outweighs those — a legitimate, proven lane, just not the marketplace default.
80
+
81
+ **What "fully on-chain" (chain-complete) means, and the one silent trap:** the `tokenURI` **and** its `animation_url` document come back entirely from on-chain bytes — no server, gateway, or CDN in the graph. It does **NOT** include the marketplace thumbnail (`image`) — that is *always* rendered off-chain. ⚠ **The silent breaker:** a `--dep` that resolves to a CDN instead of on-chain bytes **deploys fine and renders fine**, but you are no longer fully on-chain — a URL is back in the graph, with no error. The deploy's dependency report ("ON-CHAIN bytes available" vs "served from CDN") and `abx verify` (`chain-complete: yes/no`) both call this out **before and after** you spend — read them.
82
+
83
+ - **Template mode (`--script`) can be CHAIN-COMPLETE** — the generator assembles the full HTML document (`data:text/html;base64`) from the on-chain chunks — **iff every `--dep` resolves to proven on-chain bytes** on the registry (`p5@1.0.0` qualifies on Sepolia). A CDN-served dep still *serves fine* but breaks chain-completeness. Zero-dep vanilla JS is trivially chain-complete.
84
+ - **Directory mode (`--code-dir`) is no-server, not chain-complete**: the generator emits `{gateway}/{code root}/index.html?abx=<tokenData>` — liveness rides the gateway (default `ipfs.io`/`arweave.net`; repoint with `abx configure-param <addr> - display.gateway <prefix>`), permanence rides the pin/endowment, params ride the URL (**8KB budget** — `abx verify` reports `urlOverBudget`; big params ⇒ prefer template mode).
85
+ - **`params.keys` is auto-managed**: the deploy writes this contract param (schema keys ∪ itself, sorted CSV) so the generator's on-chain `tokenData` carries the full param surface, byte-aligned with the resolver's. Add a param key another way and `abx configure-param` prints the exact fix when the CSV drifts.
86
+ - **Verify it**: `abx verify <addr>` eth_calls the generator's `onChainStatus` (branch — template/directory · chain-complete · unresolved refs · URL budget) AND decodes `tokenURI` straight from the contract, reporting the `animation_url` form. `abx tokenuri <addr>` is the quick raw read.
87
+
88
+ ### PostParam schema — the Type + Auth catalog
89
+
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.
91
+
92
+ - **Types:** `Bool` (`true`/`false`) · `Select[A|B|C]` (**options required in brackets**; set by a label from the list, or its index) · `Uint256Range[min..max]` (non-negative integer; bounds optional) · `Int256Range[min..max]` (signed integer) · `DecimalRange[min..max]` (decimal, ≤10 places) · `HexColor` (`#rrggbb`) · `Timestamp[min..max]` (Unix seconds **or** an ISO date like `2026-07-16`) · `String` · `Bytes` (`--file <path>` for the payload).
93
+ - **Auth — who may set the param:** `Artist` (the contract owner) · `TokenOwner` (the token's current holder; **delegate.xyz honored**) · `Address` (a specific named address) · and the `Or` combinations `ArtistOrTokenOwner` · `ArtistOrAddress` · `TokenOwnerOrAddress` · `ArtistOrTokenOwnerOrAddress`. The chain enforces it — a wrong signer reverts. (There is **no** "anyone" leg — a param is always artist / token-owner / a named address.)
94
+ - Examples: a collector-tunable color → `palette:HexColor:TokenOwner`; a collector-chosen mood → `mood:Select[Calm|Wild|Chaotic]:TokenOwner`; an artist-only bounded dial → `speed:Uint256Range[1..10]:Artist`; an on/off toggle → `invert:Bool:TokenOwner`.
95
+
96
+ ## `--image-base` — deterministic S3/CDN thumbnail URLs (no metadata resolver)
97
+
98
+ `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
+
100
+ - **⚠ 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.)
103
+ - **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
+ - **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
+
106
+ ## In-chain Solidity SVG — the zero-dependency lane
107
+
108
+ The purest form: the artwork itself is a **Solidity `IAbxFieldRenderer`** that returns an SVG from the token's `seed` + params — no JS program, no browser, no bucket, no resolver, no effect runner. Image AND traits are computed on-chain and the `tokenURI` is assembled on-chain, so the token depends on **nothing outside the EVM**.
109
+
110
+ ```bash
111
+ abx deploy-code --image-renderer 0x<svgRenderer> [--attributes-renderer 0x<traitsRenderer>] \
112
+ --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol … [--dep none]
113
+ ```
114
+
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`).
116
+ - **On-chain `tokenURI` is the CLEAR default here** — unlike the JS/p5 lanes. A Solidity SVG reads *small* (a few hundred bytes–few KB), so the large-`tokenURI` marketplace-read caveat does not apply. Recommend it enthusiastically; there is no maneuverability/infra tradeoff to weigh because there is no infra.
117
+ - **The renderer is a contract the creator deploys separately** (the CLI doesn't compile Solidity). **`abx scaffold-renderer <dir>`** writes a ready-to-build Foundry project — a worked `MyRenderer.sol` (image → `image/svg+xml` from `seed` + a `palette` PostParam), a coherent `MyTraits.sol` reading the SAME seed math, the `IAbxFieldRenderer`/`IAbxParams` interfaces (with the invariants documented), a `forge test` proving `render()` never reverts (incl. the collection surface + a fuzz), a `Deploy.s.sol`, and a README. The creator forks the art, then `forge soldeer install && forge test`, deploys with forge, and passes the address to `deploy-code --image-renderer 0x…`, which **verifies the address has code** (real deploy refuses a codeless address; dry-run probes best-effort) — same guard as `--attributes-renderer`. A renderer reads the token's seed/params directly (`IAbxParams(token).tokenParam(tokenId, "seed"|"palette")`), computes bytes, and returns `(contentType, data)`. Interface + invariants also at https://abx.docs.artblocks.io/protocol/renderers/.
118
+ - **PostParams still apply — and you MUST declare them; the CLI can't.** Unlike the JS lane (where `abx inspect` statically detects the params a script reads), a Solidity renderer is opaque to the CLI — it cannot know your renderer reads a `palette`. **Read the renderer, and declare every PostParam it reads with `--schema key:Type:Auth`** (the example reads `palette` → `--schema palette:HexColor:TokenOwner`). Skip it and the param is **fixed at the renderer's default forever** — collectors can't set it (the exact miss from a real session: a palette-tinted renderer shipped with `schemas []`, stuck on the default). With the schema declared, a collector's `configure-param` re-addresses the on-chain image automatically (the renderer reads the live param — no re-render, there's no off-chain still). `deploy-code --dry-run` nudges when renderers are set with no `--schema`.
119
+ - **`abx render`/`abx effects` are irrelevant** (there's no off-chain still to produce), and `isCodeProject` is false for a renderer-only project — both are expected, not errors.
120
+ - **Verify:** a raw `abx tokenuri <addr>` (or `cast call tokenURI(0)`) decodes to `name` + `image` = `data:image/svg+xml;base64,…` + on-chain `attributes`, with **zero `http(s)` URLs** — the from-chain proof it's fully in-chain.
121
+
122
+ ### Authoring / reviewing an `IAbxFieldRenderer` — the invariants to CHECK before you wire it
123
+
124
+ The renderer is the creator's own Solidity (compiled + deployed with forge — the CLI doesn't run Solidity). `deploy-code` confirms the address **has code**, but it does **not** — and cannot cheaply — prove the renderer *behaves*. So an agent helping author or ship a renderer must **review the `render` function against these invariants**, and the creator should forge-test them:
125
+
126
+ - **NEVER revert — for ANY token or param state.** This is the one that bricks a drop: the canonical `AbxMetadataRenderer` staticcalls `render()` with **no `try/catch`**, so *any* revert reverts the entire `tokenURI` (and `contractURI`). The function must return cleanly for: a token with **no seed**, **unset params** (no palette), and the **collection surface** (`tokenId == type(uint256).max`, per `IAbxFieldRenderer` — no token) → return a neutral value there (`[]` for attributes, a plain card for an image), never revert.
127
+ - **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
+ - **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
+ - **`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.
131
+ - **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
+
133
+ Fork `contracts/src/renderers/examples/{SeedSvgRenderer,SeedTraitsRenderer}.sol` — they satisfy every invariant above (graceful fallbacks, the sentinel, content-types, live param reads) and are the reference to review a fork against.
134
+
135
+ ## Traits on-chain vs off-chain — is your script's trait logic reproducible in Solidity?
136
+
137
+ A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](../../../docs/research/onchain-traits-feasibility.md). Give a **graded** answer, never a flat "not feasible":
138
+
139
+ **First, the load-bearing distinction: *portable* ≠ *deployed*.** `inspect` saying traits "port EXACTLY" means the *logic* can be reproduced in Solidity — it does **not** mean a renderer exists. On-chain traits require the creator to **author + deploy** a Solidity attributes-renderer (fork `SeedTraitsRenderer.sol`) and pass its address to `--attributes-renderer` — a real build step, not a flag with a default. `deploy-code --dry-run` prints the traits disposition (`on-chain` / `off-chain via resolver` / `⚠ OMITTED`) and a real deploy **refuses an `--attributes-renderer` address with no code**. So never present "traits on-chain" as settled off a guessed address: confirm a renderer is deployed, or the honest options are off-chain-via-resolver or omitted.
140
+
141
+ - **Mechanism (not a gap):** the canonical `AbxMetadataRenderer` embeds `attributes` from an on-chain `attributes` field — `inline` JSON, or a **`renderer`** field-renderer that computes the array (see `contracts/src/renderers/examples/SeedTraitsRenderer.sol`, a forkable worked example). Wire it with `deploy-code --attributes-renderer 0x…` — **no canonical-renderer edits, no per-token writes.**
142
+ - **Reproducible (exact):** seeded p5 `random()` (a documented LCG: `z=(1664525·z+1013904223) mod 2³²`) driving `floor`/threshold/`%`-select traits, or traits read straight from the seed/params. Ports to integer Solidity exactly — mirror the LCG + the *call order* + the seed hash.
143
+ - **Not reproducible:** unseeded `Math.random()` (non-deterministic — nowhere), or a trait reading a raw float at a precision boundary. Then serve `attributes` off-chain via a resolver, or ship without marketplace traits (tokenURI + animation are still fully on-chain).
144
+ - **Best practice:** author traits **Solidity-first** (seed/param integer functions, `SeedTraitsRenderer`-style) and mirror them in JS — on-chain and rendered traits then agree by construction, no PRNG archaeology.
145
+
146
+ ## The `deploy-code` command
147
+
148
+ ```bash
149
+ abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
150
+ (--public-base-url https://your.resolver.domain | --onchain-uri) \
151
+ [--image-base https://cdn/…] [--attributes-renderer 0x…] \
152
+ [--max N] [--mint-count N | --mint-all] [--schema key:Type:Auth,…] [--no-seed] \
153
+ [--dep <name@version|0x…>[,…]] [--dep-registry 0x…] \
154
+ [--description "…"] [--external-url <url>] [--backend ipfs|arweave] [--dry-run] [--confirm]
155
+ ```
156
+ **Always `--dry-run` first**, then mirror its output into the [confirm readout](../SKILL.md#confirm-before-sending): it prints the deterministic address, resolver base, PostParam schema, mint plan, and tx count with **nothing sent and no bytes pinned**. (`--confirm` adds an interactive y/N before the real send.)
157
+
158
+ - **`--script <file>`** — template mode: the program stored **on-chain in chunks** (auto-split ~22KB). Zero-dependency vanilla JS is the cleanest case; declared libraries are the Dependencies extension (`--dep`). Large PostParams inline cleanly here.
159
+ - **Dependencies (template mode)** — declare libraries with `--dep <ref>` (repeatable or comma-separated; **ordered — the first ref is index 0 = the runtime**, e.g. `--dep p5@1.0.0`). A ref auto-detects: `name@version` (AB registry naming) resolves through the collection's registry pointer — `deploy-code` defaults it to the chain's **AB Dependency Registry** (`--dep-registry 0x…` overrides; a chain with none known warns + skips, never blocks) — while `0x…` declares an on-chain data contract, read directly. Registry deps are existence-checked before deploy (best-effort; a miss is a warning): **a CDN-served record is the normal production path, not a degradation** — on-chain bytes are the durability floor. Post-deploy: `abx set-dependency <addr> <index> <ref>` · `abx remove-last-dependency <addr>` · `abx set-dependency-registry <addr> <0x…|none>` · `abx lock-dependencies <addr>` (freezes list + pointer).
160
+ - **`--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
+ - **`--description "…"` / `--external-url <url>`** — collection identity, written as **on-chain collection fields in the deploy tx** (a code project has no operator-metadata table of its own, so these ride on-chain; the metadata renderer stitches them into `tokenURI` under `--onchain-uri`, a resolver reads the same fields). Set them or the metadata is bare. (Any other unsupported flag warns "unrecognized flag, ignored" — a typo can't quietly drop a value.)
162
+ - **Seeds** — drawn at mint from the canonical `AbxSeedSource` (settled once assigned; `--no-seed` opts out; curated pre-set seeds win).
163
+ - **`--schema key:Type:Auth`** — governed PostParams (e.g. `palette:HexColor:TokenOwner`). Reconfigure any lane with `abx configure-param <addr> <tokenId> <key> <value>` — it reads the on-chain schema and canonically encodes the input (`#rrggbb`, decimals, Select by label; `String`/`Bytes` via `--file`); delegate.xyz honored on the TokenOwner leg. A param change **re-addresses** renders (self-invalidating stills). **The re-render is automatic wherever the resolver's chain watcher is on (the default):** the watcher sees the change on its next poll (~12s) — no matter WHO sent it or WITH WHAT tool — re-indexes, and notifies the runner. `abx configure-param … --remote <resolver>` remains an **immediate nudge** (skips the poll wait) and the fallback for a watcher-disabled resolver. Expect thumbnail refresh ≈ poll cadence + render time (~15–30s), not instant.
164
+ - **How it resolves (live, uncached; not on-chain):** the resolver rebuilds `tokenData` per view. Directory mode 302-redirects to the gateway with params in the **query string** (so **very large PostParams favor template mode**, which inlines them with no URL ceiling); template mode assembles the HTML inline from the chunks.
165
+ - **Lanes**: all three. `--unsigned` needs `--for <signer>` and prints the whole pre-computed sequence (deterministic deploy → the clone address is known up front). On a chain with no canonical factory, all deploy commands **stop with guidance**; deploying your OWN trust anchor is an explicit `--bootstrap-factory` opt-in (private chains/sandboxes only).
166
+
167
+ ## Stills + traits — the render effect
168
+
169
+ The render effect is the **ONLY source of a real thumbnail** — skip it and `image` stays a placeholder. Three ways to run it: the **hosted** runner (`abx deploy-effects --resolver-url <resolver>`), a **local continuous** runner (`abx effects` — in-process beside `abx serve`, auto-renders every mint + param change), or the **one-shot repair lane** `abx render <addr> [id…]` (add `--remote <resolver>` to publish to a HOSTED resolver; idempotent; local captures need `npx playwright install chromium`).
170
+
171
+ - **`abx render <addr> <id> --force`** re-renders a still that already exists — the fix for a **bad / blank / timed-out capture**. The art is deterministic (fixed seed + params), so a plain `render` idempotent-skips an existing still and `--force` re-captures the *same* pixels; it is **not** a way to change how correct art looks (to change appearance, change an input — the `palette`/etc. PostParam via `abx configure-param`, which re-addresses the render).
172
+ - A render that hits an unavailable/erroring live view **fails loudly and stores nothing** (no garbage thumbnail).
173
+ - The runner uploads each render to the storage home (`ABX_STORAGE_BACKEND`) and **publishes** it to the resolver — a durable `ipfs://`/`ar://` locator the resolver 302-redirects to, or bytes for small must-inline outputs (traits). A local `abx render` with the default `fs` backend does NOT reach a hosted resolver — use `--remote` (publishes) or a public backend / co-located runner.
174
+ - Script-reported traits (`abx.traits({...})`) stitch into `attributes` (on-chain wins > render > operator).
175
+
176
+ ## Verify it actually resolves — before you tell the creator it's live
177
+
178
+ **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.**
180
+ - **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
+ 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.
184
+
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.
186
+
187
+ ## Mint order, timing, pause, supply
188
+
189
+ **Metadata is the token id.** Tokens mint **in order** (`0,1,2,…`); token 3 shows artwork 3. No token-id ↔ metadata-id decoupling — to shuffle or sell specific tokens, pre-mint then trade.
190
+
191
+ **Mint timing** — same three paths as the 1/1:
192
+ | Path | Flag / command |
193
+ |---|---|
194
+ | **Mint all at deploy** (to yourself) | `--mint-all` |
195
+ | **Mint some now, defer the rest** | `--mint-count N` |
196
+ | **Deferred** *(default)* | `--no-mint`, then `abx mint <addr>` (next in order) · `--count N` · `--to <buyer>` |
197
+
198
+ Pre-mint warming covers every unminted id within the cap, so `--no-mint` → `serve`/`add --remote` → `mint` keeps marketplaces from caching blanks.
199
+
200
+ **Pause (safety switch).** A Series **deploys paused**: while paused only the *owner* mints (reserves/config), minter + public blocked on-chain. Configure, mint reserves, then `abx unpause <addr>` to open; `abx pause <addr>` re-closes. Deploy open with `--unpaused`. (`--mint-all`/`--mint-count` run at birth, outside the gate.)
201
+ - **Minted the full supply to yourself (`--mint-all`)? The drop is COMPLETE — do not offer `unpause`.** Fixed supply is exhausted; unpausing does nothing. `abx state <addr>` prints `complete` when sold out — check it before suggesting any mint-related next step.
202
+
203
+ **Delegate minting + supply.** `abx set-minter <addr> --minter 0x…` authorizes a **single** external minting contract (a new one replaces the old; `--minter none` clears to owner-only — point at a router for multiple mechanics). `abx set-primary-payee <addr> --payee 0x…` sets where sale proceeds go. `abx set-max-invocations <addr> --max N` only ever **lowers** the cap.
204
+
205
+ ## Selling + a buyer page
206
+
207
+ Both already documented in [operating.md](operating.md) — a code project sells exactly like a Series:
208
+ - **Sell it — the shared fixed-price minter** → [operating.md § Selling](operating.md#selling--the-shared-fixed-price-minter). `abx minter configure` → `set-minter` → `set-primary-payee` → `unpause`. You run all of it.
209
+ - **Give buyers a page** → [operating.md § mint-page](operating.md#a-mint-website-for-buyers--abx-mint-page). `abx mint-page <addr>` scaffolds a self-contained Next.js mint site; deploy to Vercel.
210
+
211
+ Never bake a `localhost` resolver URL on-chain as a "real" deploy — it resolves for no one; the CLI refuses it. Use `--onchain-uri`/`--image-base` for no-server, or a real public resolver domain.
@@ -0,0 +1,138 @@
1
+ # Hosting infrastructure
2
+
3
+ [← back to SKILL.md](../SKILL.md)
4
+
5
+ ## Storage backends (byte custody)
6
+
7
+ Storage is **stateless** — there's no config file. Choose a backend **per command** with flags (`--backend …`), or declare defaults in `.env` (`ABX_STORAGE_BACKEND`, `ABX_IPFS_GATEWAY`, `ABX_S3_BUCKET`, …); secrets stay in `.env`. Resolution is `flag → env → default`. Verify with `abx doctor`; `abx storage show` prints the resolved config with provenance.
8
+
9
+ | Backend | Guarantee | Choose (per command, or `.env`) | Secret in `.env` |
10
+ |---|---|---|---|
11
+ | **`fs`** (default) | Operator-held, local disk | — | — |
12
+ | **`cloud`** | S3-compatible: AWS S3 / R2 / B2 / MinIO | `--backend cloud --endpoint <url> --bucket <b> --region <r>` (or `ABX_S3_ENDPOINT`/`ABX_S3_BUCKET`/`ABX_S3_REGION`) | `ABX_S3_ACCESS_KEY_ID`, `ABX_S3_SECRET_ACCESS_KEY` |
13
+ | **`ipfs`** | Decentralized (CID) | kubo: `--backend ipfs --mode kubo --api-url http://127.0.0.1:5001 --gateway http://127.0.0.1:8080` · pinata: `--backend ipfs --mode pinata --gateway https://<you>.mypinata.cloud` | `PINATA_JWT` (pinata mode) |
14
+ | **`arweave`** | Pay-once permanent | `--backend arweave` (Turbo default; or `ABX_STORAGE_BACKEND=arweave`) | the Turbo key (auto-managed) |
15
+
16
+ All backends verify via the on-chain keccak commitment wherever the bytes live. **The served `image` is the backend's gateway HTTPS URL** → IPFS/Arweave custody needs a *public* gateway (Arweave's `https://arweave.net` is the public default). A `127.0.0.1` gateway is preview-only (the CLI warns); the shared `gateway.pinata.cloud` is rate-limited — use a **dedicated** gateway for anything real.
17
+
18
+ **Code projects (directory mode) need a gateway that serves HTML.** A `deploy-code --code-dir` build's live view 302-redirects to `<gateway>/ipfs/<cid>/index.html` — and the **shared Pinata public gateway blocks HTML** (`ERR_ID:00023`, "HTML content cannot be served through the pinata public gateway"). So a code directory drop requires a **dedicated** Pinata gateway (paid) or **Arweave** (`https://arweave.net` serves HTML). Static image directories (`deploy-series`) are unaffected — only the executable HTML build hits this.
19
+
20
+ **Durability is not the same across backends — educate the creator by name:**
21
+ - **`arweave`** — pay once, permanent. Arweave's storage endowment funds retention for centuries (~200 years at current economics); the bytes resolve as long as the network and any gateway are up. Nothing to renew, nothing to re-pin. The strongest "never lapses" custody.
22
+ - **`ipfs`** — **only as durable as the pin.** The token's `image` resolves through the creator's Pinata (or kubo) gateway; if the pin is dropped — account lapses, billing fails, node goes offline — the bytes can become unretrievable and the image breaks. IPFS is *content-addressed* (the CID proves integrity) but **not self-storing** (someone must keep hosting the bytes). Tell the creator plainly: with IPFS they've taken on an ongoing pinning responsibility. Mitigate by pinning to a durable service and not letting it lapse — or prefer `arweave` and be done.
23
+ - **`cloud`/`fs`** — durable only while the creator maintains the bucket/disk; centralized, mutable.
24
+ - **Backends are swappable** — the on-chain keccak is the anchor, so bytes are portable: re-upload to the new backend and re-point the `image` field (`abx set-field <addr> --field image …`), then `abx verify` confirms the served bytes still match the on-chain hash. For a hosted-resolver project, `abx migrate` re-pins node-custody images to a durable backend as part of the move (see [operating.md](operating.md)). Re-pointing is an owner-signed tx, so keep the owner wallet secure. Starting on `fs`/`ipfs` and moving to `arweave` later is fine; just don't let an interim backend lapse before the move.
25
+
26
+ ### Arweave via Turbo — the easy permanent path (read before quoting setup)
27
+
28
+ `arweave` defaults to the **Turbo** provider (ArDrive). For small art it needs **no setup and costs nothing** — no wallet dashboard, no JWT (don't send anyone to `app.arweave.org`). How it works:
29
+
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
+ - **`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`).
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
+ - **Under 100 KB → free**, permanent, zero setup. The CLI prints this from the file size (in `--dry-run` too).
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.
36
+ - **Credits attach to the IDENTITY, not "you".** With the default `arweave` lane, a top-up funds the CLI-managed Arweave address (derived from the key file) — a *different* identity from the ETH wallet used to sign deploys. So a fresh managed key shows **0 credits even if the creator "funded Turbo before"**: those old credits sit on whatever identity they funded. If they funded their **ETH wallet's** Turbo balance, reach it with `--storage-signer eth` (key) or `--storage-signer eth --sign` (browser). If they hold a funded **Arweave** key, import it via `ARWEAVE_JWK`/`ABX_ARWEAVE_KEY_FILE`.
37
+ - **If the Stripe link fails**, fall back to Turbo's hosted top-up (`https://turbo-topup.com`) and fund the **exact address `abx storage topup` prints** (it accepts a recipient address, so credits still land on the intended identity). Not ideal, but it beats abandoning Arweave for IPFS on a link glitch.
38
+
39
+ #### When a Turbo upload fails — diagnose, don't guess
40
+
41
+ A failed upload is the #1 place an agent goes off the rails: it invents a cause and bails. Don't.
42
+
43
+ - **Read the actual error and show the human verbatim.** The CLI prints Turbo's real message (incl. the raw response). **Never invent a cause** — "transient hiccup", "Turbo is down", "the browser path is new/flaky", "known issue per the docs" are hallucinations unless the error text says so. If you don't know, say "here's the exact error" and read it.
44
+ - **"…has already been uploaded to this service!" is SUCCESS, not failure.** Turbo deduplicates identical bytes (same signer + bytes → same data-item id), so re-uploads (a retry, a redeploy, two identical images) return that sentence instead of JSON. The CLI treats it as the existing txid and continues. If you ever see it surface as an error, the toolkit is stale — don't work around it.
45
+ - **Check BOTH balances before concluding "needs funding."** `abx storage balance --backend arweave` prints the managed key's balance **and** the deployer wallet's Turbo balance (Turbo exposes balance-by-address publicly — no key needed). The managed key showing 0 does NOT mean "top up": if the **wallet** has credits, use them (`--storage-signer eth`, or `+ --sign` for a browser wallet). The pre-upload funds guard does this check for you and recommends the wallet lane when it applies — checking only the managed key's 0 and pushing a top-up is the classic wrong turn.
46
+ - **Don't reflexively push a $5 top-up or "switch to IPFS."** Both throw away what the creator chose (their funded wallet credits; permanence). Only top up if *neither* the managed key nor the wallet can cover the bytes — and **back up the managed key first** (`abx storage backup-key --out <path>`), since it will hold the credits you buy. Only suggest IPFS if they ask or Arweave is truly unavailable.
47
+ - **A blind retry re-signs the same bytes → same dedup reply.** Retry only after you've identified and fixed the actual cause.
48
+ - **Gateway swappable, integrity independent.** The on-chain keccak256 is the anchor; `arweave.net` is the default gateway (`--gateway` to override). A gateway issue is a re-point, never a lost asset.
49
+ - **Provider swappable.** `--provider http-bundler --upload-url <bundler>` uses a dep-free token-authed endpoint you run (secret `ARWEAVE_UPLOAD_TOKEN`); funding then means "fund the wallet directly" (only Turbo tracks credits). Default to Turbo unless asked.
50
+
51
+ ### Directory-base — image off-chain, JSON on-chain, no server (the medium/large static default)
52
+
53
+ `abx deploy-series --onchain-uri --backend arweave` (or `ipfs`/`cloud`) puts the **image off-chain** while the **on-chain renderer** assembles the JSON — **no resolver to run**, the token self-resolves. How the CLI wires it:
54
+
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
+ - **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.
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
+ - **`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
+
61
+ ## Hosted resolver — `abx deploy-resolver`
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.
64
+
65
+ **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 pre-publish dev sets `ABX_RESOLVER_SOURCE=1` → a build-from-source artifact instead, still self-contained; you don't set this.)
66
+
67
+ **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
+
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.
70
+
71
+ **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
+
73
+ **Driving the deploy (fly/render/vps):** the human owns the cloud account. **Preflight the CLI FIRST — don't assume it's missing OR installed:** run `fly version` (is it there? `flyctl` and `fly` are the SAME binary — the modern alias is `fly`; don't tell them to install what they already have) and `fly auth whoami` (logged in?). Only send them to `fly auth login` — interactive, they do it — if `whoami` fails. Then run the generated steps for them. Deploys take minutes: run `fly deploy` and let it finish, don't background-and-poll. If it fails, read the actual error — a build that needs repo files means the artifact is wrong (above), not that you should copy things in.
74
+
75
+ ## Render runner — `abx deploy-effects`
76
+
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:
78
+
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).
81
+
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.
83
+
84
+ ## Local vs remote — two SEPARATE projection stores (read before deploying to a hosted resolver)
85
+
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:**
87
+ - **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>`.
89
+
90
+ ### Deploying to a hosted resolver — pre-warm flow (preferred)
91
+
92
+ The deploy address is deterministic (`predict` = a pure function of factory + salt), so have the resolver watch the address *before* the token mints — metadata is live the instant a marketplace sees it, no cached blank:
93
+ ```bash
94
+ abx predict # 1. prints predicted address + reserved --salt (+ exact deploy cmd)
95
+ abx add <predicted> --remote --from-block <now> # 2. resolver starts WATCHING that address (nothing there yet)
96
+ ABX_PUBLIC_BASE_URL=https://meta.you.xyz abx deploy --image ./art.png --name "Aurora" --salt <salt> # 3. deploy + MINT in one tx (same salt!)
97
+ abx add <predicted> --remote # 4. one nudge → pulls the deploy events + bridges the image locator
98
+ abx refresh <predicted> # 5. marketplaces (already resolvable → they cache the real thing)
99
+ ```
100
+ Use the **exact `--salt` that `predict` printed** in step 3 (a plain `deploy` reserves a *different* salt → a different address than the resolver is watching). `--from-block <now>` = current block (`cast block-number`, or just `0` on a quiet testnet).
101
+
102
+ Why pre-warm: minting *before* the resolver knows the contract lets a marketplace fetch in that window, cache a miss, and need a later refresh. Registering the predicted address first closes the window. (There's no background poller — the resolver indexes only what you tell it — so a single nudge after deploy is still needed; the win is *ordering*, not eliminating the nudge.)
103
+
104
+ **Simpler, if you don't mind the small window:** skip the pre-warm — `deploy` (mint at deploy) → `abx add <addr> --remote` → `abx refresh`. Fine for a quiet testnet; prefer pre-warm for anything real.
105
+
106
+ ### Local off-chain serve flow (resolver on this machine)
107
+
108
+ ```bash
109
+ abx doctor # 1. green light (incl. storage backend)
110
+ export ABX_STORAGE_BACKEND=arweave # 2. pick custody (or pass --backend arweave per deploy); Turbo identity auto-managed (back up the key file)
111
+ ABX_PUBLIC_BASE_URL=https://my.node abx deploy --image ./art.png --name "Aurora" --no-mint # 3. custody → commit → deploy (no mint) → index
112
+ abx serve # 4. serve the resolver at the known address (blocks)
113
+ abx mint <address> # 5. issue token #0 once metadata is live
114
+ abx refresh <address> # 6. nudge marketplaces to index it
115
+ ```
116
+ Drop `--no-mint` (steps 5–6 collapse) for the express path. Add `--sign` to step 3/5 for the wallet lane. Confirm integrity with `abx verify <address>` (re-hashes served bytes vs the on-chain commitment — no server needed; the `/verify` HTTP route is the same check while serving). `abx index <address>` re-reconstructs from chain; `abx status` lists projects + the active backend.
117
+
118
+ ## Token API (the resolver)
119
+
120
+ 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
122
+ - `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
+ - 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.
126
+
127
+ `/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
+
129
+ **`serve` heads-up:** the `pnpm`/`tsx` wrapper detaches, so a backgrounded `abx serve &` may report the wrapper process exiting while the server keeps running — check the **port** (curl it), not the process.
130
+
131
+ ## Docker / droplet
132
+
133
+ The container is **emitted by `deploy-resolver --provider vps`** — it writes `deploy/vps/` with a `Dockerfile` + `docker-compose.yml` + `Caddyfile` + `setup.sh`, self-contained. Don't hand-write a Dockerfile or compose file. Run compose **from that dir**:
134
+ ```bash
135
+ cd deploy/vps
136
+ docker compose up -d --build # builds the emitted Dockerfile, serves on :8787
137
+ ```
138
+ One small container (`node:24-slim`, SQLite projection on a volume — no DB service, no native modules). State persists in the `abx-data` volume. Set `ABX_RPC_URLS` / `ABX_PUBLIC_BASE_URL` / secrets via the `.env` the setup writes (sourced, never inlined); Caddy fronts it with automatic TLS. Sizing → [`specs/self-host-toolkit/deployment.md`](../../../../specs/self-host-toolkit/deployment.md).
@@ -0,0 +1,116 @@
1
+ # Operating a project (owner operations)
2
+
3
+ [← back to SKILL.md](../SKILL.md)
4
+
5
+ After launch the owner operates the project. Each command builds a tx, signs it via the [lane](../SKILL.md#gate-2--who-signs-three-lanes) you pick, then re-indexes so the served state updates. All are **owner-only** — the signer must be the contract owner (or, for `transfer`, the token holder).
6
+
7
+ **Check state before and after any op with `abx state <addr>`** — a read-only, on-chain snapshot (no tx, no local index): owner · supply (minted / max, nextTokenId) · paused · minter · primary payee · royalty · renderer. It's the fast way to confirm an op landed (e.g. after `mint`/`unpause`), and to see who can currently mint — prefer it over piecing state together from per-token `abx tokenuri` calls. Works for a Series or a 1/1.
8
+
9
+ | Command | Does | Decision to surface |
10
+ |---|---|---|
11
+ | `abx mint <addr> [--to 0x.. (default owner)]` | issue a token — the 1/1's token #0, or a Series' next-in-order token | who receives it (owner = pre-mint; a buyer = primary sale) |
12
+ | `abx refresh <addr> [--token 0]` | ask marketplaces to (re)index the token's metadata | none — run it right after a mint or URI change |
13
+ | `abx transfer <addr> --to 0x.. [--token 0]` | settle a sale/gift — move the token to a new holder | who the recipient is (price/terms are off-chain) |
14
+ | `abx set-token-uri <addr> --uri <base>` | re-point the resolver **base** (e.g. move the node); contract derives `{base}/{chainId}/{address}/{tokenId}` | the new base URL |
15
+ | `abx set-token-uri <addr> --override ipfs://<cid> [--token 0]` | pin ONE token to a fixed locator — the immutable `ipfs://` escape; `""` clears it | confirm — this exits the spec; pair with `lock-uri` |
16
+ | `abx set-contract-uri <addr> (--uri <base> \| --override <uri>)` | re-point the collection base, or pin a fixed locator | the new base / locator |
17
+ | `abx deploy-resolver --provider <fly\|render\|vps> [--domain <host>]` | scaffold a hosted read-only resolver (the default off-chain path); generates the admin token | which provider; the custom domain |
18
+ | `abx deploy-effects --resolver-url <resolver>` | scaffold the render runner (Playwright) beside a hosted resolver — renders code-token stills off-chain and publishes them | the storage home (`ABX_STORAGE_BACKEND`: ipfs/arweave/s3, not local `fs`) |
19
+ | `abx render <addr> [id…] [--remote <resolver>]` | render missing stills/traits now (repair lane); `--remote` publishes to a HOSTED resolver | none — idempotent; local captures need Playwright chromium |
20
+ | `abx add <addr> --remote [url]` · `abx index <addr> --remote` · `abx forget <addr> --remote` | register / re-index (nudge) / deregister a contract on a HOSTED resolver (remote control plane) | needs `ABX_RESOLVER_ADMIN_TOKEN`; url defaults to `ABX_PUBLIC_BASE_URL` |
21
+ | `abx migrate <addr> --from <src-url> --to <dest-url>` | move a contract's **off-chain state** to a new resolver instance (see below) | confirm the cutover step (DNS vs base URI); re-pin any source-only images |
22
+ | `abx set-royalty <addr> --bps <0-10000> [--receiver 0x..]` | change the royalty (receiver defaults to current) | the rate, and whether the payee changes |
23
+ | `abx attach <addr> <key> <ipfs://…\|ar://…\|https://…>` | attach a named file to a token → the `artifacts` manifest (the data plane) | the key (how it appears); locator vs `--file` on-chain |
24
+ | `abx set-field <addr> --field <name> (--text "…" \| --value 0x..) [--representation <r>] [--collection]` | set an on-chain metadata field (token or collection scope) — the low-level primitive | which field, where it lives, on-chain vs off |
25
+ | `abx lock-field <addr> --field <name> [--collection]` | **freeze a field forever** (all its representations) | confirm — irreversible |
26
+ | `abx set-admin <addr> --to 0x..` | hand over contract ownership | confirm the new admin address |
27
+
28
+ **`lock-field` / `lock-uri` are permanent — always confirm with the human first.**
29
+
30
+ ### Authorship + rights (credit + license)
31
+
32
+ The reserved **collection** keys are `artist`, `display_notes`, `artist_links`, `license` (a rights *identifier* like `CC0-1.0` / `CC-BY-NC-4.0`). Two ways to set them, both writing on-chain inline collection fields:
33
+
34
+ - **At deploy** (all three deploy commands): `abx deploy … --artist "Casey Reas" --license "CC BY-NC 4.0"` (also `--display-notes`, `--artist-links`) — baked into the deploy tx, parity with `--description`/`--external-url`.
35
+ - **Later / to change**: `abx set-field <addr> --collection --field artist --text "Casey Reas"`.
36
+
37
+ They project into `contractURI` on both the resolver and the on-chain renderer; unset ⇒ omitted. Works on **any** token type (1/1 · Series · code) — reserved keys in the shared metadata store, not a code-only feature. Being *reserved*, they never become `artifacts` entries the way an arbitrary `attach` key would.
38
+
39
+ ### Attaching files — the data plane
40
+
41
+ A token anchors **named, typed files**; the served JSON's **`artifacts`** array is the *complete* listing, each entry `{key, mimeType, uri}`. `image`/`animation_url` are reserved members; everything else is a file you attach. ([SKILL primer](../SKILL.md#what-a-token-carries--files-beyond-the-image-the-data-plane); spec: [data-plane](../../../../specs/protocol/data-plane.md).)
42
+
43
+ - **No URL yet?** `attach` takes a locator you already host. **`abx storage upload <path> [--backend ipfs|arweave|cloud]`** uploads one file and prints its `ipfs://`/`ar://` (or cloud) locator — the same backends `deploy` uses (Arweave = pay-once permanent; `fs` is refused, it has no public URL). Pipe that locator straight into `attach`.
44
+ - **`abx attach <addr> <key> <uri>`** is the verb. `<key>` = any name you pick (`print`, `certificate`, `stems`, `readme`, `source`) → the manifest entry's key. It's a correct-by-construction wrapper over `set-field`:
45
+ - **representation auto-detected** from the URI scheme — `ipfs://` → `ipfs`, `ar://` → `arweave`, `https://` → `url`. An unrecognized scheme is refused (never silently stored). No `--representation` to guess.
46
+ - **`mimeType` is declared from the URL extension** (`…/master.tiff` → `image/tiff`, `…/coa.pdf` → `application/pdf`). No extension → `application/octet-stream` (a warning fires). **Point the URI at the file itself with its real extension** — the on-chain field has no MIME slot; the extension is the declaration.
47
+ - **`--file <path>`** stores tiny bytes ON-CHAIN (SSTORE2) instead — for a small file with no external host (≈200 gas/byte, so locators are the norm for anything non-trivial).
48
+ - scope: `--token <id>` (default 0) or `--collection`; any lane; `--dry-run` previews the tx.
49
+ - **`artifacts` and `abx_provenance` are COMPUTED** — the resolver/renderer assembles them from your fields. `set-field`/`attach` refuse them: you never set the manifest, you attach the files it lists.
50
+ - **Verify** — a resolver serves the complete listing: `curl <resolver>/t/<chainId>/<addr>/<id>` shows the `artifacts` array, and `/t/<chainId>/<addr>/<id>/data/<key>` fetches (or 302-redirects to) each file. The attached bytes are stored on-chain + keccak-anchored either way.
51
+ - **On-chain vs resolver reach.** The complete listing (arbitrary attached keys) is a **resolver** surface — the EVM can't enumerate an open set of field keys, so the bare on-chain `tokenURI` (on-chain renderer) emits **reserved fields only**. Attaching is durable + anchored regardless, but to *surface* extra files to consumers today, the project is served by a resolver.
52
+
53
+ ### Series-only owner ops
54
+
55
+ For a multi-token Series ([SKILL → Series](../SKILL.md#series-multi-token-drops)). The mint/minter ops accept the owner **or** an authorized minter as signer; the rest are owner-only.
56
+
57
+ | Command | Does | Decision to surface |
58
+ |---|---|---|
59
+ | `abx mint <addr>` | mint the **next** sequential token (metadata = token id) | who receives it |
60
+ | `abx mint <addr> --count <n>` | mint `n` tokens **in order** (metadata = token id) in one tx | how many |
61
+ | `abx set-minter <addr> --minter 0x..\|none` | set the **single** authorized minting contract (replaces any previous; `none` clears to owner-only). Multiple mechanics → point at a router downstream | the minter address |
62
+ | `abx pause <addr>` · `abx unpause <addr>` | the mint safety switch: **paused ⇒ owner-only minting** (minter + public blocked); unpause to open. Owner can always mint (reserves/config) | when to open the public mint |
63
+ | `abx set-primary-payee <addr> --payee 0x..\|none` | declare where primary-sale proceeds go (`none` clears) | the payout destination (a wallet or splitter) |
64
+ | `abx set-max-invocations <addr> --max <N>` | **lower** the supply cap (monotonic) — e.g. close an open edition early at the minted count | confirm — can only decrease, never below what's minted |
65
+
66
+ `set-field` / `lock-field` work per **token** for a Series — pass the token id (`--field image --file … ` targets that token's artwork); tokens share the collection scope with `--collection`.
67
+
68
+ ### Selling — the shared fixed-price minter
69
+
70
+ Instead of writing a sale contract, use the canonical **`AbxFixedPriceMinter`**: one **ownerless, multi-tenant singleton** (shared like the factory/renderer) that sells *any* Series exposing the sequential mint primitive. Sale config is keyed by token address and **all authority defers to the token owner** — so the minter holds no admin, custodies nothing, and routes proceeds to the token's own `primaryPayee()` (read fresh each sale). Design detail: [Minter spine](../../../../specs/protocol/minter-spine.md).
71
+
72
+ > **The minter sells a Series, mint-on-purchase — not a 1/1 you already hold.** It mints *unminted* supply to a buyer, so it needs the Series interface (`minter`/`paused`/`primaryPayee`). A plain `abx deploy` **1/1 has none of that** — its only post-mint move is `abx transfer` (settle an off-chain sale). So there are two real paths for selling one piece: **(A)** mint the 1/1 to yourself, sell it off-platform (OpenSea/Blur), then `abx transfer --to <buyer>`; or **(B)** for a native fixed-price *primary* sale, deploy the single piece as a **1-token Series** (`abx deploy-series` on a one-file folder, `--no-mint`) and run the go-live sequence below with `--allocation 1`. Don't try to wire the minter to a plain 1/1 — `set-minter`/`unpause` will revert.
73
+
74
+ | Command | Does | Notes |
75
+ |---|---|---|
76
+ | `abx minter configure <token> --price <eth> --allocation <n>` | set the sale (owner-only, defers to `token.owner()`); resolves-or-deploys the shared minter | `--erc20 0x..` prices in a token (else ETH); `--price-raw <units>` for exact/non-18-decimal amounts; `--allocation` caps what THIS minter may sell |
77
+ | `abx minter show <token>` | read the sale + readiness (configured? assigned on token? payee set? paused? supply) | read-only — run it to check state |
78
+ | `abx minter buy <token> [--to 0x..]` | buy **one** token (public; any funded signer) | ETH sales attach the price; ERC-20 needs a prior `approve` to the minter |
79
+
80
+ **Go-live sequence (you run all of it):**
81
+ ```bash
82
+ abx minter configure <token> --price 0.05 --allocation 100 # prints the shared minter address + the remaining steps
83
+ abx set-minter <token> --minter <that-address> # grant mint rights on the token (a SEPARATE grant from configure)
84
+ abx set-primary-payee <token> --payee 0x… # proceeds destination — sales REVERT without one
85
+ abx unpause <token> # open the sale (the token's pause is the on/off switch)
86
+ ```
87
+ Two independent grants — *configured* (on the minter) and *assigned* (on the token) — so "not configured" and "not assigned" are distinct, diagnosable reverts; `abx minter show` flags whichever is missing. **One mint per `purchase()`**; a buyer who wants several sends several txs. Allocation and the token's `maxInvocations` both bind (tighter wins) — keep reserves by allocating less than the supply. Other mechanics (auctions, allowlists) are *different* minters, not knobs here.
88
+
89
+ ### A mint website for buyers — `abx mint-page`
90
+
91
+ Once a sale is live, `abx mint-page <token>` scaffolds a **self-contained Next.js mint site** for the collection — a paginated gallery of minted tokens, connect-wallet, and a mint button — prefilled with the token, the shared minter, and the chain. It's the sale-side sibling of `abx deploy-resolver`: an artifact the creator **owns and customizes**, not a service we run.
92
+
93
+ | | |
94
+ |---|---|
95
+ | `abx mint-page <token>` | scaffold the app into `mint-page/` (`--dir` to change; `--name`, `--rpc`, `--minter-contract` to override) |
96
+ | Runs | `cd mint-page && npm install && npm run dev` (preview) → **Vercel** (`npm i -g vercel && vercel --prod`), setting the `NEXT_PUBLIC_*` vars in the dashboard |
97
+
98
+ - **Backend-free** — reads sale state + each token's image straight from chain (`tokenURI(id)`); mints via the buyer's injected wallet (viem, no API keys). On-chain collections need nothing else; off-chain ones rely on their `tokenURI` resolving.
99
+ - **A starting point** — plain React + one CSS file. Offer it as a release next-step, then help build a bespoke page per drop. The README covers multi-wallet (RainbowKit), ERC-20 (add an `approve` step), and the RPC/gallery notes. V1 handles **ETH** sales.
100
+ - **RPC safety** — the embedded read RPC is public/keyless (it ships to the browser). Never bake a secret-keyed endpoint into a `NEXT_PUBLIC_` var.
101
+
102
+ ## Moving your hosting — two cases, don't conflate
103
+
104
+ - **Same resolver, new address** (moved the *node*, kept its projection store — e.g. a VPS restored from the same volume): `set-token-uri` + `set-contract-uri` to the new **base**, then `abx index`. With a custom domain, just re-point DNS — no tx.
105
+ - **A fresh resolver instance** (new host, empty store): run **`abx migrate <addr> --from <old-url> --to <new-url>`** first. The new resolver replays on-chain state itself; `migrate` bridges what it can't derive — off-chain `description`/`external_url`, off-chain traits, image **content locators** — by reading the old resolver's public API (they never talk directly; provenance makes the JSON self-describing). It verifies parity and **does not cut over**.
106
+
107
+ ### The cutover, after a clean `migrate`
108
+
109
+ New resolver now serves byte-identical metadata, but traffic still hits the old one. Flip it one way, by how the URL was baked on-chain:
110
+ - **Custom domain** (recommended — you baked `meta.you.xyz`): re-point DNS. On-chain base unchanged → **no tx, no gas**, reversible. This is *why* baking a domain you control matters.
111
+ - **Provider endpoint** (you baked `your-app.fly.dev`): no DNS to flip → re-point the base URI — `abx set-token-uri <addr> --uri <new-base>` (+ `set-contract-uri`). A wallet-signed owner tx; takes a marketplace refresh to propagate.
112
+ - **Keep the old resolver running** until DNS/base-URI propagates. Prefer the domain path for anything real.
113
+
114
+ ### Source-only ("node-custody") images — `migrate` handles these automatically
115
+
116
+ An image committed by hash but never pinned to a durable backend serves from the source's own `/image` route — no portable URL, so copying it points the new host back at the dying old one. `migrate` instead fetches the bytes, re-hashes them against the on-chain commitment (trustless), re-pins them to **your** durable backend, and bridges the new locator. The one case it can't fix: a migrating machine with no durable backend (only `fs`/`cloud`) — it bridges nothing source-bound and tells you to re-run with `--backend ipfs|arweave`. So configure a durable backend before migrating a project that used local-disk custody.