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
idin the preimage. With the canonicalAbxSeedSourcethat 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 bymaxInvocations. 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 recipienttowas removed from the preimage to avoid;idcannot 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.