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.
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
Gated by the sell_nft KYC feature. The listing route runs a long gauntlet
before it writes anything:
-
The type must be enabled.
FIXED_PRICEchecksnftEnableFixedPriceSales;AUCTIONchecksnftEnableAuctions. A disabled type returns 403. -
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. -
Price sanity. Greater than zero, at most 1,000,000 units, at most six decimal places.
-
You must own the token and it must be
MINTED. A DRAFT token cannot be listed. -
Metadata validation, if
nftRequireMetadataValidationis 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. -
Marketplace approval. The on-chain approval for the marketplace contract is checked, with a three-second timeout per RPC call.
-
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
endTimein the future, afterstartTime. - A duration between
nftMinAuctionDurationandnftMaxAuctionDuration(defaults: 1 hour and 7 days). - A
minBidIncrementof at leastnftBidIncrementPercentageof 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
The buyer signs and broadcasts the payment first; the request body carries the
transactionHash. The endpoint then:
-
Requires a linked wallet address on the buyer's user row. This is the most common hard failure on a fresh install — see Troubleshooting.
-
Refuses self-purchase (403) and expired or non-ACTIVE listings (409).
-
Applies the high-value KYC gate if
nftRequireKycForHighValueis on: the sale price is converted to USD and compared againstnftHighValueThreshold. -
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.
-
Verifies the transaction on chain — sender, amount and recipient must match.
-
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. -
Executes the transfer. If the item is listed on the marketplace contract,
buyItemruns and the contract distributes the payment. Otherwise the platform falls back to a direct wallet-to-wallet transfer. -
Writes the sale. A
nft_salerow with the fee split, ownership moved on the token, aSALEactivity row, and aNFT_PURCHASEledger 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
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
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.
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.
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.
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.