User journeysFill a listing

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 LIQUID or DUTCH listing (Listings.getListingType returns one of those).
  • Caller holds enough CollectionToken for the floor burn plus the premium (floorMultiple - 1) that flows to listing owners. Reading Listings.getListingPrice(_collection, _tokenId) in a multicall is the canonical preflight.
  • Caller has approved the Listings contract on the CollectionToken for 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_);
FieldNotes
collectionAll filled listings must belong to this collection
tokenIdsOutOne inner array per listing owner, in any order. Mismatched grouping reverts.
recipientReceives the NFTs; usually msg.sender
maxSpendSandwich/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

#CallerCall
1Buyer<CollectionToken>.approve(listings, expectedSpend)
2BuyerListings.fillListings(params)

Events: Listings.ListingsFilled(collection, tokenIdsOut, recipient). Per-owner internals also emit ListingFeeCaptured for protocol-fee attribution.

Code

fillListings.ts
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

ErrorCause
ListingNotAvailableListing was cancelled, expired beyond the dutch tail, or already filled
InvalidCollectionA 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 Listing row updates (back to format: NONE and null owner/duration/floorMultiple if the NFT stays in the Locker as a floor item, otherwise removed).
  • A ListingActivity { activityType: FILLED, ethPrice, to } row is written.
  • Collection.publicListings decrements.
  • Listing.taxCaptured increases for the listing’s lifetime captured tax (mirrors the cumulative ListingFeeCaptured events).
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
  }
}