abx.
Protocol

Minting

Minting logic lives in a separate minter contract, not in the token. The token exposes one mint primitive; a minter calls it and emits sale events. The minter spine is the vocabulary those sale contracts speak, so a frontend can follow a mint across many projects without the protocol taking a position on price. Like effects, it is a sibling convention, scoped to the minter contract.

What the token provides

The token core has two concerns: which address may mint, through the External Minter extension read with minter(), and where proceeds go, through the Primary Payee extension read with primaryPayee(). A minter calls the sequential mint primitive:

interface IAbxSequentialMint {
    function mint(address to) external returns (uint256 tokenId);
}

A mint appears on the token as an ERC-721 Transfer from 0x0. A minter targets the interface, verified through ERC-165, and reads owner(), primaryPayee(), minter(), maxInvocations(), totalSupply(), and paused() as needed. The one-shot 1/1 OneOfOneImage does not advertise the interface and is not a minter target.

Note what the signature does not take: an id. The primitive is sequential — it mints nextTokenId — so on this lane a buyer cannot pick which token they get. The edition lane is the opposite by design.

The reference minter

The reference minter is fixed-price. It is deployed once per chain, and is ownerless and multi-tenant: each project's sale is keyed by the token address, and all authority defers to that token's owner. A sale is configured before any purchase:

struct Sale {
    bool    configured;
    address paymentToken;   // address(0) is ETH, otherwise an ERC-20
    uint256 price;          // raw units per token
    uint256 allocation;     // max tokens this minter may sell for the project
    uint256 sold;
}

Running a sale takes two grants, both from the project owner: mint rights, with setMinter(minter) on the token, and sale terms, with configure(token, paymentToken, price, allocation) on the minter. A purchase is public:

function purchase(address token, address expectedPaymentToken, uint256 maxPrice)               external payable returns (uint256 tokenId);
function purchaseTo(address token, address to, address expectedPaymentToken, uint256 maxPrice) external payable returns (uint256 tokenId);

Each purchase mints exactly one token: it checks that the sale is configured and within allocation, checks the live terms against the buyer's, records the sale (sold += 1, effects before interactions), collects exactly price, mints, and forwards payment to the primary payee. It is non-reentrant, and for ETH the payment must be exact; there is no refund path.

Because the minter defers to the token, the token's pause is the sale's on-off switch, the payee is read fresh on each purchase, and the allocation is a budget separate from the supply cap.

The buyer states the terms they accept

expectedPaymentToken and maxPrice are required, and a sale that no longer matches them reverts SaleTermsChanged. configure takes effect immediately, so without a bound a project owner could front-run a pending purchase and move the terms under it. On the ETH lane that is largely contained, since msg.value must equal price. An ERC-20 sale is not: it pulls price from a standing allowance, so an unbounded price would spend everything the buyer approved, and switching the sale to a different ERC-20 would reach an allowance granted somewhere else. There is no "no maximum" sentinel: a caller who means "the terms as they stand" passes the terms it just read.

maxPrice is a terms assertion, not slippage tolerance — and on the ETH lane it buys no tolerance at all. The minter requires msg.value == price, an equality, and V1 has no refund path, so an in-flight ETH purchase reverts if the price moves in either direction, however wide the ceiling. Only the ERC-20 lane gets real tolerance: there the minter pulls the live price from the buyer's allowance, so any price at or below the ceiling settles — at the new price. Re-pricing a live ETH sale therefore fails every buy already in flight, by design; pause first if you want a clean cutover.

Sale events

SaleConfigured(address indexed token, address paymentToken, uint256 price, uint256 allocation)
Purchase(address indexed token, address indexed buyer, address indexed to, uint256 tokenId, address paymentToken, uint256 price)

SaleConfigured fires when an owner sets or updates terms. Purchase fires when a purchase settles. Both are indexed on token. The token still emits its protocol events alongside them: the Transfer from 0x0, and the one-time MinterSet when the minter is assigned.

Minter events are not an authenticity registry

Both shared minters are ownerless, multi-tenant public utilities, and they are deliberately neutral: they will configure a sale for any contract exposing the expected owner, payee, and mint-shaped calls, because refusing unknown contracts would make a shared minter a gatekeeper. A hostile contract can therefore configure itself here, accept payment, make its mint a no-op, and cause the canonical minter to emit SaleConfigured and Purchase with no NFT issued.

This does not reach a buyer already anchored to a trust anchor — they named the target. It is an integration boundary. A consumer reading these events must not treat a Purchase from the canonical minter address as proof that a canonical NFT was sold. Verify the token with its factory's isAbxClone, verify the token actually selected that minter, and reconcile the purchase against the token's own Transfer / TransferSingle. ERC-165 does not authenticate a contract that wants to lie.

The edition minter lane

An edition (the ERC-1155 twin of a 1/1, Series, or code project — copies of a work, not unique tokens) targets a different mint primitive, not a generalization of the sequential one:

interface IAbxEditionMint {
    function mint(address to, uint256 id, uint256 amount) external;
}

Same token-side auth as the sequential primitive (owner always; the assigned minter when not paused). There is no mintMany — every ABX token is Multicallable, so batching several ids or amounts is multicall's job, not the primitive's. OneOfOneEdition keeps the same uniform signature but reverts unless id == 0, since its id space is fixed to the one work.

The edition sibling of the reference minter is AbxFixedPriceMinter1155 — the same ownerless, multi-tenant, non-reentrant shape, keyed one step finer: (token, id) instead of just token, so an edition project prices each work on its own terms.

function configure(address token, uint256 id, address paymentToken, uint256 price, uint256 allocation) external;
function purchase(address token, uint256 id, uint256 qty, address expectedPaymentToken, uint256 maxTotalPrice)               external payable;
function purchaseTo(address token, uint256 id, uint256 qty, address to, address expectedPaymentToken, uint256 maxTotalPrice) external payable;

Same require → reserve → collect → mint → forward shape as the reference minter, generalized to a quantity: it checks the sale is configured and within allocation, checks the buyer's terms, records the sale (sold += qty, effects before interactions), collects exactly price * qty, mints that many copies, and forwards payment to the primary payee. Minting an ERC-1155 to a contract fires the standard onERC1155Received callback (there is no such callback on the 721 lane); it runs after the sale is recorded and payment collected, inside the non-reentrant guard. The token's pause is still the sale's on-off switch, and the payee is still read fresh on each purchase.

The terms guard bounds the total, maxTotalPrice, not the unit price. The total is what leaves the buyer's balance, and qty multiplies it, so a buyer who authorized two copies' worth cannot be sold three.

Who picks the work: the two lanes differ, deliberately

This is the part worth stating plainly, because the two lanes answer it in opposite ways.

On the 721 Series lane the buyer takes what is next. purchase(token, …) calls the sequential primitive, which mints nextTokenId. There is no id argument, so there is nothing to choose: on a code project the token's seed is drawn during that mint, and the buyer either completes the purchase or does not.

On the edition lane the buyer names the id, and therefore names the work. Sales are keyed (token, id), so purchase(token, id, qty, …) says which of the project's works to buy copies of. On an EditionCode project each id has its own seed — drawn at that id's first mint, because the seed belongs to the work rather than to any individual copy — so choosing the id chooses the seed that comes with it. That is the product, not a leak: an edition is copies of a known work, and a buyer who can see the work they are buying is the normal case, exactly as it would be on secondary.

Two consequences follow, and a project selling a multi-id edition should know both:

  • Buying an already-minted id is choosing among settled, publicly visible works. The seed is fixed and readable; the buyer is picking the one they like. Nothing about the outcome is contingent.
  • Buying an id that has never been minted draws its seed then and there, with the buyer's chosen id in the preimage. With the canonical AbxSeedSource that is a real selection surface, not just an accept-or-decline: a buyer can compute the seed each unminted id would get, in a view loop, and buy the one they prefer. The space is bounded by which ids the owner configured a sale for, and by maxInvocations. If a particular outcome is worth materially more than mint price, price it in — or use a custom seed source, or draw the ids yourself first (below). This is the same grinding surface the recipient to was removed from the preimage to avoid; id cannot be removed, because it is the work.

Mint the supply yourself, then sell on secondary

A first-class way to run an edition, and often the simplest: the owner mints the copies to their own address and never assigns a minter at all. No sale to configure, no allocation to budget, no pause to manage — and every id's seed is settled by the creator's own transactions, so nothing about the work is contingent on who buys first. From there the copies are listed wherever secondary happens, at whatever price and mechanism that venue offers.

The owner may always mint, paused or not, up to the id-space cap and each id's copy cap (supply caps are monotonic), so this path needs nothing the protocol does not already give. It also composes: mint a reserve to yourself, then run a fixed-price sale for the rest.

A OneOfOneEdition is a first-class target here — unlike a plain 1/1. OneOfOneImage sits out the reference minter by design (one-shot, owner-only, no sequential primitive); OneOfOneEdition doesn't: a priced open or limited edition of a single work is the dominant 1155 product, so it exposes IAbxEditionMint (id space fixed to 0) and needs no Series wrapper to get a native sale.

Sale events mirror the reference minter's, indexed on (token, id):

SaleConfigured(address indexed token, uint256 indexed id, address paymentToken, uint256 price, uint256 allocation)
Purchase(address indexed token, address indexed buyer, address indexed to, uint256 id, uint256 amount, address paymentToken, uint256 price)

Additive, same as the reference minter: strip these and an ABX-aware indexer still reconstructs holdings and per-id supply from TransferSingle/TransferBatch and MaxSupplyUpdated alone. The authenticity caveat above applies to this minter identically — it is just as neutral about what it will sell, so a consumer authenticates the token, never the minter's event.

Other sale mechanics

Only the fixed-price minter ships now. Other price mechanics, such as Dutch or English auctions, free mints, or allowlist-gated sales, are separate minter contracts that target the same IAbxSequentialMint and extend the spine with their own events. To run more than one at once, a router is assigned as the single minter() and dispatches to them.

On this page