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

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.
package/skill/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: abx-self-host
3
- description: Launch and operate a self-hosted ABX NFT end to end with the ABX CLI (`abx`) on testnet — a 1/1 (`abx deploy`), a multi-token Series from a folder of media (`abx deploy-series`), or a generative/code drop (`abx deploy-code`). Covers on-chain vs off-chain metadata, storage custody (local disk, S3/R2, IPFS, Arweave), deploy + mint (now or pre-warmed at a predicted address), rendered thumbnails and on-chain traits for code projects, collector-configurable on-chain parameters (PostParams — typed, auth-gated, settable by creator/token-owner/address), primary sales via the shared fixed-price minter, owner ops (transfer, refresh, re-point URIs, royalties, pause/unpause, supply cap, delegate minting, and the one-way locks: fields, URI config, script, dependencies, param hooks), and optionally listing a deployed collection in the ABX App Store (`abx submit-app`, never folded into deploy). Use when the user wants to self-host an ABX project, take an image to an NFT on testnet, deploy a collection from a folder of images, launch generative/code projects (art, collectibles, game assets, or anything else), add collector-settable parameters/traits, mint or run a primary sale, refresh a listing, lock down what a project stores or freeze its param hooks, operate a project they launched, list an ABX app in the App Store, choose a storage backend, stand up hosting they own, or point a project at a hosted/managed metadata provider with an API key.
3
+ description: Launch and operate a self-hosted ABX NFT end to end with the ABX CLI (`abx`) on testnet — a 1/1 (`abx deploy`), a multi-token Series from a folder of media (`abx deploy-series`), or a generative/code drop (`abx deploy-code`). Covers on-chain vs off-chain metadata, storage custody (local disk, S3/R2, IPFS, Arweave), deploy + mint (now or pre-warmed at a predicted address), rendered thumbnails and on-chain traits for code projects, collector-configurable on-chain parameters (PostParams — typed, auth-gated, settable by creator/token-owner/address), primary sales via the shared fixed-price minter, custom mechanics without a custom token (your own minter contract for auctions/allowlists/raffles/ERC-20 pricing, plus the three param hooks that run arbitrary Solidity at write time, transfer time and read time — soulbinding, vesting, escrow, burn-to-redeem, monotonic ratchets, validated collector input, oracle-fed and derived metadata), fully on-chain content at any size (no refusal — the CLI measures your RPC and reports who can read it), owner ops (transfer, refresh, re-point URIs, royalties, pause/unpause, supply cap, delegate minting, and the one-way locks: fields, URI config, script, dependencies, param hooks), and optionally listing a deployed collection in the ABX App Store (`abx submit-app`, never folded into deploy). Use when the user wants to self-host an ABX project, take an image to an NFT on testnet, deploy a collection from a folder of images, launch generative/code projects (art, collectibles, game assets, or anything else), add collector-settable parameters/traits, mint or run a primary sale, refresh a listing, lock down what a project stores or freeze its param hooks, operate a project they launched, build a custom mechanic (transfer restrictions, soulbound tokens, auctions, allowlists, escrow, redemption, tokens that react to on-chain state) without writing a token contract, put large images fully on-chain, or ASK WHETHER ABX CAN DO SOMETHING AT ALL, list an ABX app in the App Store, choose a storage backend, stand up hosting they own, or point a project at a hosted/managed metadata provider with an API key.
4
4
  compatibility: Drives the abx CLI (@artblocks/abx-cli). Co-versioned with it — install/refresh with `abx skill install` so this skill matches the CLI's `abx version`. Requires Node 22.13+.
5
5
  metadata:
6
- version: "0.1.0-alpha.31"
6
+ version: "0.1.0-alpha.32"
7
7
  ---
8
8
 
9
9
  # ABX Self-Host Toolkit (`abx`)
@@ -65,23 +65,47 @@ L3 agentic surface: image → live self-hosted NFT the creator owns — a **1/1*
65
65
  10%) is reduce-only, but any gap above the live rate is royalty the owner can add later without
66
66
  asking anyone — and no listing page shows that gap. If the creator means "5%, forever", say so out
67
67
  loud and offer `abx set-royalty-cap <addr> --cap 500`. `abx state` prints rate and ceiling together.
68
- - **What this toolkit does NOT do** check here before inventing a way:
69
- no secondary listings · no mainnet · fixed-price sales only (auctions and
70
- allowlists would be different *minters*, not flags) · no post-deploy script replace · and `abx` never
71
- compiles or deploys a token contract (`scaffold-renderer` writes a renderer, not a collection). If the
72
- ask needs one of these, say so plainly instead of building around it.
73
- - **A custom mechanic almost never needs a custom token reach for the extension points FIRST.** A
74
- collection can point at **your own minter** (`abx set-minter`), and a code project can arm **param
75
- hooks** (`abx set-param-hooks`); one contract can be both, and the token stays a factory clone the
76
- buyer can verify. Escrow/vault, soulbind-once, a monotonic ratchet, equip-and-validate, burn-to-combine
77
- every mechanic built on this toolkit so far is *a controller plus one or two hooks*, and agents
78
- still reach for "write a token contract" first. Named patterns, working Solidity, and the
79
- `abx predict` recipe that breaks the controller/schema chicken-and-egg:
80
- https://abx.docs.artblocks.io/using-abx/guides/custom-mechanics/ read it before designing one.
81
- Two caveats: neither point is scaffolded (real engineering), and a **`--transfer` hook is a VETO, not
82
- a trigger** it can refuse a transfer but never *cause* one, and a reverting hook stops transfers
83
- **and mints** for every token. It *does* see burns (`to == 0x0`) on a `--burnable` collection, which is
84
- how burn-to-combine and redemption settle.
68
+ - **⚠ NEVER say "ABX can't do that" from the absence of a flag — check
69
+ [capabilities.md](reference/capabilities.md) first.** This is the most expensive mistake made on this
70
+ toolkit, and it is made every week: a 77 KB raster called "not practical" (it ships, with one warning),
71
+ a transfer-and-mutability mechanic declared impossible (it is two hooks), a bespoke non-canonical token
72
+ written for something stock hooks cover. Falling off the end of a routing table here is **not** a "no" —
73
+ the tables are recommended routes, not the edge of the protocol. So before the words *not possible*,
74
+ *not practical*, *you'd need a different* or the name of another protocol leave your mouth: check the
75
+ four seams, then check the genuine no-list. **If it is on neither, it is possible: answer with the COST,
76
+ never a refusal** ("~31M gas to read, so it needs a high-gas RPC" — not "that's impractical"). And
77
+ **never route a creator to another protocol for something ABX does.**
78
+ - **What this toolkit genuinely does NOT do** the whole list; anything else has a seam:
79
+ no mainnet (testnet only) · no secondary listings · no post-deploy script replace · `abx` never compiles
80
+ or deploys Solidity (`scaffold-renderer` writes a project, you build it) · and **no retrofitting a
81
+ deploy-time choice** contract type (hooks need `deploy-code`), `--burnable`, `--copies`, ERC-721C
82
+ enrolment are permanent from the deploy. Ask about those four **before** you deploy; that is the one way
83
+ a creator ends up truly unable to have what they wanted. **Auctions, allowlists, raffles, ERC-20 pricing,
84
+ soulbinding, vesting, escrow, redemption, oracle-fed metadata and a large raster on-chain are NOT on this
85
+ list** — they are seams, below.
86
+ - **The four seams — ABX runs YOUR arbitrary Solidity at all three moments in a token's life.** This is
87
+ the protocol's real power and the thing agents most consistently miss. A token can point at **your own
88
+ minter** (`abx set-minter` — it may do anything before it calls `mint`), and a code project can arm
89
+ **three param hooks** (`abx set-param-hooks`, all optional, one contract can be several):
90
+ **`--configure`** runs *before a param write persists* and its **revert vetoes the write** (the stored
91
+ value is still readable, so "may only go up" / "set once" are expressible); **`--transfer`** runs *after
92
+ every ownership change — mint (`from == 0x0`) and burn (`to == 0x0`) included* and its **revert fails
93
+ the transfer**; **`--augment`** runs *at read time* as a `view` when `tokenData` is assembled, **adding
94
+ and overriding keys** with anything a `view` can reach — block data, an oracle, another contract — for
95
+ **no gas on the token and no ~24 KB blob ceiling** (it is bounded only by `eth_call`). Write-time,
96
+ transfer-time, read-time. Escrow/vault, soulbind, a monotonic ratchet, equip-and-validate,
97
+ burn-to-combine, timed reveal, live-reactive art are each *a controller plus one or two hooks*, and the
98
+ token stays a factory clone the buyer verifies. **Mechanic → seam lookup, the hooks in depth, and the
99
+ `abx predict` recipe that breaks the controller/schema chicken-and-egg:**
100
+ [capabilities.md](reference/capabilities.md). Two caveats: a **transfer hook is a VETO, not a trigger**
101
+ (it can refuse a move, never cause one — and a reverting hook stops **mints** too), and none of this is
102
+ scaffolded, so it is real Foundry engineering.
103
+ - **⚠ Hooks exist ONLY on `deploy-code` projects (`SeriesCode`/`EditionCode`), and the contract type is
104
+ fixed at deploy forever.** A `deploy` 1/1 or a `deploy-series` image collection has no params, so no
105
+ hooks, and `set-param-hooks` refuses it. **If the creator has hinted at any rule at all** — "holders can
106
+ rename it", "it can't be resold for a month", "it levels up", "it reacts to the chain" — **deploy it
107
+ with `deploy-code` even if the art is a plain image** (a script that draws the image is fine; the
108
+ Solidity lane is `--image-renderer`). You can ask before the deploy. You cannot ask after.
85
109
  - **Two gates decide everything: (1) demo or real? (2) who signs?** Settle both first — *once there's something to launch*.
86
110
  - **Confirm the full config before any on-chain write** ([readout](#confirm-before-sending)); wait for go-ahead. Never invent a field silently (name/symbol from filename, an auto description) — show it, flag it `inferred`.
87
111
  - **Never hand-build a service URL — ask the chain.** `abx tokenuri <addr> --fetch` and
@@ -114,8 +138,9 @@ Route by the **content** first, then apply the gates below. The three paths diff
114
138
  > baked into the on-chain JSON — no server, permanence is the backend's. A **raster on `fs`** has no
115
139
  > public URL to bake, so the renderer holds only a hash and `tokenURI` serves a **placeholder**; the
116
140
  > CLI warns before the spend. For the bytes themselves on-chain regardless of format, use
117
- > `--onchain-image --compress fastlz` (**keep it under ~40 KB/token that's a READ-gas ceiling, not a
118
- > cost one; see [Quick start](#quick-start)**). Say which one you're giving them: "no server" and
141
+ > `--onchain-image --compress fastlz` (**any format, any sizenothing is refused. ≲117 KB reads on every
142
+ > endpoint; above that the CLI measures your RPC and names who else can. A READ-reach question, not a
143
+ > write limit; see [Quick start](#quick-start)**). Say which one you're giving them: "no server" and
119
144
  > "on-chain" are not the same promise.
120
145
 
121
146
  **The dividing line is static content vs a running program.** Static content is self-resolving (the file *is* the thumbnail, nothing to keep running); **a code project's thumbnail is *rendered* off-chain, so it ALWAYS needs a public home you provide (`--image-base` bucket, or a resolver) — settle that infra fork with the creator FIRST** (details in [Code projects](#code-projects-generative--code-based-drops)). Nail the project type before the gates.
@@ -214,10 +239,11 @@ command has finished.
214
239
  |---|---|---|
215
240
  | **Small** (≲ 40 KB/file) | fully **on-chain** (`--onchain-image --compress fastlz`) | no host, renders forever, cheaper *at this size* |
216
241
  | **Bigger / photographic** (most PNG/JPEG) | image **off-chain**, JSON on-chain (`--onchain-uri --backend arweave`) | on-chain is ~200 gas/byte to write, and the READ is what bites (below); Arweave is pay-once permanent |
242
+ | **Bigger, but they want it on-chain anyway** | still **on-chain** (`--onchain-image`) — this row is a *default*, not a limit | **no size is refused**; ≲117 KB renders anywhere, above that the CLI measures your RPC and says who else can read it — quote the gas and let them choose |
217
243
 
218
244
  On-chain's edge past small is self-resolution/permanence, **never cost** — don't call it "cheaper" above the threshold.
219
245
 
220
- **The real ceiling is READ gas, not storage cost and 40 KB is the number, not 256 KB.** `tokenURI` reassembles the whole document per call at **~360–405k gas per KB** (quote the range, never one flat rate). So **per token**: **≲40 KB reads fine** (~15M gas) · **40–100 KB warns** (needs a high-gas RPC) · **>100 KB is refused** a margin below geth's 50M `eth_call` default, because hosted providers cap lower and marketplaces would see a **revert**, not the work. `--compress fastlz` makes it cheaper to *write*, not to *read*. The override `--allow-unreadable-onchain` is a deliberate creator choice ("almost nothing will display your work") get an explicit yes, never use it to clear a warning; the usual answer is `--backend arweave` instead. Being per token, a 300-piece collection of 5 KB SVGs is fine. Measured table → [decisions.md → The READ ceiling](reference/decisions.md#the-read-ceiling--the-number-that-actually-decides-can-this-go-on-chain).
246
+ **Nothing is refused at any size — the question is who can READ it.** The **write** is chunked into 22,000-byte SSTORE2 transactions, so no block limit binds it at any size (~200 gas/byte, money only). The **read** is one `eth_call` at **~360–405k gas per KB** (quote the range, never a flat rate), and whether it succeeds belongs to whoever's endpoint is asking. **≲117 KB (~50M gas) reads on every endpoint measured** — that is the floor: geth's default and what publicnode/drpc serve. Above it the CLI **probes your actual RPC** and reports its real cap. Measured 2026-08-24: `sepolia.base.org`, the default endpoint of our default chain, allows **600M (~729 KB)**. **A 77 KB PNG renders everywhere: ~16M gas to write, ~31M to read.** A 312 KB one ships too — it warns that a 50M-capped provider will show a revert. **We can measure YOUR RPC; we cannot know a marketplace's or an indexer's** and theirs decide whether the token displays, so never present a measured cap as settling that (it is point-in-time too: pooled endpoints rotate). **Reach is fixable after the fact, and the bytes are permanent either way:** a resolver (`abx deploy-resolver`, or a hosted one) reads on-chain content with **its own** RPC and serves plain HTTP, so a marketplace fetches a URL and never makes the big call `abx set-renderer <addr> --off` repoints `tokenURI` at it with every byte left where it is. **Storage and serving are separate choices:** on-chain is the strongest storage answer at any size; self-resolving vs a resolver is a serving decision you can change later. `--backend arweave` is the other side of the *storage* trade. **Never quote a BLOCK gas limit as the read ceiling** — three limits share that name and differ by orders of magnitude on one endpoint (measured Base Sepolia: `eth_call` 600M · `eth_estimateGas` 16.7M · block 1,200M); the block limit bounds a *contract* reading inside a tx (~1,100 KB there), not an off-chain read. `--compress fastlz` is cheaper to *write*, not to *read*. Per token, so a 300-piece collection of 5 KB SVGs is fine. Measured table → [decisions.md → The READ cost](reference/decisions.md#the-read-cost--who-can-see-it-and-why-nothing-is-refused).
221
247
 
222
248
  **When the user names off-chain custody for tiny content ("deploy it as an IPFS NFT"), lead with the on-chain recommendation in your *first* reply** — don't bury it, and don't collect resolver-URL details for a path you're about to advise against. "For a 2.4 KB SVG I'd go fully on-chain — no server, renders forever, cheaper. Want that, or IPFS?" Then let them choose (you surface the better default; you don't override the request). Defaulting to "IPFS" for tiny content is what lands it at a broken localhost URI.
223
249
 
@@ -482,7 +508,8 @@ Load one when the decision tree above sends you there — each is the depth behi
482
508
 
483
509
  | File | Holds |
484
510
  |---|---|
485
- | [decisions.md](reference/decisions.md) | the six launch decisions in depth · the READ ceiling (measured) · `--copies` custody · **locks: what freezes and what doesn't** |
511
+ | [capabilities.md](reference/capabilities.md) | **"can ABX do X?" — the four seams (minter + the three hooks), mechanic→seam lookup, and the genuine no-list.** Read it before you ever tell a creator something is impossible |
512
+ | [decisions.md](reference/decisions.md) | the six launch decisions in depth · the READ cost + measured RPC caps · `--copies` custody · **locks: what freezes and what doesn't** |
486
513
  | [code-projects.md](reference/code-projects.md) | the `abx.js` runtime contract · studio loop · PostParam catalog · seeds · in-chain Solidity lane · render ops · what to keep running |
487
514
  | [operating.md](reference/operating.md) | owner ops · credit + license · **attaching files (data plane)** · selling via the shared minter · mint page · App Store · deploy mechanics · migrating hosting |
488
515
  | [hosting.md](reference/hosting.md) | storage backends + Turbo playbook · gateways and their floors · managed providers + named remotes · `deploy-resolver`/`deploy-effects` |
@@ -0,0 +1,301 @@
1
+ # Can ABX do that? — the four seams, and the honest no-list
2
+
3
+ **Read this before you tell a creator something is impossible.** The most expensive failure on this
4
+ toolkit is not a bad deploy — it is an agent saying "ABX can't do that" about something ABX does. Three
5
+ real sessions in one week:
6
+
7
+ - a creator asked to put a **77 KB PNG fully on-chain**; the agent called it "not practical" and sent
8
+ them to a different protocol. It ships today, warns once, and costs ~31M gas to read.
9
+ - a creator wanted **transfer restrictions plus mutable state**; the agent said no. The creator said
10
+ "isn't that hooks?", the agent agreed and built it that afternoon.
11
+ - an agent wrote a **bespoke, non-canonical token contract** for a mechanic two stock hooks cover —
12
+ permanently forfeiting the factory provenance that is the whole point of deploying here.
13
+
14
+ None of those agents lacked information. Each answered from the **command list**, and the command list
15
+ is a set of *recommended routes*, not the edge of what the protocol does. Falling off the end of a
16
+ routing table is not a "no".
17
+
18
+ ## The rule
19
+
20
+ **Never answer "ABX can't do that" from the absence of a flag.** Before the words *not possible*, *not
21
+ practical*, *you'd need a different* — or the name of another protocol — leave your mouth:
22
+
23
+ 1. **Check the seam table below.** Can a minter or one of the three hooks carry it? Almost always yes.
24
+ 2. **Check [the genuine no-list](#the-genuine-no-list) at the bottom.** Is it actually on it? It is a
25
+ short list, and it is the whole list.
26
+ 3. **If it is on neither, it is possible.** Answer with the *cost*, not a refusal.
27
+
28
+ **The answer shape is always "yes, and here is what it costs" — never a flat no.** A creator who hears
29
+ *"that's not practical"* hears *"ABX can't"* and leaves. A creator who hears *"that's about 31M gas to
30
+ read, so it needs a high-gas RPC and most marketplaces will show nothing — worth it if permanence is the
31
+ point, and `--backend arweave` is the alternative"* can actually decide. The second one is also true.
32
+
33
+ **Never route a creator to another protocol for something ABX does.** If ABX genuinely cannot do it,
34
+ say so plainly and stop — recommending a competitor's stack is not your call to make on their behalf,
35
+ and it has so far always been wrong on the facts.
36
+
37
+ ## The four seams
38
+
39
+ A canonical ABX token is a stock factory clone that never changes. Your mechanic lives *beside* it, in
40
+ contracts the collector can also read. That separation is the product: the buyer verifies the token
41
+ against `isAbxClone`, and verifies your rules independently.
42
+
43
+ | Seam | Wired with | Runs | Can it veto? |
44
+ |---|---|---|---|
45
+ | **Minter** | `abx set-minter <token> --minter 0x…` (Series or edition — a plain 1/1 has no minter) | when your contract calls `mint(to)` — it may do anything first | n/a — it *is* the mint |
46
+ | **Configure hook** | `abx set-param-hooks <token> --configure 0x…` | before a governed param write persists | **yes** — its revert rejects the write |
47
+ | **Transfer hook** | `abx set-param-hooks <token> --transfer 0x…` | after every ownership change, **mint and burn included** | **yes** — its revert fails the transfer |
48
+ | **Augment hook** | `abx set-param-hooks <token> --augment 0x…` | at read time, when `tokenData` is assembled | no — it **adds and overrides** keys |
49
+
50
+ One contract can be several of these at once. That is the shape nearly every real mechanic takes: a
51
+ controller holding the money and the rules, wired into a clone that stays byte-identical to every other
52
+ ABX project of its type.
53
+
54
+ **Between them, these seams run arbitrary Solidity at all three moments in a token's life: when someone
55
+ writes to it, when it moves, and when it is read.** That is the honest scope of the claim. If a mechanic
56
+ can be expressed as *"reject this write"*, *"reject this move"*, *"account for what just happened"*, or
57
+ *"compute this at read time"*, it is buildable here without a custom token.
58
+
59
+ > **⚠ The precondition, and it is deploy-time-irreversible: hooks exist only on `SeriesCode` /
60
+ > `EditionCode` — the `deploy-code` lane.** A `deploy` 1/1 or a `deploy-series` image collection has no
61
+ > configurable params, therefore no hooks, and `set-param-hooks` refuses it. **The contract type is fixed
62
+ > at deploy and cannot be retrofitted**, so a static-art collection deployed with `deploy-series` has
63
+ > permanently foreclosed every mechanic on this page. **If a creator has hinted at *any* rule — "holders
64
+ > can rename it", "it can't be resold for a month", "it levels up", "it reacts to the chain" — deploy it
65
+ > with `deploy-code` even when the art is a plain image** (a script that draws the image is fine, and
66
+ > `--image-renderer` covers the Solidity lane). Ask before you deploy; you cannot ask after.
67
+ >
68
+ > **The minter seam is the exception** — `abx set-minter` works on any Series or edition, so an
69
+ > auction, an allowlist or a raffle needs no code project. Only the three hooks do.
70
+
71
+ There is a **fifth** transfer-time seam that is not a hook and should never be conflated with one:
72
+ the **ERC-721C / ERC-1155C transfer validator**, an opt-in-at-deploy standards-track mechanism for
73
+ royalty enforcement, managed with `abx set-transfer-validator`. Different purpose, different lock,
74
+ different audience → [creator-token.md](creator-token.md).
75
+
76
+ ## The three hooks, in depth
77
+
78
+ Full calling conventions live in
79
+ [`IAbxParamHooks.sol`](https://github.com/ArtBlocks/abx/blob/main/contracts/src/extensions/configurable-params/IAbxParamHooks.sol);
80
+ working Solidity for each pattern is at
81
+ https://abx.docs.artblocks.io/using-abx/guides/custom-mechanics/. Wire all three with one
82
+ `abx set-param-hooks` call (the contract has no per-hook setter — omit a role to keep it, `none` to
83
+ clear it).
84
+
85
+ ### Configure hook — the write-time validator
86
+
87
+ ```solidity
88
+ function onParamConfigured(
89
+ uint256 tokenId, bytes32 key, bytes32 value,
90
+ address updatedBy, uint256 dataLength, address dataBlobAddress
91
+ ) external;
92
+ ```
93
+
94
+ Called **before the value persists**, inside a schema-governed `configure-param`. **Reverting vetoes the
95
+ write.**
96
+
97
+ - **`tokenParam` still returns the OLD value during this call** — which is what makes "may only go up",
98
+ "may only be set once", and "must differ from the last" expressible at all.
99
+ - **Two shapes, one function.** A scalar write puts the whole value in `value` and zeroes the last two
100
+ args. A `String`/`Bytes` write cannot — `value` carries only `keccak256(data)`, and the content is at
101
+ `dataBlobAddress` (a Solady SSTORE2 pointer, already deployed). `dataLength` is the reliable
102
+ discriminator: **never `0` on the blob path**.
103
+ - Read the incoming blob with `SSTORE2.read(dataBlobAddress)` — never `CALL` the pointer. A hook that
104
+ only enforces a size ceiling reads `dataLength` and never pays to touch the bytes.
105
+ - **A veto costs the writer the storage they already paid for** (the blob is written before the hook
106
+ runs, by necessity). An `eth_call` dry run surfaces the rejection for free — that is what a good
107
+ configuration UI does before asking for a signature.
108
+ - **You cannot write zero bytes.** A zero-length `String`/`Bytes` write is refused by the contract
109
+ before your hook ever runs, so "unequip everything" cannot be an empty payload — use a sentinel byte
110
+ or a scalar companion key.
111
+
112
+ ### Transfer hook — the ownership-change veto
113
+
114
+ ```solidity
115
+ function onTokenTransfer(
116
+ uint256 tokenId, address from, address to, address operator, uint256 amount
117
+ ) external;
118
+ ```
119
+
120
+ Called after every ownership change. **`from == address(0)` is a mint; `to == address(0)` is a burn.**
121
+ **Reverting fails the transfer** — the token no longer swallows the revert, because it could not honestly
122
+ promise otherwise (Solady runs the receiver-acceptance check *after* this call, so a hook could starve
123
+ `safeTransferFrom` regardless).
124
+
125
+ - **It is a veto, not a trigger.** It can refuse a move; it can never *cause* one.
126
+ - **It vetoes mints too** — a mint is a transfer from `0x0`, so a hook that reverts unconditionally
127
+ freezes issuance as well as trading.
128
+ - **It sees burns**, which is how burn-to-combine, redemption and vaults settle — but only on a
129
+ collection deployed `--burnable`, which is **deploy-time-only and cannot be retrofitted**.
130
+ - It may persist and derive state through its own authority; the token exposes no special path.
131
+ - `operator` and `amount` are not redundant with `from`: on an ERC-1155 an approved marketplace moves a
132
+ holder's copies. **On a multi-copy edition, think hard before storing per-id state** — params are per
133
+ *id* and therefore shared by every holder, so "the current owner" really means "whoever moved most
134
+ recently". Aggregate or monotonic state (transfer counts, "has ever been held by") is coherent there;
135
+ a single-owner notion is not.
136
+ - **A buyer's guarantee that you will never arm one is `abx lock-param-hooks`, not your word.** Freezing
137
+ an empty set is the proof that no transfer veto can ever exist.
138
+
139
+ ### Augment hook — read-time compute, and the most under-used seam we have
140
+
141
+ ```solidity
142
+ function augmentTokenParams(address token, uint256 tokenId)
143
+ external view returns (AugmentedParam[] memory); // { bytes32 key; string value; }
144
+ ```
145
+
146
+ A `view`, called when `tokenData` is assembled — by the on-chain `AbxGenerator` and by the off-chain
147
+ resolver. **No storage, no event, no gas on the token.** It may **add** keys and **override** stored ones
148
+ (augment wins per key, last-wins in every mainstream JSON parser).
149
+
150
+ - **Its return is bounded only by `eth_call` limits — the ~24 KB stored-blob ceiling does not apply to
151
+ compute.** This is the lane for large *derived* data.
152
+ - It reads whatever a `view` can reach: block data, an oracle, another contract, the token's own params,
153
+ a holder's balance elsewhere. A piece that genuinely reacts to live chain state is one hook.
154
+ - Values are the **final canonical string** exactly as `tokenData` carries it — a hook injecting binary
155
+ base64s it itself (free, view context). Raw, not JSON-escaped; escaping is the serializer's job.
156
+ - **The live view reads it per view (always current); the rendered still is a snapshot of settled state
157
+ only.** So a volatile hook (a timestamp, a price) animates the live view without re-rendering the
158
+ thumbnail every block. Re-render triggers are settled-state changes.
159
+ - Reserved `tokenData` keys (`seed`, `tokenId`, …) are rejected — a hook cannot forge them.
160
+ - **It does not reach a renderer you wrote.** `render(token, tokenId, field)` takes no `tokenData`.
161
+ Whoever builds `tokenData` calls the hook, and in your own renderer that is you: read the live state
162
+ directly, or read `paramHooks()` and call `augmentTokenParams` yourself, wrapped so a broken hook
163
+ cannot make `render()` revert. Do **not** tell a creator "augment hooks are a resolver concept" —
164
+ the generator calls one on-chain.
165
+ - Resolver operators have a kill-switch (`ABX_DISABLE_AUGMENT=1`, degrading to settled params);
166
+ `abx verify` prints the project's live-data posture.
167
+
168
+ ## Mechanic → seam (the inverse lookup)
169
+
170
+ Every one of these has been built here, or is one small contract away. When a creator describes a
171
+ mechanic, find the row before you find an objection.
172
+
173
+ | The creator wants | Seam | Shape |
174
+ |---|---|---|
175
+ | Can't be resold for N days · vesting · lockup | transfer hook | `require(block.timestamp >= unlockAt)` |
176
+ | Soulbound — mint once, never moves | transfer hook | `require(from == address(0))` |
177
+ | Burn to redeem · burn to combine · breeding | `--burnable` + transfer hook | settle on `to == address(0)` |
178
+ | Escrow / vault — ETH locked behind the token | minter **+** transfer hook | take payment on mint, pay out on burn |
179
+ | A value that may only go up (level, growth, high-water) | configure hook | compare against the still-old `tokenParam` |
180
+ | Structured collector input that must be well-formed (equip, layout, inventory) | configure hook | `SSTORE2.read(dataBlobAddress)`, decode, validate |
181
+ | Only holders of *some other* token may configure this | configure hook, or `Address(…)` auth in the schema | check the other contract in the hook |
182
+ | Auction · Dutch auction · allowlist · raffle · free claim | **custom minter** | your contract, `abx set-minter` |
183
+ | Priced in an ERC-20, or in anything but ETH | **custom minter** | take payment however you like, then `mint(to)` |
184
+ | Art that reacts to live chain state (block, price, an oracle) | augment hook | read it in a `view`, return it as `tokenData` |
185
+ | Timed reveal | augment hook | branch on `block.timestamp` |
186
+ | Derived traits too large to store on-chain | augment hook | compute at read time — no blob ceiling |
187
+ | Provenance / transfer count / "has ever been held by" | transfer hook | the hook writes its own storage |
188
+ | A raster of any size fully on-chain | `--onchain-image` | nothing is refused — [below](#on-chain-size-nothing-is-refused-and-the-question-is-who-can-read-it) |
189
+ | Royalty enforcement at the marketplace layer | ERC-721C validator (opt in **at deploy**) | [creator-token.md](creator-token.md) |
190
+
191
+ **The controller chicken-and-egg has a solution — use it.** The controller needs the token's address at
192
+ construction and the token's schema needs the controller's address. Do not run the five-step dance:
193
+ `abx predict` computes the deterministic address first, so the controller is built against a constant and
194
+ the schema rides the deploy. Recipe, including the salt guard-prefix trap (leading 20 bytes must equal
195
+ your deploying wallet, or the address is permissionless and anyone may take it):
196
+ https://abx.docs.artblocks.io/using-abx/guides/custom-mechanics/#break-the-controller-cycle-with-abx-predict
197
+
198
+ **`abx scaffold-renderer` is the fastest start even when you are building a hook, not a renderer** — it
199
+ writes a Foundry project with the interfaces vendored and tests wired. And test against a *real* deployed
200
+ clone (`forge test --fork-url`), never a hand-rolled mock: a mock invents its way around the real edge
201
+ cases, starting with the zero-length refusal above.
202
+
203
+ ## On-chain size: nothing is refused, and the question is who can READ it
204
+
205
+ The routing tables elsewhere in this skill recommend **≲40 KB/token** for on-chain content. That is
206
+ **advice about what is cheap, not a limit** — and reading it as a limit is what produced the 77 KB
207
+ refusal. There is no size the toolkit refuses.
208
+
209
+ **Two axes, and only one is about size at all.** The **write** is chunked into 22,000-byte SSTORE2
210
+ transactions, so no block gas limit binds it at any size — purely money, ~200 gas/byte, paid once. The
211
+ **read** is a single `eth_call`, and whether it succeeds is a property of *whose endpoint is asking*.
212
+
213
+ Measured 2026-08-24, which is why the old 100 KB refusal was wrong:
214
+
215
+ | Endpoint | `eth_call` cap | Content it serves |
216
+ |---|---|---|
217
+ | **`sepolia.base.org`** — the default endpoint of our default chain | **600M** | **~729 KB** |
218
+ | `base-sepolia.publicnode.com` · `base-sepolia.drpc.org` | 50M | ~117 KB |
219
+ | `ethereum-sepolia.publicnode.com` | 50M | ~117 KB |
220
+
221
+ 50M is the **floor**, not the ceiling. The refusal sat at 100 KB — below every endpoint above — so it
222
+ blocked content all of them could read. The CLI now measures the RPC you are actually on (a
223
+ state-override call that sends nothing) and reports it:
224
+
225
+ | Content per token | What the CLI does |
226
+ |---|---|
227
+ | ≲ **117 KB** (~50M gas) | a plain note with write and read figures — renders anywhere |
228
+ | **above that** | ⚠ names your RPC's measured cap and warns that a 50M-capped provider shows a **revert** |
229
+ | **past your own RPC's cap** | ⚠ says plainly that nothing you can point at renders it |
230
+
231
+ For that **77 KB PNG**: **~16M gas to write** (four chunks) and **~31M to read** — under the floor, so
232
+ it renders everywhere. Say the numbers; it is not a warn case at all.
233
+
234
+ **Two things have to survive into the conversation.**
235
+
236
+ **(1) We can measure YOUR RPC. We cannot know a marketplace's or an indexer's** — and theirs decide
237
+ whether the token displays. A measured cap proves *you* can read it, never that OpenSea can, and it is
238
+ point-in-time besides (pooled endpoints rotate: Sepolia's publicnode measured 50M one hour and 2,000M
239
+ the next). Say the measurement as a measurement.
240
+
241
+ **(2) Reach is fixable after the fact; the bytes are permanent either way.** The read cost decides
242
+ whether a token resolves *in one `eth_call`*, not whether the content is reachable. A resolver
243
+ (`abx deploy-resolver`, or a hosted one) reads on-chain content with **its own** RPC — `inline`,
244
+ `inline-gzip`, `reader`, `reader-gzip` all resolve through it — and serves plain HTTP, so a marketplace
245
+ fetches a URL and never makes the big call. `abx set-renderer <addr> --off` repoints `tokenURI` at it
246
+ with every byte left exactly where it is.
247
+
248
+ So keep **storage** and **serving** separate, because they are: on-chain is the strongest storage
249
+ answer available at *any* size, and self-resolving-vs-resolver is a serving decision the creator can
250
+ change later. A token whose read is too big for some endpoint is not a lost token — it is one that
251
+ wants a reader in front of it. `--backend arweave` is the other side of the *storage* trade, not a
252
+ correction of their ask.
253
+
254
+ **Never quote a BLOCK gas limit as the read ceiling.** Three limits share the name and differ by orders
255
+ of magnitude on one endpoint (measured 2026-08-24, Base Sepolia): **`eth_call` 600M** bounds the read
256
+ that decides display · **`eth_estimateGas` 16,777,216** bounds what can be *sent* (identical on every
257
+ endpoint tried — it is why writes batch at 8M, not the block limit) · **block `gasLimit` 1,200M**
258
+ bounds a contract reading inside a transaction, so a contract there reads ~1,100 KB. Off-chain reads
259
+ are not bounded by block limits, and are routinely far above them.
260
+
261
+ Two things not to garble: `--compress fastlz` cuts the **write** cost, never the read (the renderer
262
+ rebuilds the full uncompressed bytes every call). And the gate is **per token** — a 300-piece collection
263
+ of 5 KB works reads perfectly well.
264
+
265
+ Measured gas table and the per-KB curve →
266
+ [decisions.md → The READ cost](decisions.md#the-read-cost--who-can-see-it-and-why-nothing-is-refused).
267
+
268
+ ## The genuine no-list
269
+
270
+ This is the whole list. If the ask is not here and not blocked by a deploy-time choice already made,
271
+ stop looking for a reason to say no.
272
+
273
+ **Actually not possible today:**
274
+
275
+ - **Mainnet.** Testnet only — Base Sepolia by default, Sepolia via `ABX_CHAIN`. Never imply otherwise.
276
+ - **Secondary listings.** Reselling a held token means an external marketplace or a manual
277
+ `abx transfer`. There is no listing feature and no order book.
278
+ - **`abx` compiling or deploying Solidity for you.** You write hooks, minters and renderers with Foundry
279
+ and deploy them yourself; `abx scaffold-renderer` writes the project, it does not build or ship it.
280
+ - **Replacing a code project's script through the CLI post-deploy.** There is no `abx set-script`.
281
+ - **Retrofitting a deploy-time choice.** These are permanent from the moment the deploy lands:
282
+ the **contract type** (hooks require `deploy-code`), **`--burnable`**, **`--copies`** (721 vs 1155),
283
+ and **ERC-721C enrollment**. Ask about all four *before* the deploy — this is the single most common
284
+ way a creator ends up genuinely unable to have what they wanted.
285
+ - **Unsupported chains.** Say plainly it is not in the toolkit; do not fabricate a recipe.
286
+
287
+ **Not a flag — but absolutely possible on a seam.** These belong in the paragraph above only if you also
288
+ say how they are built. Listing them as things "abx doesn't do" is what caused this file to exist:
289
+
290
+ - auctions, Dutch auctions, allowlists, raffles, free claims, ERC-20 pricing → **a custom minter**
291
+ - transfer restrictions, soulbinding, vesting, redemption, escrow → **the transfer hook**
292
+ - validated or governed collector input → **the configure hook**
293
+ - live, derived, oracle-fed or oversized metadata → **the augment hook**
294
+ - a large raster fully on-chain → **`--onchain-image`**, at any size
295
+
296
+ **And one thing that is possible but is almost never the right answer: writing your own token contract.**
297
+ It is a legitimate creator choice, but it permanently forfeits `isAbxClone` — the only signal
298
+ marketplaces and the App Store allowlist against — and canonicity cannot be added afterwards. Never make
299
+ that trade *for* a creator silently. Say: *"I can do that, but your collection won't be a canonical ABX
300
+ contract — marketplaces won't recognise it, and the only way back is redeploying and moving holders.
301
+ Want that, or shall we find a way inside the toolkit?"* Then wait for an answer.
@@ -234,7 +234,7 @@ Fork `contracts/src/renderers/examples/{SeedSvgRenderer,SeedTraitsRenderer}.sol`
234
234
 
235
235
  ## Traits on-chain vs off-chain — is your script's trait logic reproducible in Solidity?
236
236
 
237
- A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](../../../docs/research/onchain-traits-feasibility.md). Give a **graded** answer, never a flat "not feasible":
237
+ A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](https://github.com/ArtBlocks/abx/blob/main/docs/research/onchain-traits-feasibility.md). Give a **graded** answer, never a flat "not feasible":
238
238
 
239
239
  **First, the load-bearing distinction: *portable* ≠ *deployed*.** `inspect` saying traits "port EXACTLY" means the *logic* can be reproduced in Solidity — it does **not** mean a renderer exists. On-chain traits require the creator to **author + deploy** a Solidity attributes-renderer (fork `SeedTraitsRenderer.sol`) and pass its address to `--attributes-renderer` — a real build step, not a flag with a default. `deploy-code --dry-run` prints the traits disposition (`on-chain` / `off-chain via resolver` / `⚠ OMITTED`) and a real deploy **refuses an `--attributes-renderer` address with no code**. So never present "traits on-chain" as settled off a guessed address: confirm a renderer is deployed, or the honest options are off-chain-via-resolver or omitted.
240
240