Fill a listing
Buys one or more existing listings, paying their owners and burning the floor portion from the caller. All listings to be filled must belong to the same collection in a single call; the array of arrays groups them by listing owner so the fill loop can settle each owner in one balance update.
Prerequisites
- Each tokenId is currently a
LIQUIDorDUTCHlisting (Listings.getListingTypereturns one of those). - Caller holds enough
CollectionTokenfor the floor burn plus the premium (floorMultiple - 1) that flows to listing owners. ReadingListings.getListingPrice(_collection, _tokenId)in a multicall is the canonical preflight. - Caller has approved the
Listingscontract on theCollectionTokenfor at least the realised total (or uses Permit2 /NFTXZap). - Listings is not paused.
In practice the FE almost always uses
NFTXZap.buyNFTWithETH so the user never sees
the intermediate CollectionToken step. The journey below documents the
raw Listings.fillListings for completeness; the Zap path is described
on the contract page.
Inputs
struct FillListingsParams {
address collection;
uint[][] tokenIdsOut; // grouped by owner
address recipient;
uint maxSpend; // hard cap on the buyer-debited total; 0 to opt out
}
function fillListings(FillListingsParams calldata params) external returns (uint totalBurn_);| Field | Notes |
|---|---|
collection | All filled listings must belong to this collection |
tokenIdsOut | One inner array per listing owner, in any order. Mismatched grouping reverts. |
recipient | Receives the NFTs; usually msg.sender |
maxSpend | Sandwich/front-run guard. Counts buyer-debited amounts (totalPrice premium + floor burn); excludes protocol fee funded by sellers’ tax. Pass 0 for back-compat (no cap). |
totalBurn_ is the floor portion burned from the caller’s CollectionToken
balance and is the headline number for “how much CT did I spend”.
Contract calls
| # | Caller | Call |
|---|---|---|
| 1 | Buyer | <CollectionToken>.approve(listings, expectedSpend) |
| 2 | Buyer | Listings.fillListings(params) |
Events: Listings.ListingsFilled(collection, tokenIdsOut, recipient).
Per-owner internals also emit ListingFeeCaptured for protocol-fee
attribution.
Code
import { listingsAbi, collectionTokenAbi } from "@nftx/contracts";
import { NFTX_CONTRACTS } from "@nftx/config";
import { parseUnits } from "viem";
const collection = "0xCollection";
const listings = NFTX_CONTRACTS[base.id].listings;
const ct = await publicClient.readContract({
address: NFTX_CONTRACTS[base.id].locker,
abi: lockerAbi,
functionName: "collectionToken",
args: [collection],
});
// 1) Group tokenIds by listing owner.
const groups = [
[11n, 12n], // owned by Alice
[42n], // owned by Bob
];
// 2) Preflight maxSpend off-chain (sum of getListingPrice + buffer).
const maxSpend = parseUnits("3.6", 18); // tighter than expectedSpend
// 3) Approve a slightly larger envelope to avoid an exact race.
await walletClient.writeContract({
address: ct,
abi: collectionTokenAbi,
functionName: "approve",
args: [listings, parseUnits("4", 18)],
});
// 4) Fill.
await walletClient.writeContract({
address: listings,
abi: listingsAbi,
functionName: "fillListings",
args: [
{
collection,
tokenIdsOut: groups,
recipient: account,
maxSpend,
},
],
});Failure modes
| Error | Cause |
|---|---|
ListingNotAvailable | Listing was cancelled, expired beyond the dutch tail, or already filled |
InvalidCollection | A grouped tokenId doesn’t belong to params.collection |
FillCostExceedsMaxSpend(totalCost, maxSpend) | Realised cost exceeded maxSpend (typical sandwich guard trip) |
Paused() | Listings paused |
A missing/insufficient CT allowance surfaces as a generic ERC-20 revert.
Handle it the same as on redeem.
Subgraph follow-through
For every filled tokenId:
- The
Listingrow updates (back toformat: NONEand nullowner/duration/floorMultipleif the NFT stays in the Locker as a floor item, otherwise removed). - A
ListingActivity { activityType: FILLED, ethPrice, to }row is written. Collection.publicListingsdecrements.Listing.taxCapturedincreases for the listing’s lifetime captured tax (mirrors the cumulativeListingFeeCapturedevents).
query RecentFills($chainId: Int!, $collection: String!) {
ListingActivity(
where: {
chainId: { _eq: $chainId }
collection: { collection: { _eq: $collection } }
activityType: { _eq: FILLED }
}
order_by: { created: desc }
limit: 50
) {
tokenId
ethPrice
to
txHash
created
}
}