@artblocks/abx-cli 0.1.0-alpha.8 → 0.1.0-alpha.9

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/dist/remote.js CHANGED
@@ -1,54 +1,139 @@
1
1
  /**
2
- * Talk to a REMOTE resolver's admin control plane the HTTP form of `abx add` /
3
- * `abx index` / `abx forget`, pointed at a hosted node (fly/render/vps) instead of
4
- * this machine's local projection. This is the bridge the toolkit was missing: a
5
- * contract you deploy here is registered with the *local* store; the resolver baked
6
- * into your on-chain tokenURI is a *different* store and must be told separately, or
7
- * it answers "unknown project". These helpers do the telling.
2
+ * Remote-resolver resolution which service `--remote` means, and whose credential drives it.
3
+ * The HTTP client itself lives in the SDK (`AbxServiceClient`, speaking the /v1 control plane from
4
+ * specs/self-host-toolkit/remote-services.md); this module owns the CLI conventions around it:
8
5
  *
9
- * The admin token authorizes indexing control only never on-chain signing so the
10
- * "no signing key on the host" rule is intact. It lives in `.env` as
11
- * ABX_RESOLVER_ADMIN_TOKEN and must match the token set on the resolver.
6
+ * --remote <name> a NAMED remote`ABX_REMOTE_<NAME>_URL` + `ABX_REMOTE_<NAME>_TOKEN` in .env
7
+ * (same env-name normalization as ABX_RPC_URLS_<CHAIN>). A managed provider's
8
+ * per-account API key lives here and deliberately NEVER falls back to
9
+ * ABX_RESOLVER_ADMIN_TOKEN, so your node-admin secret can't silently go to a
10
+ * third party.
11
+ * --remote <url> an ad-hoc URL — token from --remote-token or ABX_RESOLVER_ADMIN_TOKEN.
12
+ * --remote bare: the self-host default — ABX_PUBLIC_BASE_URL (the URL baked on-chain,
13
+ * else ABX_RESOLVER_URL) + ABX_RESOLVER_ADMIN_TOKEN. Unchanged from day one.
14
+ *
15
+ * Every token here authorizes indexing/metadata control only — never on-chain signing — so the
16
+ * "no signing key on the host" rule is intact. (`--remote-token`, not `--token`: `--token` already
17
+ * means a token ID across the owner ops.)
12
18
  */
13
- /** Normalize a resolver base URL to its `/admin/projects` control-plane endpoint. */
14
- export function adminProjectsUrl(base) {
15
- return base.replace(/\/+$/, '') + '/admin/projects';
16
- }
17
- function authHeaders(token) {
18
- return { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
19
- }
20
- /** Map a non-2xx admin response into a useful Error (surfaces the server's message). */
21
- async function asError(label, resp) {
22
- let detail = '';
23
- try {
24
- detail = (await resp.json()).error ?? '';
19
+ import { AbxServiceClient, AbxServiceError, envSuffix } from '@artblocks/abx-sdk';
20
+ /**
21
+ * Resolve the `--remote` flag value (undefined = local op → null). Pure: pass `env` in tests.
22
+ * `tokenFlag` is `--remote-token` and wins over any env token for this invocation.
23
+ */
24
+ export function resolveRemote(spec, tokenFlag, env = process.env) {
25
+ if (spec === undefined)
26
+ return null;
27
+ const strip = (u) => u.replace(/\/+$/, '');
28
+ // bare `--remote` — the self-host default (the resolver this project's on-chain URIs point at)
29
+ if (spec === 'true' || spec === '') {
30
+ const base = env.ABX_PUBLIC_BASE_URL ?? env.ABX_RESOLVER_URL;
31
+ if (!base) {
32
+ throw new Error('`--remote` needs a target: pass `--remote <name>` (ABX_REMOTE_<NAME>_URL in .env), `--remote https://host`, or set ABX_PUBLIC_BASE_URL in .env');
33
+ }
34
+ return { url: strip(base), token: tokenFlag ?? env.ABX_RESOLVER_ADMIN_TOKEN, source: 'default', tokenVar: 'ABX_RESOLVER_ADMIN_TOKEN' };
25
35
  }
26
- catch {
27
- /* non-JSON body */
36
+ // `--remote <url>` — ad-hoc URL, self-host credential conventions
37
+ if (spec.includes('://')) {
38
+ return { url: strip(spec), token: tokenFlag ?? env.ABX_RESOLVER_ADMIN_TOKEN, source: 'url', tokenVar: 'ABX_RESOLVER_ADMIN_TOKEN' };
28
39
  }
29
- if (resp.status === 401) {
30
- return new Error(`${label}: 401 unauthorized — ABX_RESOLVER_ADMIN_TOKEN doesn't match the token on the resolver.`);
40
+ // `--remote <name>` — a named remote (a managed provider, another node of yours, a staging box)
41
+ const name = envSuffix(spec);
42
+ const urlVar = `ABX_REMOTE_${name}_URL`;
43
+ const tokenVar = `ABX_REMOTE_${name}_TOKEN`;
44
+ const url = env[urlVar];
45
+ if (!url) {
46
+ throw new Error(`--remote ${spec}: no ${urlVar} in your .env. Set ${urlVar}=<the provider's base URL> (+ ${tokenVar}=<your API key>). ` +
47
+ `(Meant a URL? Pass a scheme: --remote https://host)`);
31
48
  }
32
- if (resp.status === 404 && detail.includes('disabled')) {
33
- return new Error(`${label}: the resolver has no admin token set — it can't accept remote add/remove. ${detail}`);
49
+ return { url: strip(url), token: tokenFlag ?? env[tokenVar], name, source: 'named', tokenVar };
50
+ }
51
+ /** The token, when the remote is the command's OBJECT (add/index/forget/render/migrate-dest) —
52
+ * a missing credential is a hard stop that names the exact var to set. */
53
+ export function requireRemoteToken(t, env = process.env) {
54
+ if (t.token)
55
+ return t.token;
56
+ if (t.source === 'named') {
57
+ // The likeliest cause of "URL but no token" is a near-miss var name, and the value is sitting
58
+ // right there in .env — so name it instead of letting them diff strings by eye.
59
+ const nearMiss = misnamedRemoteVars(env).find((v) => v.suggestion === t.tokenVar);
60
+ throw new Error(`remote '${t.name}' has a URL but no token — set ${t.tokenVar} in your .env (the API key the provider issued), or pass --remote-token.` +
61
+ (nearMiss ? `\n Found ${nearMiss.key} in your env — that name isn't read; rename it to ${t.tokenVar}.` : ''));
34
62
  }
35
- return new Error(`${label}: ${resp.status} ${detail || resp.statusText}`);
63
+ throw new Error(`remote ops need ${t.tokenVar} in your .env — it must match the token set on the resolver ` +
64
+ '(`abx deploy-resolver` generates one and wires both sides). Or pass --remote-token.');
36
65
  }
37
- /** Register + index a project with a remote resolver (remote `abx add`/`index`). */
38
- export async function remoteAddProject(base, token, body) {
39
- const resp = await fetch(adminProjectsUrl(base), {
40
- method: 'POST',
41
- headers: authHeaders(token),
42
- body: JSON.stringify(body),
43
- });
44
- if (!resp.ok)
45
- throw await asError('remote add', resp);
46
- return (await resp.json());
66
+ /** Every named remote configured in the env (`ABX_REMOTE_<NAME>_URL`). The normalized name is
67
+ * canonical the env convention is lossy, so enumeration reads the env, never inverts it. */
68
+ export function listConfiguredRemotes(env = process.env) {
69
+ const out = [];
70
+ for (const [key, value] of Object.entries(env)) {
71
+ const m = /^ABX_REMOTE_(.+)_URL$/.exec(key);
72
+ if (!m || !value)
73
+ continue;
74
+ out.push({ name: m[1], url: value.replace(/\/+$/, ''), hasToken: !!env[`ABX_REMOTE_${m[1]}_TOKEN`] });
75
+ }
76
+ return out.sort((a, b) => a.name.localeCompare(b.name));
47
77
  }
48
- /** Stop a remote resolver from indexing a project (remote `abx forget`). */
49
- export async function remoteRemoveProject(base, token, address) {
50
- const resp = await fetch(`${adminProjectsUrl(base)}/${address}`, { method: 'DELETE', headers: authHeaders(token) });
51
- if (!resp.ok && resp.status !== 404)
52
- throw await asError('remote remove', resp);
78
+ /**
79
+ * `ABX_REMOTE_*` vars that follow no recognized suffix — i.e. a typo. The convention has exactly two
80
+ * suffixes (`_URL`, `_TOKEN`), and anything else (`_KEY`, `_APIKEY`, `_SECRET`) is silently ignored:
81
+ * the CLI reports "no token" while the value sits right there in `.env`. Cheap to detect, so detect
82
+ * it rather than leaving the creator to compare strings by eye.
83
+ */
84
+ export function misnamedRemoteVars(env = process.env) {
85
+ const out = [];
86
+ for (const key of Object.keys(env)) {
87
+ if (!key.startsWith('ABX_REMOTE_') || key === 'ABX_REMOTE_TOKEN')
88
+ continue;
89
+ if (/_URL$/.test(key) || /_TOKEN$/.test(key))
90
+ continue;
91
+ // Strip a trailing credential-ish word to recover the intended remote name.
92
+ const name = key.replace(/^ABX_REMOTE_/, '').replace(/_(KEY|APIKEY|API_KEY|SECRET|PASS|PASSWORD|BEARER|AUTH)$/, '');
93
+ out.push({ key, suggestion: `ABX_REMOTE_${name}_TOKEN` });
94
+ }
95
+ return out.sort((a, b) => a.key.localeCompare(b.key));
96
+ }
97
+ /** The SDK service client bound to this target (call {@link requireRemoteToken} first when the
98
+ * operation needs auth). */
99
+ export function serviceClient(t) {
100
+ return new AbxServiceClient({ baseUrl: t.url, token: t.token });
101
+ }
102
+ /**
103
+ * Map a control-plane failure to an actionable CLI error. Keys off `AbxServiceError.code`/status,
104
+ * never off message prose (the machine-code half of the interface spec):
105
+ * 401 — the credential itself was rejected → name the var that supplied it.
106
+ * 403 — credential fine, not authorized for this resource → provider scoping, don't retry.
107
+ * `disabled` — the node has no control-plane token configured at all.
108
+ * bare 404 on /v1 — no control plane at this URL (pre-/v1 node, or not an ABX service).
109
+ */
110
+ export function describeRemoteError(err, t, label) {
111
+ if (!(err instanceof AbxServiceError))
112
+ return err;
113
+ if (err.status === 0) {
114
+ // Nothing answered. Say WHERE we asked and, for the bare form, which env var chose it — a lone
115
+ // `fetch failed` leaves the creator with no idea which URL was even tried.
116
+ const from = t.source === 'default'
117
+ ? ' (bare `--remote` uses ABX_PUBLIC_BASE_URL, else ABX_RESOLVER_URL)'
118
+ : t.source === 'named'
119
+ ? ` (from ABX_REMOTE_${t.name}_URL)`
120
+ : '';
121
+ return new Error(`${label}: nothing responded at ${t.url}${from}. Is it running, and is that the right address?`);
122
+ }
123
+ if (err.status === 401) {
124
+ return new Error(`${label}: 401 unauthorized — ${t.url} rejected the token from ${t.tokenVar}. Fix or rotate the key (or pass --remote-token).`);
125
+ }
126
+ if (err.status === 403) {
127
+ return new Error(`${label}: 403 forbidden — the token is valid but not authorized for this project/chain (provider-side scoping, not a typo). ` +
128
+ `Check the provider's dashboard and its descriptor (\`abx remote ${t.name ?? t.url}\`).`);
129
+ }
130
+ if (err.code === 'disabled') {
131
+ return new Error(`${label}: the resolver at ${t.url} has no control-plane token configured — it can't accept remote registrations. For your own node, set ABX_RESOLVER_ADMIN_TOKEN on it (\`abx deploy-resolver\` wires it).`);
132
+ }
133
+ if (err.status === 404 && !err.code) {
134
+ return new Error(`${label}: ${t.url} has no /v1 control plane — an older resolver (redeploy it: \`abx deploy-resolver\`), or not an ABX service. ` +
135
+ `Check: curl ${t.url}/.well-known/abx-service`);
136
+ }
137
+ return new Error(`${label}: ${err.message}`);
53
138
  }
54
139
  //# sourceMappingURL=remote.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6BH,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,iBAAiB,CAAC;AACtD,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,EAAC,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAC,CAAC;AAChF,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,OAAO,CAAC,KAAa,EAAE,IAAc;IAClD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAsB,CAAC,KAAK,IAAI,EAAE,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB;IACrB,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACxB,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,wFAAwF,CAAC,CAAC;IACrH,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,8EAA8E,MAAM,EAAE,CAAC,CAAC;IACnH,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,IAAI,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa,EAAE,IAAmB;IACrF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE;QAC/C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC;QAC3B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,MAAM,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IACtD,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAoB,CAAC;AAChD,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAa,EAAE,OAAe;IACpF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,OAAO,EAAE,EAAE,EAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,EAAC,CAAC,CAAC;IAClH,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,MAAM,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAClF,CAAC"}
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artblocks/abx-cli",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-alpha.9",
4
4
  "license": "MIT",
5
5
  "description": "ABX CLI ('abx') — the agentic UX surface of the Self-Host Toolkit (Layer 3). Deploy, index, serve, and demo a self-hosted ABX project end to end. Wraps the SDK; runs a different implementation and the protocol works identically.",
6
6
  "type": "module",
@@ -38,13 +38,13 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "viem": "^2.21.0",
41
- "@artblocks/abx-sdk": "0.1.0-alpha.1",
42
- "@artblocks/abx-indexer": "0.1.0-alpha.2",
43
- "@artblocks/abx-storage": "0.1.0-alpha.1",
44
- "@artblocks/abx-token-api": "0.1.0-alpha.4"
41
+ "@artblocks/abx-sdk": "0.1.0-alpha.2",
42
+ "@artblocks/abx-indexer": "0.1.0-alpha.3",
43
+ "@artblocks/abx-token-api": "0.1.0-alpha.5",
44
+ "@artblocks/abx-storage": "0.1.0-alpha.2"
45
45
  },
46
46
  "optionalDependencies": {
47
- "@artblocks/abx-effects": "0.1.0-alpha.1"
47
+ "@artblocks/abx-effects": "0.1.0-alpha.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "playwright": "1.61.1"
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 art, primary sales via the shared fixed-price minter, and owner ops (transfer, refresh, re-point URIs, royalties, lock fields, pause/unpause, supply cap, delegate minting). 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 art, mint or run a primary sale, refresh a listing, operate a project they launched, choose a storage backend, or stand up hosting they own.
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 art, primary sales via the shared fixed-price minter, and owner ops (transfer, refresh, re-point URIs, royalties, lock fields, pause/unpause, supply cap, delegate minting). 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 art, mint or run a primary sale, refresh a listing, operate a project they launched, 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.5+.
5
5
  metadata:
6
- version: "0.1.0-alpha.8"
6
+ version: "0.1.0-alpha.9"
7
7
  ---
8
8
 
9
9
  # ABX Self-Host Toolkit (`abx`)
@@ -20,7 +20,7 @@ L3 agentic surface: image → live self-hosted NFT the creator owns — a **1/1*
20
20
  - **CLI ⇄ npm.** If any `abx` command prints **`update available`**, upgrade it yourself: `npm i -g @artblocks/abx-cli@latest` (global install) or `npm i --save-dev @artblocks/abx-cli@latest` (project-local — match however it was installed), then `abx skill install`, then reload. Do **not** relay the notice and carry on: a stale CLI can hold canonical addresses that have since moved, so it deploys against dead singletons, and a mid-run `… is not a function` is usually this. You have a shell — upgrading is your job, not the creator's.
21
21
  - **Resolve the CLI before you install anything — local beats global.** Probe `npx --no-install abx version` (project-local), then `abx version` (global); only install if both miss, and default to the **project-local** `npm install --save-dev @artblocks/abx-cli`. The package is `@artblocks/abx-cli`; `@artblocks/abx-sdk` is the library and ships no binary. Full ladder + why `--no-install` matters → [Setup](#setup--environment).
22
22
  - **`abx doctor` first, always** — full preflight (Node, pnpm, RPC, signing key, storage). Fix any ✗ before deploying ([Setup](#setup--environment)). A missing public-base-url is not a "set up IPFS" signal: for tiny art go on-chain, for larger art pick an off-chain backend — see [Quick start](#quick-start).
23
- - **Never collect secrets in chat.** Keys, `PINATA_JWT`, S3 secrets → the project's `.env`. The Arweave/Turbo key is a CLI-managed file (`.abx-self-host/arweave-key.json`) — never paste it. Name the var/file; never take the value.
23
+ - **Never collect secrets in chat, and never `cat`/`grep` `.env`.** Keys, `PINATA_JWT`, S3 secrets, provider API keys → the project's `.env`. The Arweave/Turbo key is a CLI-managed file (`.abx-self-host/arweave-key.json`) — never paste it. Name the var/file; never take the value. **To see what's configured, ask the tool, not the file:** `abx doctor` and `abx remote` report each credential as set/unset without ever printing one. Reading `.env` spills every secret in it into the transcript — irreversible, and a plain `abx doctor` tells you the same thing.
24
24
  - **Testnet only today** — every launch is on a testnet: **Base Sepolia by default** (`ABX_CHAIN` unset), with **Sepolia** also shipped (`ABX_CHAIN=sepolia`). Say "testnet"; don't imply mainnet. **Testnet IS the preview + e2e environment**: it runs the *real* wiring (renderers, generator, on-chain tokenURI assembly), so a creator should deploy there, inspect the actual result (`abx tokenuri` / the live view / `abx verify`), confirm it looks right, and only *then* go to mainnet — no separate local "preview" is as faithful as the real testnet drop, and a testnet deploy is ~free + ~minutes. **One cross-chain gotcha: on-chain library deps (`--dep p5@…`) resolve to on-chain bytes only where an Art Blocks dependency registry exists — that's Sepolia, NOT Base Sepolia.** A no-dependency script (vanilla JS/GLSL) goes fully on-chain on either; a drop that needs a registry-hosted library on-chain must target `ABX_CHAIN=sepolia` (or run the resolver lane).
25
25
  - **Scope today = ERC-721 on testnet.** The shipped token standard is **ERC-721** (a 1/1, or a **Series** for many tokens), on Base Sepolia (default) or Sepolia via `ABX_CHAIN`. There is **no `--chain` flag (pick the chain with `ABX_CHAIN`) and no `--erc1155`/`--standard` flag** — don't invent one; mainnet + ERC-1155 are roadmap, not something you flip here. Map the ask to what ships: **"an edition of N" / "N copies"** → an ERC-721 **Series** (`abx deploy-series`, N tokens; for a priced sale of one piece, a 1-token Series). If a creator needs a true ERC-1155 shared-supply edition or an unsupported chain, say plainly it's not in the toolkit today rather than fabricating a recipe.
26
26
  - **Is the work finished yet?** If the creator is still *making* the piece, you're in **[Phase 0](#phase-0--make-the-work-first-skip-every-gate-below-until-its-good)** — iterate on the art and keep every deploy question off the table until they say ship. The gates below apply to launching something that already exists.
@@ -146,7 +146,7 @@ Files natural-sort into **tokens `0…N-1`** (`--count N` uses the first N); a t
146
146
  A **program is the content** (`abx deploy-code` → a `SeriesCode`): output is a function of live on-chain state (`tokenData`: coordinates + `seed` + PostParams), injected at view time. Everything from a [Series](#series-multi-token-drops) applies (mint order, lanes, identity, supply cap, minter, pause). This section is the **decision tree**; the operating depth — what to keep running, the resume loop, verify steps, render ops, lane internals, the arweave delay, selling — lives in **[reference/code-projects.md](reference/code-projects.md)**.
147
147
 
148
148
  **Infra fork FIRST (before any lane talk): a code project's thumbnail is *rendered* off-chain, so it ALWAYS needs a PUBLIC home you provide — there is NO zero-infrastructure code drop, and "fully on-chain" does NOT mean "nothing to run."** Settle the shape with the creator up front:
149
- - **Off-chain resolver** (`--public-base-url` + an effects runner, ~a few $/mo) — **the default for a drop you'll sell.** Auto-renders every mint + param change, serves traits with no Solidity, and stays **maneuverable** (metadata/serving evolve with no on-chain surgery) while marketplaces fetch a **small** `tokenURI`.
149
+ - **Off-chain resolver** (`--public-base-url` + rendering, ~a few $/mo self-hosted) — **the default for a drop you'll sell.** Auto-renders every mint + param change, serves traits with no Solidity, and stays **maneuverable** (metadata/serving evolve with no on-chain surgery) while marketplaces fetch a **small** `tokenURI`. A **managed provider whose descriptor says `render.attached`** covers both halves with one API key — no effects runner to stand up ([hosting.md → Managed providers](reference/hosting.md#managed-providers--a-resolver-someone-else-runs---remote-name)).
150
150
  - **Fully on-chain** (`--onchain-uri --image-base <a bucket you own>`) — maximal durability, no always-on service. Trade-offs: the whole ~200KB+ doc rides each `tokenURI` (some marketplace/indexer reads choke), **manual** stills (`abx render`), on-chain traits need a deployed renderer, later changes are on-chain re-points. Choose it deliberately when permanence outweighs maneuverability. *(The one zero-infra-AND-on-chain exception: the in-chain **Solidity** lane below.)*
151
151
 
152
152
  **Writing the program yourself (the creator brought an *idea*, not a file)? There's ONE runtime contract — get it right or the drop is silently broken** (seed never injects → every token identical; traits empty). The program reads state via **`abx.tokenData`** (a flat object: `.seed`, and each `--schema` key flat, e.g. `.palette`) and reports traits via **`abx.traits({…})`** — never an invented global (`window.tokenData`, `window.tokenTraits`) and never "defensively across variants." `abx.traits()` is the ONLY thing captured into `attributes`, on the resolver lane too. Verify with `abx inspect` (its **PostParams** + **Traits** lines reflect what the program actually reads/reports — if they're empty but you intended a param/traits, you read it the wrong way), THEN pick a lane. Full contract: [reference/code-projects.md → Authoring the program](reference/code-projects.md#authoring-the-program--the-abxjs-runtime-contract-get-this-right-first).
@@ -181,15 +181,15 @@ Master call is **custody × mutability**:
181
181
  | | **Mutable** (name/desc/traits may change) | **Immutable** (never changes) |
182
182
  |---|---|---|
183
183
  | **Tiny static** (≲ 24 KB/file, ≲ 256 KB total) | **on-chain renderer** — `--onchain-image --compress fastlz`. No host, mutable via `set-field`, permanent. | on-chain renderer + `lock-field` + `lock-uri` once it resolves. |
184
- | **Bigger / dynamic** (most PNG/JPEG) | **image off-chain, JSON on-chain, no server** — `--onchain-uri --backend arweave` (or `ipfs`). Renderer assembles JSON pointing at the bytes; many files → one `url-template` (O(1)). For metadata you edit often, a **hosted resolver** instead (`abx deploy-resolver`, [hosting.md](reference/hosting.md)). Not fully on-chain (~200 gas/byte). | image off-chain (Arweave = permanent) + on-chain renderer + `lock-field`/`lock-uri`. Or a frozen `ipfs://` override + `lock-uri`. |
184
+ | **Bigger / dynamic** (most PNG/JPEG) | **image off-chain, JSON on-chain, no server** — `--onchain-uri --backend arweave` (or `ipfs`). Renderer assembles JSON pointing at the bytes; many files → one `url-template` (O(1)). For metadata you edit often, a **resolver** instead — a managed provider or your own (`abx deploy-resolver`), [hosting.md](reference/hosting.md). Not fully on-chain (~200 gas/byte). | image off-chain (Arweave = permanent) + on-chain renderer + `lock-field`/`lock-uri`. Or a frozen `ipfs://` override + `lock-uri`. |
185
185
 
186
186
  **Four patterns, by where bytes live × how `tokenURI` resolves:**
187
187
  1. **Fully on-chain** (`--onchain-image`) — bytes + JSON on-chain. Tiny art only.
188
188
  2. **Image off-chain, JSON on-chain, no server** (`--onchain-uri --backend arweave|ipfs|cloud`) — the sweet spot for static art. Arweave/IPFS (permanent, content-addressed) or your S3/CDN (`--backend cloud --public-base <url>`; centralized, mutable). Many files → one `url-template`.
189
- 3. **Hosted resolver** (`--public-base-url` + `abx deploy-resolver`) — for mutable/dynamic metadata; you run a node.
189
+ 3. **Remote resolver** (`--public-base-url` + a node) — for mutable/dynamic metadata; **self-hosted** (`abx deploy-resolver`, you run it) or a **managed provider** (an API key, they run it). Same interface — swap with one re-point.
190
190
  4. **Inline SVG on-chain** — self-contained vector art inlined into `tokenURI`. For a **1/1** that's `abx deploy … --onchain-uri`; for a **Series** of tiny SVGs use `abx deploy-series … --onchain-image --compress fastlz` (bare `--onchain-uri` on a folder does NOT inline the images — it's the image-custody flag `--onchain-image` that puts SVG bytes on-chain per token).
191
191
 
192
- **Picking IPFS (or Arweave) does NOT mean running a server.** The `--onchain-uri --backend ipfs|arweave` path (pattern 2) bakes the image's public **gateway** URL into on-chain JSON — a pinning service's read endpoint (a *dedicated* Pinata gateway for IPFS), not a resolver you host. So when a creator chooses IPFS, **default to this no-server path** — image on IPFS, JSON on-chain, nothing to keep running (just keep the pin alive). You only need a **hosted resolver** (pattern 3) if they want *freely editable* metadata. Never present IPFS as blocked on "a public URL" or "a server always online": the gateway belongs to the pinning service and the JSON lives on-chain. (The one real input IPFS needs is `PINATA_JWT` in `.env` for pinning — that's an API upload, not a host.)
192
+ **Picking IPFS (or Arweave) does NOT mean running a server.** The `--onchain-uri --backend ipfs|arweave` path (pattern 2) bakes the image's public **gateway** URL into on-chain JSON — a pinning service's read endpoint (a *dedicated* Pinata gateway for IPFS), not a resolver you host. So when a creator chooses IPFS, **default to this no-server path** — image on IPFS, JSON on-chain, nothing to keep running (just keep the pin alive). You only need a **resolver** (pattern 3 — managed or self-hosted) if they want *freely editable* metadata. Never present IPFS as blocked on "a public URL" or "a server always online": the gateway belongs to the pinning service and the JSON lives on-chain. (The one real input IPFS needs is `PINATA_JWT` in `.env` for pinning — that's an API upload, not a host.)
193
193
 
194
194
  **No-server tradeoff (patterns 1, 2, 4):** with the on-chain renderer only the *image* is off-chain — any **description / traits / animation_url live on-chain** (gas to write, permanent, lockable), vs a hosted resolver where they're free to edit. Cheap (a shared value is **one collection-scope field**, not one per token — the renderer falls back token→collection), but the creator should choose "no server" knowing their text metadata is on-chain.
195
195
 
@@ -200,13 +200,15 @@ Get decisions 1–2 right before deploy (image commitment + resolver URL are wri
200
200
  - **`arweave` is nearly as easy as `fs` for small art** — Turbo default: **under 100 KB free, no setup** (a managed `.abx-self-host/arweave-key.json` minted on first upload; back it up with `abx storage backup-key`). Choose per command with `--backend` (stateless, no config file); a backend missing its secret falls back to `fs`.
201
201
  - **Who pays is a lane (`--storage-signer`)** — Turbo credits attach to an identity (managed key · `.env` key · browser wallet). **Before any top-up, check BOTH balances** (`abx storage balance --backend arweave` shows the managed key AND the wallet — spend the wallet's credits if present). On an upload error surface it verbatim — `…already been uploaded…` is *success* (dedup); don't reflexively top-up or switch to IPFS. Full lanes + failure playbook → [hosting.md](reference/hosting.md#arweave-via-turbo--the-easy-permanent-path-read-before-quoting-setup).
202
202
 
203
- **2. Public host URL**where the resolver runs (**off-chain custody only**). Baked into `tokenURI` at deploy, so the CLI **refuses an off-chain deploy without a public URL** (`ABX_PUBLIC_BASE_URL` or `--public-base-url https://…`) and **never bakes localhost** (that token resolves for no one). No exceptions.
203
+ **2. Public host URL — and who runs the resolver** (**off-chain custody only**). Baked into `tokenURI` at deploy, so the CLI **refuses an off-chain deploy without a public URL** (`ABX_PUBLIC_BASE_URL` or `--public-base-url https://…`) and **never bakes localhost** (that token resolves for no one). No exceptions.
204
204
  - **First ask whether you need a host at all** — tiny art is cheaper and more durable on-chain (no host). For bigger art, Arweave (no host to run) beats a resolver unless you need mutability or serve many files.
205
- - **Tunnels (ngrok/cloudflared) are preview-onlynever bake one on-chain** (dies on sleep, rotates on restart). A real launch puts the resolver on a host you control under your own domain (move = a DNS re-point).
205
+ - **A named remote is already configured (`ABX_REMOTE_<NAME>_URL` in `.env`)? Use it.** The creator already chose a provider don't stand up new infrastructure beside it. **Run `abx remote <name>` FIRST, before registering anything**: it prints the provider's chain coverage + whether rendering is managed, and it *validates the key* (`401` = the token in `ABX_REMOTE_<NAME>_TOKEN` is stale/wrong → they replace the value in `.env`; `403` = the key is fine but not authorized for this contract/chain → provider-side scoping, don't touch the key). Then register: `abx add <addr> --remote <name>`. Testing a replacement key without editing `.env` first: `abx remote <name> --remote-token <new-key>`.
206
+ - **Otherwise, two equal ways to have a resolver, one config change apart.** A **managed provider** — one base URL + one API key, no cloud account, nothing to keep alive; often **managed rendering** too, so a code drop needs no effects runner (**lead with this when the creator doesn't already run infrastructure or doesn't want to** — [hosting.md → Managed providers](reference/hosting.md#managed-providers--a-resolver-someone-else-runs---remote-name)). Or **self-host** (`abx deploy-resolver`, [hosting.md](reference/hosting.md)) — the creator owns the node and the cloud account. Same interface, same commands; a project moves between them with one re-point + re-register. **No provider key in hand and none to get? Self-host is the fully-supported path today** — the provider market is only starting to form; never invent or recommend a provider that isn't in front of you.
207
+ - **Tunnels (ngrok/cloudflared) are preview-only — never bake one on-chain** (dies on sleep, rotates on restart). A real launch puts the resolver on a host you control under your own domain (move = a DNS re-point), or behind a provider.
206
208
 
207
209
  **3. Identity** — `--name`, `--symbol`, `--royalty-bps` (default 500 = 5%), `--description "…"`, `--external-url <url>` (both served in the metadata — set them or the description is boilerplate). Owner + royalty receiver = the deploying wallet. These default to off-chain operator metadata (editable via `abx add <addr> --description "…"`). For a description that should outlast any node, add `--description-onchain` (or later `abx set-field <addr> --field description --text "…"`) → on-chain, freezable via `lock-field`; the resolver prefers the on-chain value. This is the per-field on-chain model — any field on-chain or off, one active `representation` (inline · reader · keccak256 · arweave · ipfs · url). Background: [metadata model](https://abx.docs.artblocks.io/protocol/metadata/).
208
210
  - **Credit + license** — deploy flags `--artist "…"` · `--license "…"` (also `--display-notes`, `--artist-links`) bake authorship + rights ON-CHAIN in the deploy tx (all three deploy commands); or set/change them later with `abx set-field <addr> --collection --field artist|license --text "…"`. Reserved collection fields served in `contractURI`, on any type (1/1 · Series · code). Detail: [operating.md → Authorship + rights](reference/operating.md#authorship--rights-credit--license).
209
- - **Propose a real name/symbol and confirm — never silently bake a generic folder-name guess.** A folder called `series`/`images`/`photos` infers junk ("Series" / "SRS"), and the CLI *refuses* demo defaults without `--name`/`--symbol` precisely because on-chain identity is effectively permanent. Suggest a specific title + a short ticker-style symbol drawn from the actual work, and get an explicit yes before deploying. Inference is a suggestion to confirm, not a default to ship — if the folder name is generic, say so and ask rather than proposing it.
211
+ - **Propose a real name/symbol and confirm — never silently bake a generic folder-name guess.** A folder called `series`/`images`/`photos` infers junk ("Series" / "SRS"), and on all three deploy commands the CLI *refuses* a real send that would bake its own placeholder identity (`--name`/`--symbol` missing) because on-chain identity is effectively permanent. **In `--dry-run` the same check only warns** (so a preview still runs before you have the creator's title); don't read that warning as "the CLI allows it" — the real deploy stops. Suggest a specific title + a short ticker-style symbol drawn from the actual work, and get an explicit yes before deploying. Inference is a suggestion to confirm, not a default to ship — if the folder name is generic, say so and ask rather than proposing it.
210
212
 
211
213
  **4. Image placement** — `--image <path>` (png · jpg · gif · svg · webp). The on-chain keccak256 (`image` field) anchors integrity; size is bounded by the backend, not the chain.
212
214
  - *Off-chain:* the served `image` is the backend's **gateway HTTPS URL** (`https://<gateway>/ipfs/<cid>`), not raw `ipfs://` (wallets/marketplaces can't render that). So off-chain needs a pinning service + a **public** gateway — with Pinata use a **dedicated** gateway (`--gateway https://<you>.mypinata.cloud`); a local kubo gateway is preview-only. The keccak stays the anchor → move gateways without a tx.
@@ -226,6 +228,8 @@ Get decisions 1–2 right before deploy (image commitment + resolver URL are wri
226
228
 
227
229
  Run `abx deploy --dry-run` for real values, present **this exact shape** — one row per on-chain value — then wait for go-ahead. **Mirror the dry-run's values; don't compose your own.**
228
230
 
231
+ **No signing key in `.env` yet? `--dry-run` still needs a deployer address — pass `--for 0x<the creator's wallet>`.** The address is a pure function of (factory, salt, deployer), so a preview can't compute it from nothing; it signs nothing, so no key is involved. Ask the creator for their wallet address once, up front — it's also what the real wallet-lane deploy takes (`--sign --for 0x…`).
232
+
229
233
  ```
230
234
  Deploy config — confirm before I send (everything below is written on-chain):
231
235
 
@@ -329,7 +333,8 @@ Whichever you land on, **keep using that same invocation for every command in th
329
333
 
330
334
  `.env` (in the creator's project dir) = **secrets only**:
331
335
  - **Signing:** a key (`SEPOLIA_FUNDED_PK` / `ABX_DEPLOYER_PK` / `SEPOLIA_WALLET_PK`) is needed ONLY for hot/unattended signing. If the creator owns a wallet, prefer **`--sign`** — no key in `.env`. `doctor`'s missing-key ✗ is **not fatal** on the `--sign` path.
332
- - `ABX_RPC_URLS`, `ABX_PUBLIC_BASE_URL` (hosted-resolver custody only), optional `OPENSEA_API_KEY`, optional `ABX_RESOLVER_ADMIN_TOKEN` (`deploy-resolver` generates it), plus any backend secret.
336
+ - `ABX_RPC_URLS`, `ABX_PUBLIC_BASE_URL` (remote-resolver custody only), optional `OPENSEA_API_KEY`, plus any backend secret.
337
+ - **Two resolver credentials — don't mix them up.** `ABX_RESOLVER_ADMIN_TOKEN` = **a node you run** (`deploy-resolver` generates it; it's the operator secret, and bare `--remote` uses it). `ABX_REMOTE_<NAME>_TOKEN` (+ `_URL`) = **a managed provider's per-account API key** for `--remote <name>` (same name normalization as `ABX_RPC_URLS_<CHAIN>`). A named remote deliberately never falls back to the node-admin token, so putting a provider key in `ABX_RESOLVER_ADMIN_TOKEN` silently won't work. Only `_URL`/`_TOKEN` are read — `ABX_REMOTE_<NAME>_KEY` is ignored (the CLI now flags a near-miss name).
333
338
 
334
339
  <sub>Working from the abx **source repo** (contributor)? `pnpm install`, then `pnpm abx <cmd>` or `pnpm sandbox`. Every command below is identical.</sub>
335
340
 
@@ -340,7 +345,7 @@ Whichever you land on, **keep using that same invocation for every command in th
340
345
  - **Public docs — the human-facing companion** at **https://abx.docs.artblocks.io** (quickstart, guides, the CLI/SDK reference, the protocol model). This skill is YOUR operating manual and stays authoritative for how to drive the CLI; the docs site is what you **link the creator to** for background/onboarding, and a place you can read if you want the protocol rationale behind a command. Don't send the creator commands to run (you run them) — send them the docs to *read*.
341
346
  - **Code projects — operating depth** (what to keep running, the resume loop, verify-it-resolves, render ops, `--onchain-uri`/`--image-base`/traits internals, arweave delay, `deploy-code` flags, mint timing/pause/supply) → **[reference/code-projects.md](reference/code-projects.md)**
342
347
  - **Operating an existing project** (owner ops, **artist credit + license fields**, **attaching files / the data plane**, selling via the shared minter, `abx mint-page`, moving hosting, resolver→resolver `migrate`) → **[reference/operating.md](reference/operating.md)**
343
- - **Hosting infrastructure** (storage backends, Turbo lanes + failure playbook, `deploy-resolver`, `deploy-effects`, local-vs-remote stores, token API routes, Docker) → **[reference/hosting.md](reference/hosting.md)**
348
+ - **Hosting infrastructure** (storage backends, Turbo lanes + failure playbook, **managed providers + named remotes + the service descriptor**, `deploy-resolver`, `deploy-effects`, local-vs-remote stores, token API routes, Docker) → **[reference/hosting.md](reference/hosting.md)**
344
349
  - **Environment detail** (RPC selection + failover, multi-chain, troubleshooting) → **[reference/setup.md](reference/setup.md)**
345
350
  - **Troubleshooting — "my NFT looks wrong"** (gray placeholder, stale-on-marketplace, tokenURI reverts, localhost baked, "not registered") — diagnose before acting → **[reference/troubleshooting.md](reference/troubleshooting.md)**
346
351
 
@@ -58,9 +58,19 @@ A failed upload is the #1 place an agent goes off the rails: it invents a cause
58
58
  - **Integrity** (IPFS/Arweave) comes from the content-addressed root (CID / manifest txid), not a per-token keccak. The gateway host is baked on-chain → moving gateways is a `set-field` (bytes stay put). Prefer a **dedicated** gateway.
59
59
  - **`url-template`** is a first-class representation ([spec](../../../../specs/protocol/onchain-metadata.md)); set by hand with `abx set-field <addr> --collection --field image --representation url-template --text "<gateway>/ipfs/<cid>/{id}.png"` then `abx set-renderer <addr>`.
60
60
 
61
- ## Hosted resolver`abx deploy-resolver`
61
+ ## Managed providersa resolver someone else runs (`--remote <name>`)
62
62
 
63
- For the large/mutable default. `abx deploy-resolver --provider <fly|render|vps> --domain <meta.you.xyz>` scaffolds the artifact and prints the exact next steps, the DNS record, and the bake reminder. Providers: **fly.io** / **render** (Docker PaaS, free tier, custom domain) and a **VPS** (compose + Caddy auto-TLS). The host is **read-only** — serves + accepts admin index-control, no signing key on it (writes are signed locally), so a compromised host can at worst serve wrong bytes (the keccak catches it). Prefer **a domain you control** (move = DNS, not a tx) but without one the scaffold now bakes the **real platform hostname** (`<app>.fly.dev` / `<app>.onrender.com`) so the resolver works out of the box (add a custom domain later). It NO LONGER bakes a dead `<app>.example` placeholder, and the resolver **refuses to serve** an `.example`/placeholder base (or a localhost base in a hosted image, `ABX_HOSTED=1`) — a loud fail beats silently serving dead image/animation links. The one sub-decision is *which provider* (ask + recommend); you scaffold, the human owns the cloud account + domain.
63
+ The other way to have a resolver: be a **customer** of a hosted provider instead of running a node one base URL + one API key, no cloud account, no Dockerfile, nothing to keep alive. Same interface, same commands as self-hosting ([spec](../../../../specs/self-host-toolkit/remote-services.md)); a project moves between a provider and your own node with one re-point + re-register ([operating.md Moving your hosting](operating.md#moving-your-hosting--two-cases-dont-conflate)).
64
+
65
+ - **Named remotes.** Put the provider in `.env`: `ABX_REMOTE_<NAME>_URL=<base>` + `ABX_REMOTE_<NAME>_TOKEN=<api key>` (same name normalization as `ABX_RPC_URLS_<CHAIN>` — `my-provider` → `MY_PROVIDER`). Then every remote command takes the name: `abx add <addr> --remote <name>`, ditto `index` / `forget` / `render` / `verify`, `abx migrate --from/--to <name>`, and owner-op nudges. Ad-hoc: `--remote <url> --remote-token <key>`. **Bare `--remote` stays the self-host default** (`ABX_PUBLIC_BASE_URL` + `ABX_RESOLVER_ADMIN_TOKEN`) — nothing changes for a node you deployed. A named remote never falls back to `ABX_RESOLVER_ADMIN_TOKEN`, so your node-admin secret can't leak to a provider.
66
+ - **Read the descriptor BEFORE registering — match the project to the provider.** `abx remote <name>` (or `curl <base>/.well-known/abx-service`, public, no key) prints what the service supports: **`chains`** must cover the project's chain (else registration is refused with `unsupported_chain`); **`render.attached`** means thumbnails/traits for code drops are rendered behind the provider — **skip `abx deploy-effects` entirely** (without it, renders are still yours: `abx render <addr> --remote <name>` or your own runner); **`auth.signupUrl`** is where a human gets a key.
67
+ - **The API key is the human's step — a membrane ask, never a chat paste.** Say: *"This provider covers your chain and manages rendering. You'll need an API key from `<signupUrl>` — put it in `.env` as `ABX_REMOTE_<NAME>_TOKEN` (never paste it in chat) and I'll do the rest."* Then verify with `abx remote <name>`: it lists the projects visible to the key. **401** = the key is missing/wrong (check the var, rotate at the provider). **403** = the key is valid but not authorized for this contract/chain — provider-side scoping, not a typo; don't retry-loop, check the provider dashboard.
68
+ - **The exit is guaranteed by the interface**, not by the provider's goodwill: registration is never load-bearing for resolution, and `abx migrate --from <provider> --to <anywhere>` reads only the provider's *public* endpoints — leaving is one config change plus (for a provider-hostname base) one on-chain re-point.
69
+ - **Honesty:** no default provider is baked into the CLI, the SDK, or this skill, and the provider market is only starting to form. A creator with a provider key (or a named remote already in `.env`) should use it; otherwise **self-hosting (next section) is the fully-supported path today** and what the rest of this file assumes.
70
+
71
+ ## Self-hosted resolver — `abx deploy-resolver`
72
+
73
+ For the large/mutable default, when the creator runs the node themselves (the alternative to a managed provider above — same interface, they own the cloud account). `abx deploy-resolver --provider <fly|render|vps> --domain <meta.you.xyz>` scaffolds the artifact and prints the exact next steps, the DNS record, and the bake reminder. Providers: **fly.io** / **render** (Docker PaaS, free tier, custom domain) and a **VPS** (compose + Caddy auto-TLS). The host is **read-only** — serves + accepts admin index-control, no signing key on it (writes are signed locally), so a compromised host can at worst serve wrong bytes (the keccak catches it). Prefer **a domain you control** (move = DNS, not a tx) — but without one the scaffold now bakes the **real platform hostname** (`<app>.fly.dev` / `<app>.onrender.com`) so the resolver works out of the box (add a custom domain later). It NO LONGER bakes a dead `<app>.example` placeholder, and the resolver **refuses to serve** an `.example`/placeholder base (or a localhost base in a hosted image, `ABX_HOSTED=1`) — a loud fail beats silently serving dead image/animation links. The one sub-decision is *which provider* (ask + recommend); you scaffold, the human owns the cloud account + domain.
64
74
 
65
75
  **The artifact is fully self-contained.** `deploy-resolver` writes `deploy/<provider>/` with its OWN `Dockerfile` + `.dockerignore` + config (a production image installs the published CLI: `npm i -g --no-optional @artblocks/abx-cli`). **Run every next step from that dir.** You never supply, copy, or hand-edit a Dockerfile. **If a step seems to need a file from elsewhere (`../Dockerfile`, a `packages/` dir, the repo), STOP — that's a scaffold bug, not something to work around.** Report it; don't MacGyver it. (Local from-source/contributor dev sets `ABX_RESOLVER_SOURCE=1` → a build-from-source artifact instead, still self-contained; you don't set this.)
66
76
 
@@ -74,18 +84,18 @@ For the large/mutable default. `abx deploy-resolver --provider <fly|render|vps>
74
84
 
75
85
  ## Render runner — `abx deploy-effects`
76
86
 
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:
87
+ A code project's marketplace still is rendered off-chain, so a **runner** (Playwright + Chromium — the resolver image stays browserless) must render each token's live view and hand the resolver the result. **A managed provider with `render.attached` in its descriptor does all of this for you — skip this section.** Self-hosting it: `abx deploy-effects --resolver-url <resolver>` scaffolds it for Fly (a self-contained `Dockerfile.effects` + `fly.toml` + vendored source), wired to the resolver and your storage home. **Decide where renders live up front** (`ABX_STORAGE_BACKEND`): **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 control plane (`POST /v1/effect-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
88
 
79
89
  - **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
90
  - **Co-located:** runner beside the resolver sharing one backend/volume (no admin token needed — both read/write the same store).
81
91
 
82
92
  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
93
 
84
- ## Local vs remote — two SEPARATE projection stores (read before deploying to a hosted resolver)
94
+ ## Local vs remote — two SEPARATE projection stores (read before deploying to a remote resolver)
85
95
 
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:**
96
+ A project is served only if it's in **the resolver's own** store. `abx deploy`/`abx add` (no `--remote`) index into **this machine's** store; a remote resolver — self-hosted or a managed provider — is a **different store** and doesn't learn about a contract just because you deployed it. A local deploy pointing its `tokenURI` at a remote resolver returns `{"error":"unknown project"}` for everyone — "works on my `serve`, broken for the world." **Bridge it:**
87
97
  - **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>`.
98
+ - **Remote resolver** (baked URL is remote): after deploy run **`abx add <clone> --remote [name|url]`** to register + index it on the node (bare `--remote` defaults to `ABX_PUBLIC_BASE_URL` + `ABX_RESOLVER_ADMIN_TOKEN`; a named remote uses its own `ABX_REMOTE_<NAME>_TOKEN`). Also **bridges** what the node can't derive: the durable `ipfs://`/`ar://` locator and off-chain traits. Post-deploy nudge (after a deferred mint) is **`abx index <clone> --remote`** (idempotent). Remove with **`abx forget <clone> --remote`**. Every remote command prints `REMOTE → <url>`.
89
99
 
90
100
  ### Deploying to a hosted resolver — pre-warm flow (preferred)
91
101
 
@@ -121,8 +131,9 @@ Default `http://localhost:8787` (or `ABX_PUBLIC_BASE_URL`). Routes carry the **c
121
131
  - `GET /` — read-only index of served contracts · `GET /t/<chainId>/<address>/0` — ERC-721 metadata · `…/0/image` — the image
122
132
  - `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
133
  - 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.
134
+ - `GET /.well-known/abx-service` — the **service descriptor** (public): what the node supports `interfaces`, `chains`, `auth` (incl. `signupUrl` for humans), `render.attached` (managed rendering). Read it before registering with any remote service.
135
+ - `POST /v1/projects` `{chainId, address, fromBlock?, factory?, description?, externalUrl?, attributes?, contentLocators?}` · `GET /v1/projects` (the projects visible to the token) · `DELETE /v1/projects/<chainId>/<address>` · `POST /v1/projects/<chainId>/<address>/reindex` · `GET /v1/projects/<chainId>/<address>/status` — the **control plane** ([spec](../../../../specs/self-host-toolkit/remote-services.md)): register/forget which contracts the node indexes, bridge off-chain traits + locators; a remote `add`/`forget`. Bearer-gated (`Authorization: Bearer <token>`), 404 code `disabled` when no token is configured. Errors carry a machine `code` (`unauthorized` 401 · `forbidden` 403 · `unsupported_chain` · `not_registered`). Indexing control only — never signing.
136
+ - `GET /api/project/<address>/verify` — bearer-gated too (it triggers outbound fetches). Run from the CLI: `abx verify <addr>` / `abx index <addr> --remote`.
126
137
 
127
138
  `/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
139
 
@@ -16,9 +16,10 @@ After launch the owner operates the project. Each command builds a tx, signs it
16
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
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
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 |
19
+ | `abx render <addr> [id…] [--remote <name\|url>]` | render missing stills/traits now (repair lane); `--remote` publishes to a REMOTE resolver | none — idempotent; local captures need Playwright chromium |
20
+ | `abx remote [<name\|url>]` | inspect a remote service: named remotes in `.env`, or a target's descriptor (chains · managed rendering · signup URL) + the projects your token sees | none read-only; the "is my provider key valid?" check |
21
+ | `abx add <addr> --remote <name\|url>` · `abx index <addr> --remote` · `abx forget <addr> --remote` | register / re-index (nudge) / deregister a contract on a REMOTE resolver (control plane) | which remote a name (`ABX_REMOTE_<NAME>_URL/_TOKEN`, a managed provider's key) or bare `--remote` (self-host default: `ABX_PUBLIC_BASE_URL` + `ABX_RESOLVER_ADMIN_TOKEN`); ad-hoc `--remote <url> --remote-token <t>` |
22
+ | `abx migrate <addr> --from <src name\|url> --to <dest name\|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; only the DESTINATION needs a token |
22
23
  | `abx set-royalty <addr> --bps <0-10000> [--receiver 0x..]` | change the royalty (receiver defaults to current) | the rate, and whether the payee changes |
23
24
  | `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
25
  | `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 |
@@ -103,6 +104,7 @@ Once a sale is live, `abx mint-page <token>` scaffolds a **self-contained Next.j
103
104
 
104
105
  - **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
106
  - **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**.
107
+ - **Leaving (or joining) a managed provider — the same two cases.** `abx migrate <addr> --from <provider name|url> --to <your-node-or-new-provider>` reads only the provider's **public** endpoints — the interface guarantees the exit needs zero provider cooperation, and no source credential. Then the standard cutover below (a provider base URL is the "provider endpoint" case: one on-chain re-point). Registration is never load-bearing for resolution, so a vanished provider costs availability, never state.
106
108
 
107
109
  ### The cutover, after a clean `migrate`
108
110
 
@@ -13,6 +13,8 @@ When `abx doctor` flags a tight range, or a reconstruction would be large/slow
13
13
 
14
14
  **One RPC var, network-scoped.** `ABX_RPC_URLS` is a comma/space-separated list of endpoints **for the active `ABX_CHAIN`** (there is no singular `ABX_RPC_URL`). The client **fails over** across them at request time — a request one endpoint rejects (a too-wide `eth_getLogs`) is retried on the next — and `abx doctor` **probes every endpoint** and reports a per-endpoint verdict (✓ wide range + archive · ⚠ usable but range-capped · ✗ unusable, with the reason, e.g. *archive refused* / *wrong network*), then names the best for reconstruction. So list a couple, let `doctor` pick, and order the list best-first. For **multiple chains**, set `ABX_RPC_URLS_<CHAIN>` (e.g. `ABX_RPC_URLS_BASE_SEPOLIA`, `ABX_RPC_URLS_SEPOLIA` — the default `ABX_CHAIN` is `base-sepolia`) — a per-chain list that overrides the bare var, never mixed, so one network's endpoints can't leak into another. The var *names* a network but the URL could point anywhere, so before any write the toolkit **verifies `eth_chainId` matches `ABX_CHAIN`** and hard-fails on a mismatch (reads stay unguarded/fast) — that's what makes "which network is this?" a checked fact, not a guess.
15
15
 
16
+ **Named remotes follow the same env-name normalization**: `--remote my-provider` reads `ABX_REMOTE_MY_PROVIDER_URL` / `ABX_REMOTE_MY_PROVIDER_TOKEN` (uppercase, non-alphanumerics collapse to `_`). Inspect what's configured with `abx remote` ([hosting.md → Managed providers](hosting.md#managed-providers--a-resolver-someone-else-runs---remote-name)).
17
+
16
18
  **Re-index is incremental by default** — it resumes from the last-indexed block and only fetches new blocks, so routine re-indexing (after an owner op, or just refreshing) stays instant *regardless* of the range cap. Even the **first** reconstruction scans only from the **deploy block** (not genesis), so range/archive capability bites only on a genuinely long span — a forced full replay (`abx index --full`, a fresh projection, or the dashboard's "Re-index from chain" — the durability proof) of a contract deployed long ago. A "why is this scanning millions of blocks?" moment almost always means the scan floor is wrong (block 0), not that the RPC is inadequate.
17
19
 
18
20
  ## Troubleshooting
@@ -22,7 +24,7 @@ When `abx doctor` flags a tight range, or a reconstruction would be large/slow
22
24
  - **storage 'cloud' missing accessKeyId/…** → set `ABX_S3_*` in `.env`. **'ipfs' unreachable** → Kubo not running or `PINATA_JWT` missing.
23
25
  - **re-index slow, or a "scan too large" stop** → **first check the scan floor: is it indexing from block 0 instead of the deploy block?** That's the usual cause of a "huge" scan (an `add` that lost the deploy block). The floor is `flags['from-block'] → the stored deploy block → on-chain discovery`; if none resolve, the CLI refuses rather than sweeping genesis. Only once the floor is correct does the RPC's `eth_getLogs` range matter: the toolkit chunks automatically, but a genuinely large job stops with an estimate rather than grinding. Real fix then: research a current higher-range free endpoint and set `ABX_RPC_URLS` (`abx doctor` reports your range); or re-run with `--yes` to chunk through. **archive-range getLogs refused** → use an archive-capable provider.
24
26
  - **`serve`/resolver returns `{"error":"unknown project"}`** → the resolver you hit doesn't have that contract in *its own* projection store **yet**. Two distinct situations — diagnose which, and NEVER default to "RPC limit":
25
- - **A hosted resolver you just registered** (`abx add … --remote`): it may still be **backfilling** — hit `GET /` (or `abx status --remote`) to see if it's appearing. If it's slow or stuck, the cause is almost always a **from-genesis scan (from-block=0)**, not the RPC tier — a fixed `abx add` forwards the deploy block, so re-run it and confirm the floor. (Fixed in-toolkit: a first remote add now forwards/derives the deploy block and refuses a genesis default.)
27
+ - **A remote resolver you just registered** (`abx add … --remote`): it may still be **backfilling** — hit `GET /` (or `abx remote <name|url>`, which lists the projects the token sees) to see if it's appearing. If it's slow or stuck, the cause is almost always a **from-genesis scan (from-block=0)**, not the RPC tier — a fixed `abx add` forwards the deploy block, so re-run it and confirm the floor. (Fixed in-toolkit: a first remote add now forwards/derives the deploy block and refuses a genesis default.)
26
28
  - **A local `abx serve`**: a **store/port** problem. Usual causes, in order: (1) a **stale/duplicate `abx serve` from an old session** holds the port and serves a *different* store — hit `GET /` and see what it lists; (2) you're serving a different store directory than the deploy indexed into; (3) the contract was never registered there. Fix the server/port/registration.
27
29
  - **`/a/…` returns `{"error":"no live view — not a code project"}` on a project that IS a code drop** → the resolver indexed *above* the deploy block, so it never saw the `code` field written at deploy — a scan-floor bug, **not** a resolver version/compat gap (do NOT redeploy as a static NFT). This is the *opposite* of the genesis bug: the floor is too **high**, not too low. It happened when a `--sign` code deploy spanned blocks (deploy at N, mint at N+2) and the mint block was recorded as the floor. Confirm: `GET /api/project/<addr>` → `collectionFields` is `[]` and `fromBlock` sits above the deploy block. Fix: `abx add <addr> --remote <url> --from-block <deployBlock>` — a *changed* floor forces a full replay that picks up the `code` field. Find the true deploy block with `abx add <addr>` locally (it prints "deploy block N (discovered on-chain)"). (Fixed in-toolkit: `deploy-code` now records the clone-CREATION block, and discovers it on-chain rather than trusting the last-tx receipt.)
28
30
  - **directory-mode live view 302s to a doubled URL** (`https://arweave.net/https://arweave.net/<txid>/index.html`) → the `code` locator was stored as a full gateway URL and the gateway got prefixed again. Fixed in-toolkit (deploy stores the bare txid/CID; the resolver serves an already-absolute locator verbatim). A resolver image built before the fix still doubles — redeploy it to pick up the resolver-side tolerance.
@@ -32,5 +34,9 @@ When `abx doctor` flags a tight range, or a reconstruction would be large/slow
32
34
  - **the project's on-chain `tokenURI` points at a HOSTED resolver but it isn't serving** → **do NOT "fix" it by running `abx serve` locally and handing over a `localhost` link.** The baked URL is the hosted one; a local serve resolves for no one but you. Fix the HOSTED resolver instead: re-run `abx add <addr> --remote` (now forwards the deploy block), check `GET /` on the host, and vet its RPC with `abx doctor`. A local serve is only ever the answer when the baked base is that same local machine.
33
35
  - **port hygiene before `serve`** → check the port is free first (`lsof -i:8787`). If an old session holds it, kill that process or serve this project on a distinct `--port <n>` (and point its `--public-base-url`/tokenURI accordingly) — a stale server silently answering on the port is the #1 cause of "works for me, `unknown project` for everyone."
34
36
  - **serving code projects is RPC-heavier than static art** → the live view rebuilds `tokenData` per request, and template-mode script chunks + data-backed (`String`/`Bytes`) params are read **live per view** (scalar PostParams come from the indexed projection, so they don't hit RPC each view). A resolver serving many code projects under marketplace traffic wants a range-generous, reliable RPC — this is a genuine scaling consideration, distinct from the getLogs range cap above.
37
+ - **a remote command returns 401 unauthorized** → the token the CLI resolved is missing/wrong for that target — the error names the var it used (`ABX_REMOTE_<NAME>_TOKEN` for a named remote, else `ABX_RESOLVER_ADMIN_TOKEN`, else `--remote-token`). Check that var, or rotate the key at the provider if it may have leaked. Named remotes deliberately never fall back to `ABX_RESOLVER_ADMIN_TOKEN`.
38
+ - **a remote command returns 403 forbidden** → the key is **valid but not authorized** for this contract/chain — provider-side scoping, not a typo. Don't retry-loop and don't swap tokens blindly: check the provider dashboard, and the descriptor's `chains` (`abx remote <name>`).
39
+ - **register refused with `unsupported_chain`** → the service doesn't serve the project's chain — its descriptor `chains` says which it does. Pick a provider that covers the chain, or self-host.
40
+ - **`/.well-known/abx-service` 404s** → an older self-hosted node (fine if it's yours — the remote commands still work against it once redeployed to the current image) or **not an ABX service at all** — verify the URL with `abx remote <url>` before registering anything; don't register blind.
35
41
  - **wallet lane: "no injected wallet"** → the human has no extension wallet; fall back to the hot lane (`--send`) if they're comfortable, or the cold lane for a Safe.
36
42
  - **owner op reverts** → the signer isn't the owner/holder. Check `abx status` / the `/api/project` state for the current owner; sign as that wallet.
@@ -16,7 +16,7 @@ Walk the cause down:
16
16
  ### I changed the metadata but the marketplace shows the old value
17
17
  - **First confirm the change landed:** `abx tokenuri <addr> --token N` (or the resolver's JSON) — if it shows the NEW value with `abx_provenance` `onChain: true`, the write worked. The gap is 100% the marketplace's cache.
18
18
  - **Nudge it:** `abx refresh <addr>` — ABX emits **ERC-4906** on metadata changes so 4906-aware marketplaces self-refresh; `refresh` calls OpenSea directly with `OPENSEA_API_KEY`, else prints the link to click. Marketplace caches still lag on their own schedule — that's their side.
19
- - **Hosted resolver?** It serves from its own store — a local edit must reach it: `abx add <addr> --remote <url>` / `abx index <addr> --remote <url>` re-indexes the hosted node. Never "resubmit the transaction" or redeploy.
19
+ - **Remote resolver (self-hosted or a managed provider)?** It serves from its own store — a local edit must reach it: `abx add <addr> --remote <name|url>` / `abx index <addr> --remote <name|url>` re-indexes the remote node. Never "resubmit the transaction" or redeploy.
20
20
 
21
21
  ### `abx tokenuri` / Etherscan reverts on a fully-on-chain code project
22
22
  A large on-chain `tokenURI` document can exceed the **unauthenticated eth_call gas cap** some RPCs/explorers impose on a public read — the call reverts in that UI but the data is fine on a normal RPC. This is **expected for a big on-chain doc, NOT an indexing problem** — do not `abx index --full` or redeploy. Read it via a node without the cap.