abx.
Using ABX

Remote services

A resolver does not have to be one you run. The same control plane that lets you register a project with your own node lets you register it with a managed provider — a hosted service you hold an API key for. Self-hosting and a provider are one configuration change apart: same interface, same commands, and leaving is never blocked on the provider's cooperation. The interface is pinned by the remote-services spec.

Named remotes

A remote is a {url, token} pair resolved per invocation — the CLI stays stateless. Put a provider in .env:

ABX_REMOTE_MYPROVIDER_URL=https://meta.provider.xyz
ABX_REMOTE_MYPROVIDER_TOKEN=<your API key>

Then every remote command takes the name: abx add <addr> --remote myprovider, and likewise index, forget, render, verify, abx migrate --from/--to <name>. Names normalize the same way as ABX_RPC_URLS_<CHAIN>: uppercase, runs of non-alphanumerics collapse to _.

  • --remote <url> targets an ad-hoc URL; the token comes from --remote-token or ABX_REMOTE_SELF_TOKEN.
  • Bare --remote stays the self-host default: ABX_PUBLIC_BASE_URL plus ABX_REMOTE_SELF_TOKEN — nothing changes about which node you're talking to if you deployed one, only the env var it reads client-side (renamed from ABX_RESOLVER_ADMIN_TOKEN; abx doctor flags the old name if it's still set).
  • A named remote never falls back to ABX_REMOTE_SELF_TOKEN, so your own node's credential cannot silently go to a third-party provider.

Inspect a remote before trusting it with a registration: abx remote <name|url> prints its descriptor and, when a token resolves, the projects visible to that token — the one-command "is my key valid?" check.

The control plane

Every conforming service exposes the same versioned, role-neutral HTTP surface, authenticated with Authorization: Bearer <token>. The token authorizes index and metadata control only — never on-chain signing.

Method and pathPurpose
POST /v1/projectsRegister a project (idempotent upsert; chainId required in the body).
GET /v1/projectsList the projects visible to this token, each with its indexing status.
DELETE /v1/projects/{chainId}/{address}Deregister.
POST /v1/projects/{chainId}/{address}/reindexNudge a re-index.
GET /v1/projects/{chainId}/{address}/statusIndexing lifecycle + freshness.
POST /v1/effect-artifacts, POST /v1/effect-statusThe artifact registry: a producer registers each render output and reports run state.

Errors carry a machine code alongside the human message: unauthorized (401, fix the token), forbidden (403, the token is valid but not authorized for that resource — a multi-tenant provider's scoping), unsupported_chain, not_registered, disabled (the node has no control-plane credential configured), invalid_request, and internal_error (500). A 500 never carries internal detail: upstream errors embed the node's own endpoint URLs, and a keyed URL is a credential, so the cause goes to the operator's logs rather than to the caller.

Three ways a read can miss

The public read routes carry the same machine code when a lookup misses, because "no metadata came back" has three very different causes and a bare 404 for all of them is a trap — a client that built the URL itself reads its own mistake as a provider outage:

AnswerMeaningWhat to do
400 invalid_requesta real route, wrong shape. The body names the correct template, and may include didYouMeanfix the path — or better, stop building paths
404 unknown_routethis node serves nothing at that path. The body lists the templates it does servecheck the node's interfaces version
404 not_registeredthe path and chain were right; this node doesn't index that contractregister it: abx add <address> --remote <name>
400 unsupported_chainthis node serves a different chain. The body carries the chains it doespoint at a node for your chain

The classic miss is dropping the {tokenId} off /t/{chainId}/{address}/{tokenId} expecting collection metadata — that's /c/{chainId}/{address}, and the 400 will say so.

Treat these as diagnostics, not discovery. The route grammar is fixed by the abx-token-api/v1 interface, and per contract the authoritative URL is the one committed on-chain — read it with abx tokenuri / abx contracturi rather than assembling one.

Registering is asynchronous underneath

Catching a contract up to chain head takes milliseconds for a fresh deploy and can take minutes for a cold replay of an older contract on a rate-limited RPC. So POST /v1/projects has two conformant answers, and the HTTP status code tells them apart:

  • 200 — catch-up finished inside the request. The body carries the real counts.
  • 202 — the registration is durable and catch-up is still running. No counts yet; poll .../status for progress.

Two guarantees make that safe. A service must persist the registration before it starts replaying — so a flaky RPC produces a slower backfill, never a lost registration — and a re-POST while catch-up is running never starts a second replay.

You mostly don't have to think about it, because abx add <addr> --remote <name> handles both: it prints registered — backfilling…, polls until the project is live, and then prints the same summary it would have printed synchronously. --no-wait returns immediately instead.

Indexing status

status is a closed set of five words, and they mean the same thing on a managed provider as on a node you run yourself:

StatusMeaning
queuedRegistered and accepted; catch-up hasn't started.
backfillingCatching up from the deploy block to head. May already serve partial state.
liveCaught up, tracking head.
staleWas live, now lagging — still serving its last-known state.
failedCatch-up errored. Carries a cause; the service retries with backoff.

Check it with abx status <address> --remote <name> (add --watch to follow along), or without --remote for a project on your own node — same command, same five words. abx remote <name> shows the roll-up for everything a key can see: "3 live, 1 backfilling, 1 failed (rpc_rate_limited)".

A failed status names a machine-readable cause from a closed set — rpc_unavailable, rpc_rate_limited, not_abx_contract, internal — and the accompanying hint is guaranteed credential-free, because "the provider's RPC is throttled, it will retry" and "the provider is broken" call for very different reactions and neither should require a support ticket to distinguish.

not_abx_contract is the one that points back at you: the service replayed the chain at that address and found no ABX events — almost always a wrong address or the wrong chain. It's decided by replay rather than at registration time, because the only trustworthy evidence that a contract is ABX is that it actually emitted the event spine; anything a service could check instantly is just the contract saying so about itself. (A service may separately choose to serve only contracts deployed from a canonical factory. That's a business policy, not the protocol's line, and such a service refuses those registrations up front rather than accepting them.)

Indexing status is service observability, never protocol truth. Nothing about resolving a token consults it, it never appears in served metadata, and a project reconstructs from chain no matter what any provider says about it. GET .../status also reports fromBlock / toBlock / headBlock, which is what a progress bar is made of.

The service descriptor

GET /.well-known/abx-service is public and says what a service supports before you send it anything:

{
  "service": {"name": "Example Provider", "version": "0.1.0"},
  "interfaces": ["abx-token-api/v1", "abx-control-plane/v1"],
  "chains": [84532],
  "auth": {"scheme": "bearer", "signupUrl": "https://…", "docsUrl": "https://…"},
  "render": {"attached": true, "effects": [{"key": "render", "outputs": [{"key": "image", "mimeType": "image/png"}]}]},
  "baseUrl": "https://meta.provider.xyz"
}
  • interfaces declares which surfaces exist. abx-control-plane/v1 appears only when the control plane is enabled, which is how a client tells "disabled" from "wrong URL". There are exactly two ids and they are all-or-nothing: declaring one promises every route it names, so partial support is expressed by not declaring the interface (or per-caller with a 403), never by declaring it and serving a subset. The artifact-registry routes ride abx-control-plane/v1 rather than an id of their own — see who holds the bytes for why there is nothing to negotiate.
  • chains are the EIP-155 ids served; registering a project on any other chain is refused with unsupported_chain.
  • auth.signupUrl is for humans: where a customer gets an API key.
  • render.attached means rendering runs behind the service.

Managed rendering

The resolver already fronts the effects runner — clients never talk to a renderer directly. So a provider that runs its own renderer advertises render: {attached: true} and a generative project gets thumbnails and traits with nothing extra to stand up: no abx deploy-effects, no Playwright, no runner. Without it, renders remain yours to produce (abx render <addr> --remote <name> or your own runner publishing through /v1/effect-artifacts).

Who holds the bytes

A resolver is not an object store, and the interface makes that structural rather than a matter of manners. Every effect output is one of two classes, decided by how it binds into the metadata JSON:

  • Referenced — the still, a video, a model: the projection carries its URL, or it only appears in the artifacts manifest. The producer keeps the bytes and registers a locator; the resolver records the pointer and 302-redirects to it. It never fetches the locator when you register it, and never proxies the bytes. Sending bytes for a referenced output is a 400 — the resolver would gain nothing (it redirects either way) and would take on storage, retention and egress.
  • Bound — script-reported traits: its content is stitched into attributes, so a locator could never work (it would put a third-party fetch on tokenURI, the hottest read there is). These are published as inline bytes, capped at 64 KB, and the resolver keeps them only at the token's current inputsHash — so what it holds is bounded by supply, not by how often you change a param. Sending a locator for a bound output is also a 400: it would be recorded and then never stitch, which is worse than a refusal.

The practical consequence for rendering: a runner that doesn't share the resolver's disk needs a storage backend that can name a reachable URLcloud (S3/R2 with a public base), ipfs, or arweave. These are peers, not a ranking; pick on cost and operations. Derived output is re-creatable (a lost render is a re-render), so the protocol expresses no preference among schemes, and an https:// gateway URL you chose is exactly as legitimate as ipfs://. The only real requirement is that someone other than you can fetch it: a loopback host or an expiring presigned URL is refused. The default fs backend can't name a URL at all, so abx render --remote and abx deploy-effects refuse it up front rather than after the render — or render co-located with the resolver, where there is nothing to publish and any backend works.

Leaving a provider

Registration is never load-bearing for resolution: the on-chain base URL and chain replay stay the source of truth, and the public read plane is guaranteed to be sufficient to migrate with zero provider cooperation. abx migrate <addr> --from <provider> --to <anywhere> reads only public endpoints, then the cutover is one DNS change or one on-chain re-point. See self-hosting → migrating.

No default provider ships in the CLI, the SDK, or the skill — any provider implements the same public interface, and the reference resolver is itself a conforming implementation. The provider market is nascent; self-hosting remains the fully-supported path while it forms. A provider can self-verify with the conformance fixture: pnpm conformance -- <base-url> --token <key>.

On this page