Listings, sales, auctions and offers

The four ways an NFT changes hands on this platform — fixed-price listings, bundles, on-chain timed auctions with anti-snipe, and escrowed offers with a two-step settlement — and what each one does with the money.

8 min readUpdated 3 August 2026listings, auctions, offers, escrow, bids

There are three listing types and one separate offer mechanism, and they do not share a money path. Read Fees and royalties alongside this page — this one covers mechanics, that one covers where the money lands.

Mechanism Row Payment Settled by
Fixed price nft_listing type FIXED_PRICE On chain, buyer-signed The buy endpoint, synchronously
Bundle nft_listing type BUNDLE On chain, buyer-signed The buy endpoint, synchronously
Auction nft_listing type AUCTION + an NFTAuction contract On chain, held by the auction contract The settleAuctions cron
Offer nft_offer Custodial — the buyer's SPOT wallet The confirm endpoint, or the stale sweep

Creating a listing

Creates a listing for a token you own
Creates several listings in one call
Cancels a listing

Gated by the sell_nft KYC feature. The listing route runs a long gauntlet before it writes anything:

  1. The type must be enabled. FIXED_PRICE checks nftEnableFixedPriceSales; AUCTION checks nftEnableAuctions. A disabled type returns 403.

  2. The currency must be supported. The accepted set is hardcoded: ETH, BNB, MATIC, USDT, USDC, BUSD. Anything else is rejected with the list in the message.

  3. Price sanity. Greater than zero, at most 1,000,000 units, at most six decimal places.

  4. You must own the token and it must be MINTED. A DRAFT token cannot be listed.

  5. Metadata validation, if nftRequireMetadataValidation is on and the token has both a metadata URI and an image. The platform fetches the IPFS document and checks it is valid NFT metadata; unreachable gateways fail the listing.

  6. Marketplace approval. The on-chain approval for the marketplace contract is checked, with a three-second timeout per RPC call.

  7. No existing ACTIVE listing for the same token — a second one returns 409.

The NFT is not escrowed. It stays in the owner's wallet and the marketplace contract transfers it directly to the buyer at purchase time. That is why approval matters and why revoking approval breaks a live listing.

Auction-specific rules

An AUCTION listing additionally requires:

  • An endTime in the future, after startTime.
  • A duration between nftMinAuctionDuration and nftMaxAuctionDuration (defaults: 1 hour and 7 days).
  • A minBidIncrement of at least nftBidIncrementPercentage of the starting price. The error names the computed figure, so a seller submitting the default 0.01 on a 10 ETH auction with a 5% increment is told to use 0.5.
  • A deployed collection contract, and a token with a blockchainTokenId.

Those last two are refusals, not warnings. Without them the auction could never be settled, because auction escrow lives in the auction contract.

Bundle rules

A BUNDLE listing takes bundleTokenIds. You must own every token in it, every token must be approved for the marketplace, and none of them may already be listed. All of them flip to isListed and all of them transfer together.

What the response tells the seller

The listing response includes marketplaceFee, royaltyFee, listingFee and an estimatedTotal computed as price × (1 − fee% − royalty%). The royalty in that figure is already clamped to nftMaxRoyaltyPercentage, so the number the seller is shown is the number settlement will use.

Buying at a fixed price

Records a buyer-signed purchase and moves ownership

The buyer signs and broadcasts the payment first; the request body carries the transactionHash. The endpoint then:

  1. Requires a linked wallet address on the buyer's user row. This is the most common hard failure on a fresh install — see Troubleshooting.

  2. Refuses self-purchase (403) and expired or non-ACTIVE listings (409).

  3. Applies the high-value KYC gate if nftRequireKycForHighValue is on: the sale price is converted to USD and compared against nftHighValueThreshold.

  4. Checks the buyer's on-chain balance for the sale price plus gas. A technical failure here logs a warning and continues; a genuine shortfall returns 400 with the exact figures.

  5. Verifies the transaction on chain — sender, amount and recipient must match.

  6. Claims the listing atomically. A single UPDATE … WHERE status = 'ACTIVE' flips it to SOLD. Concurrent buyers and replayed requests see zero affected rows and are rejected, so two people cannot both buy the same token.

  7. Executes the transfer. If the item is listed on the marketplace contract, buyItem runs and the contract distributes the payment. Otherwise the platform falls back to a direct wallet-to-wallet transfer.

  8. Writes the sale. A nft_sale row with the fee split, ownership moved on the token, a SALE activity row, and a NFT_PURCHASE ledger entry against the buyer's SPOT wallet as a record only — no balance is debited.

If the on-chain transfer fails at step 7, the claim is released and the listing returns to ACTIVE. If the release itself fails, the listing is stranded in SOLD with no sale recorded — that is one of the things the moderation dashboard exists to surface.

Auctions

An auction is a listing plus a deployed contract plus a settlement job.

Deploying the auction contract

Deploys an NFTAuction contract for a listing

One contract per auction, deployed from the master wallet, holding the bids until endAuction. Deploying also clears any manual-review flag on the listing, which is how a stuck auction is recovered.

Bidding

Places a bid — the canonical route
Places a bid on a specific auction

The second is a thin alias over the first: it looks up the listing's currency and delegates. Both therefore enforce the same rules, which was not always true — the alias used to read a setting key nothing writes and accepted bids 0.01 above the standing bid on auctions with an increment of 10.

A bid must clear the current highest ACTIVE bid by at least the listing's minBidIncrement, must match the listing currency, and cannot come from the seller. On an auction with a deployed contract the bid is executed on chain and the bidder must have a linked wallet address.

Anti-snipe

When nftEnableAntiSnipe is on and a bid lands with less than nftAntiSnipeExtension seconds remaining, the auction's endTime is pushed out to now plus that extension. Default is 300 seconds. The bid response reports auctionExtended: true so the UI can say so.

Settlement

settleAuctions runs every 10 minutes over ACTIVE auctions whose endTime has passed. With a winning bid and a deployed auction contract, it calls endAuction, transfers the NFT, records the sale and pays out. With no bids it simply expires the listing.

Settles an ended auction manually

If an auction ends with a winning bid but has no auctionContractAddress, no funds are escrowed anywhere. Moving the NFT would hand the winner the token without the seller ever being paid, so the cron refuses: it stamps settlementBlockedAt and leaves the listing ACTIVE for manual review.

Those rows appear as Blocked auction settlements on the moderation dashboard. The flag also stops the cron re-selecting the same unsettleable rows every ten minutes, which used to starve auctions that could settle.

Offers — the custodial path

Offers are the only part of this product that moves platform wallet balances.

Makes an escrowed offer on a token or a collection
Cancels your own offer
Rejects an offer

An offer can target a single token or an entire collection. Making one holds the offer amount plus the marketplace fee from the buyer's SPOT wallet under a stable idempotency key of nft_offer_hold_<offerId>. That hold row is the authority on what was reserved — settlement reads it back rather than recomputing, so an admin changing the fee percentage midway cannot over- or under-release.

You cannot offer on your own token or your own collection, and you cannot have two ACTIVE offers on the same target.

Accept, then confirm

Acceptance is deliberately split in two.

Accepts an offer — step one, no money moves
Confirms the on-chain transfer — step two, everything settles

Accept flips the offer to ACCEPTED, rejects competing offers, cancels any active listing on the token, and records who accepted. Nothing is paid.

Confirm is where the sale happens, atomically, inside one database transaction: the buyer's escrow is consumed, the seller is credited the amount less the creator royalty, the creator is credited the royalty, the platform fee is collected, ownership flips, and a TRANSFER activity row is written.

That ordering matters. When the payout ran at acceptance, the seller held both the cash and the token until the buyer confirmed — and if the transfer never happened, three credits had to be clawed back out of wallets that may already have been spent from. Settling at confirmation degrades the worst case from "reverse three credits" to "release one hold".

The stale-offer sweep

An accepted offer whose transfer is never confirmed is unwound after nftTransferConfirmGraceHours (default 24). The sweep releases the hold and returns the token to the market.

If the release cannot complete — which requires the hold bookkeeping itself to be corrupt — the offer is stamped flaggedAt, a nft_dispute row is created with type NOT_RECEIVED and priority HIGH, and admins holding access.nft.dispute are notified. Those rows are the Flagged escrow queue on the moderation dashboard, and they mean somebody's money is locked while they hold no NFT.

Expiry

expireOffers runs every 5 minutes over ACTIVE offers past expiresAt, releases the escrow and marks them EXPIRED. Expiry only ever looks at ACTIVE offers — ACCEPTED ones are the stale sweep's job.

Live updates

A WebSocket endpoint at /api/nft/market pushes auction updates, token updates, collection updates, activity and bid changes to subscribers. It is only queried when clients are actually connected.

Reading the market

All active listings
Auctions
Bids on an auction
Offers
Marketplace-wide statistics
Trending collections
Top creators
Landing page payload