abx.
Protocol

What a project owner can do

ABX is self-serve. Nobody vets a project, and there is no platform in the middle to appeal to, so the creator who deployed a collection is a genuinely privileged role: they own the contract, and they keep powers over it after the sale. The protocol's job is not to pretend otherwise. It is to make sure that role can never reach a collector's funds or a collector's tokens, and to put everything else on-chain where a buyer can read it and price it.

Most of these powers exist because a project needs them — re-host an image, fix a typo, run a second sale — and a creator who destroys their own collection has destroyed the thing they were paid for. The point of listing them is not that creators are untrustworthy. It is that "trust me" should be optional: every item below is a read you can perform yourself, before you buy.

That is also the rule for what the contracts check. Where a new capability is no worse than something the owner can already do more directly, the protocol does not spend gas and complexity preventing it — it states the assumption here and gives you the read. On-chain validation is reserved for three jobs: protecting a collector's funds and tokens, which is absolute; catching an obvious footgun before a creator fires it; and making a guarantee this protocol states out loud actually true. Simple, robust, and honest beats defensive.

What the owner can do

Block transfers, on an enrolled collection only. A collection that opted into ERC-721C / ERC-1155C at deploy checks every transfer against a validator contract, and the owner can re-point or suspend that validator at any time. A validator that reverts blocks transfers entirely until the owner changes it. Enrollment is permanent and decided at deploy, so a collection that deployed plain can never acquire this — check supportsInterface or abx state, which reports the validator when one exists. One safeguard: if the owner renounces ownership, anyone may suspend the validator, and nobody can re-arm it.

Block transfers and mints with a param transfer hook, on a code project whose hooks are not frozen. A SeriesCode/EditionCode project may wire a transferHook — a contract the creator deploys that the token calls on every ownership change. It is a veto: if it reverts, the transfer fails. It also runs at mint, since a mint is a transfer from the zero address, so a reverting hook stops issuance for that project too, including through the shared minter. (The hook's contract also runs on burns, where a collection opted into them — see below.) This is the mechanism behind work that responds to its owner, and it is a genuine power over whether you can sell. Two reads settle it: paramHooks() — a zero in the third slot means nothing runs on transfer — and whether the set is frozen (ParamHooksFrozen, or paramHooksLocked()). abx state prints both. Unlike the transfer validator above, a hook is always the creator's own contract, and that one fact decides two things: it is why the protocol lets a hook block issuance where the validator never does, and it is why setParamHooks stays owner-only forever — including on a renounced collection, where the validator's release valve does apply. Nobody but the owner can arm a hook, and nobody but the owner can disarm one.

Change a live sale's terms at any time, with no timelock. Price, payment token, allocation, payee, and pause state are all owner-writable on the fixed-price minter and the token, and take effect immediately. A change to the price or payment token cannot reach a purchase already in flight — the buyer states the terms they accept and the purchase reverts SaleTermsChanged instead of paying the new ones.

A change to the payee is different, and worth knowing: primaryPayee is read fresh from the token during each purchase and sits outside that guard, so an owner who re-points it can redirect the proceeds of a purchase that is already in the mempool. The buyer still pays exactly what they authorized and still receives their token — what changes is who receives the money. That matters if you are buying from a project whose payout address you were relying on, and it is why the payee is listed here as a live power rather than a settled one.

Keep receiving primary-sale proceeds after selling the project. primaryPayee does not move when ownership does: transferOwnership transfers the contract, not the payout address. The new owner has to set it themselves, and until they do, sale proceeds go to the address the previous owner named. Read primaryPayee() and compare it to owner().

Mint reserves outside the minter's allocation. allocation is bookkeeping inside the minter contract; the token's mint gate is only "owner or the assigned minter". Assigning a minter therefore grants it the whole remaining supply cap rather than a slice, and the owner can mint alongside it (and while paused) up to that same cap. The cap is the real ceiling — read maxInvocations().

Change the royalty receiver and rate at any time, forever — but never above the cap. There is no royalty lock and no timelock on the rate, and the standard is ERC-2981, which marketplaces honor at their discretion. But the rate is bounded by a ceiling the owner set at deploy (0–100%) and can only ever lower, never raisemaxRoyaltyBps() is that ceiling, and reduceMaxRoyaltyBps only moves it down. So a project that set a low cap has made a real, monotonic promise a buyer can read: the royalty can never exceed that number, and can only fall. Tooling defaults the cap to 10%.

What that leaves is headroom, and it is worth naming because a listing page never shows it: any gap between the live rate and the ceiling is royalty the owner can add unilaterally, with no notice to anyone. So "5% royalty" is a fact about today and a ceiling of 10% is the fact about tomorrow — read both, always together (royaltyInfo and maxRoyaltyBps(); abx state prints the pair). The corollary belongs to creators: reducing the cap to your current rate is what turns "5% today" into "5%, provably, forever"abx set-royalty-cap <address> --cap 500. It costs one transaction and is the only way to make a rate a promise.

Burn — only if the collection opted in, and never as an owner power. A collection deployed with burning enabled (burnable() reads true, fixed at deploy) lets a holder or their approved operator destroy their own token; a collection that did not opt in can never burn, by anyone. There is no owner bypass either way — see the burn line under "what an owner provably cannot do".

One consequence to price on a burnable edition: its maxSupply(id) cap is a live ceiling, not an ever-minted one. A burn frees a slot the creator or minter can re-mint, so "N copies" on a burnable edition means "at most N at once", not "at most N ever" — including a burnable OneOfOneEdition (a 1/1 edition can be re-minted after its copy is burned). This differs from the ERC-721 lane, where the cap floors on a monotonic mint cursor burn never touches, so "N" there is "N ever". A non-burnable edition has no such gap (live == ever). Read burnable() alongside the cap.

On the ERC-721 lane, a burn is permanent and visible: ids come from a monotonic nextTokenId, so a destroyed id never returns, tokenURI(id) reverts from then on, and any conforming resolver answers 410 Gone for it rather than serving stale metadata. nextTokenId() − totalSupply() is the collection's burn count, readable by anyone.

Re-point metadata. Base URIs, per-token overrides, renderers, on-chain fields, script chunks, dependencies, and the dependency registry are all owner-writable until the matching lock is engaged, and each lock is one-way. Three caveats a buyer should know, because none is covered by the metadata locks: a parameter has its own, separate lock — an ungoverned parameter has none at all, so the owner keeps writing it and the program keeps reading the new value as token data, while a parameter with a schema can be welded permanently, token-scope value and schema together, by a lockAfter in the past. That weld has one deliberate exception, and it is an owner power: a contract-scope default on the same key can still be deleted. clearContractParam sits outside the schema guard on purpose — it is the only exit from a contract-scope value poisoned before the schema existed, which would otherwise be frozen for every token in the collection with no way back. So an owner may set a collection-wide default, weld the key, sell tokens that inherit that default, and then clear it, changing every token that never wrote a value of its own. What clearing cannot do is forge a value, bypass an auth rule, or touch a token-scope value already written — the schema keeps governing every token-scope write exactly as before. A buyer reading a "frozen parameter" claim should check whether the value is written per token or merely inherited: tokenParam(id, key) says which. And a Registry dependency fetches its bytes live from a contract the registry's owner controls — those bytes ride into the rendered document exactly as the registry hands them over, unread and unscanned, because pointing a project at a registry is the decision to trust whoever owns it. onChainStatus(token) reports which dependencies resolve that way. So a fully locked project is one whose stored metadata is frozen — a real guarantee, and not the same as a frozen output. Read paramSchema(key) per key to see which parameters are welded and which are still open; abx state prints the whole governed set with its lock dates.

Author the metadata document's contents directly. Two paths hand the owner's bytes to the renderer verbatim, because that is what they are for. attributes carried inline is raw JSON — the field must be a JSON array, so there is nothing to quote or escape, and the renderer splices it in as written. A malformed value breaks the document, and a value that closes the array and appends ,"image":"…" adds a sibling member that a last-wins parser prefers. A field renderer likewise declares its own content type, and that string lands verbatim in the data: URL built around its bytes; a data: URL's mediatype ends at the first comma, so a content type containing one changes what the browser renders. Neither is checked on chain. Both are the owner shaping their own collection's document, neither reaches anything they could not do more directly — image and animation_url are fields they already set — and checking would mean a JSON parser in Solidity, paid on every render, to stop a creator from misrepresenting their own project. The read is the document itself: fetch tokenURI and look, then read abx_provenance, which names the representation and source behind every field the renderer assembled.

Swap the seed source. Where a code project draws mint seeds is owner-writable and affects future mints only; seeds already assigned are settled.

Let a token's seed be reassigned — only if that was declared before the collection's first seed exists, and not necessarily by the owner. A token's seed can be set again if and only if a seed parameter schema was declared before the collection's first seed existed. After that the key is closed permanently, in either direction, for every token the collection will ever mint. Read the schema for who: its AuthOption names the authorized party, and the common choice is TokenOwner — the collector picking the seed of their own token, which is a feature rather than a power over them. Creator (the project owner), a named Address, and the Or combinations are the other possibilities, and only the ones naming Creator put this in the owner's hands.

The boundary is the collection's first seed, not the sale opening — those are not the same moment, and the gap between them is owner-reachable. paramSchema("seed") reading exists = false describes only that block; it is not a forward guarantee. Declaring the schema is an ordinary, non-reentrant transaction the owner can send at any time before the first seed lands, including one that lands ahead of a buyer's already-signed, already-broadcast first-mint transaction. A buyer who checks, sees no schema, and immediately signs a purchase can still have that purchase settle after a same-block or higher-priority setParamSchema("seed", …) — at which point their newly-minted token, and every token minted afterward (the flag is collection-wide, not per-token), is reassignable under whatever AuthOption the owner just declared. So read this power honestly as the project owner fixed the seed's governance model no later than the moment they opened minting to anyone, never as "no schema was seen, so none can ever appear." paramSchema("seed") is still the right read — just not one a pending transaction can rely on. Note what any authorized party's write gives them, once a schema exists: the caller supplies the value, so it is a chosen seed, not a re-roll — nothing re-invokes the seed source.

Walk away. The owner can transfer ownership to any address, including one nobody controls, or renounce it outright. A renounced collection is permanently frozen in whatever state it was left in — no more re-points, no more royalty changes, and no way back. Exactly one thing deliberately unfreezes at that moment, in the one direction that protects collectors: anyone may suspend the transfer validator, and nobody may re-arm it. That valve is scoped to the validator on purpose — a validator is usually a third party's policy contract and can only ever refuse a transfer, so switching it off can only permit more. A param transfer hook is the creator's own contract and is often the work, so nobody but the owner may touch it, ever. Which means renouncing with a live transfer hook that reverts is a real end state: it freezes every collector's token and all remaining issuance, permanently, with nobody left to fix it. paramHooks() and paramHooksLocked() are the reads that tell you whether a project can reach it.

What the owner cannot do

This is the half that matters, because it is the part you do not have to verify project by project. It holds for every ABX collection, by construction.

Take a buyer's funds beyond the terms that buyer signed for. purchase and purchaseTo require expectedPaymentToken and a maximum — maxPrice on the 721 lane, maxTotalPrice on the edition lane — and revert SaleTermsChanged if the live sale no longer matches. There is deliberately no "no maximum" sentinel. So re-pricing a live sale cannot front-run a pending buy, and switching the sale to a different ERC-20 cannot reach an allowance granted somewhere else. See the buyer states the terms they accept.

Exceed the supply cap. maxInvocations on a 721 and the per-id cap on an edition are monotonic: they can only be lowered, never below what already exists, and never back to open once set. Every mint path — the owner's, a minter's, a router's — passes the same check.

Rewrite or clear a settled seed. Absent the pre-declared schema above, a seed is written once and every route to change it reverts SeedSettled — including clearing it, and including storing one as a data blob to sidestep the check.

Arm a transfer veto on a project whose hooks are frozen. lockParamHooks() is owner-only and one-way: after it, every setParamHooks reverts ParamHooksLocked, so the three hook addresses are fixed forever — no carve-out, no ownerless exception, nobody. A project that froze an empty set can never acquire the transfer-hook power at all — the strongest thing this page can say about it — and a project that froze a set containing a hook cannot re-point that hook to something else. paramHooks() and paramHooksLocked() are the reads, abx state prints both, and the ParamHooksFrozen event is the from-chain proof. (Freezing the set does not disarm a hook already in it: a wired transfer hook keeps its veto.)

Supply unique per-mint content on a shared contract. Content is creator-controlled or renderer-derived, not arbitrary caller-supplied at mint. Field setters are owner-gated. ERC-1155 copies of one id share one URI. If each caller needs unique bytes, that is one contract per item (a 1/1). Holder-controlled state after mint is Parameters — a different model (schema + a second write), not a content-at-mint path.

A lock freezes the address, not the code behind it

This — and every other lock on this page — is a pointer lock. A hook is a contract, and a contract can be a proxy. A creator may lock a proxy as the transfer hook, renounce ownership, and later upgrade that proxy to revert, permanently freezing transfers while paramHooksLocked() still reads true. The same distinction applies to renderers and readers: a locked renderer address can sit in front of upgradeable code.

Detecting proxies on chain is complex and incomplete, so the protocol does not attempt it. Locks are defined as pointer locks and the limit is stated instead. A project making a permanence claim should deploy immutable hooks and renderers; checking what sits behind a supposedly settled project's addresses is verification tooling's job, because it needs knowledge the chain does not carry.

Reach another project through a shared singleton. The renderer, generator, chunk store, and seed source hold no mutable shared state: the renderer and chunk store have no storage at all, the generator's wiring is immutable with no setters, and the seed source is a stateless view namespaced by its caller. The minters key every sale by token address (and by id on an edition) and defer all authority to that token's own owner, and no funds ever rest in them. There is no admin key over any of it.

Take a token out of a collector's wallet. There is no owner-only transfer, no owner burn, no clawback, and no override of the ERC-721/1155 approval rules anywhere in the protocol. Burn, where a collection enabled it, is holder-or-approved only — the collection owner cannot burn a token they do not hold or aren't approved for, exactly like a transfer. Once a token is yours, the only paths out are yours to authorize.

Reading a specific project

abx state <address> reports owner, supply and cap, pause state, minter, primary payee, royalty, renderer, the transfer validator where one exists, every governed parameter's schema (type, auth, bounds, options, and its lock date or a retired marker), and the three param hooks plus whether they are frozen. abx verify <address> adds, on a code project, whether the script and the dependency set are frozen, and re-flags the param-hook lock.

Neither command prints the metadata locks today — the per-field lock and the URI-config lock. Those are one eth_call each: tokenFieldLocked(tokenId, field), contractFieldLocked(field), tokenURILocked(), contractURILocked(). Everything on this page is readable directly from the contract, so a collector who trusts no tooling can check all of it with eth_call.

Minting, Metadata, Parameters, Code projects, Royalty enforcement, Operate a project

On this page