Settings
All seventeen NFT Marketplace settings keys with their defaults, the five tabs they live under, what actually reads each one, and the three that change nothing.
The addon's settings screen is Admin → NFT → System → Settings
(/admin/nft/settings). Opening it needs access.nft.settings; saving it
calls the platform-wide PUT /api/admin/system/settings, which needs
edit.settings. Those are two different keys and an admin can easily hold one
without the other — the screen loads, the values edit, and Save returns 403.
Every value lands in the platform settings table as a string, so the
backend parses each one on read and falls back when the parse fails. Saves take
effect immediately: the endpoint upserts only the keys whose value actually
changed and then clears the settings cache.
Earlier versions of this page listed nftMinBidIncrement as the auction bid
increment. No seeder, no migration and no admin screen has ever written it.
Two routes used to read it, always got nothing, and silently fell back to a
hardcoded 0.01, which is how an auction created with an increment of 10 could
accept a bid one cent above the standing one.
Both readers now use the per-listing nft_listing.minBidIncrement column. The
setting that governs that column is nftBidIncrementPercentage. If you have
a nftMinBidIncrement row in your settings table from a hand edit, it is
inert — deleting it changes nothing.
The five tabs
| Tab | What it groups | Keys |
|---|---|---|
| Trading | Which sale types exist, auction mechanics, the offer confirmation window | 9 |
| Fees | Platform fee, royalty ceiling, listing fee | 3 |
| Verification | KYC gates | 3 |
| Content | Metadata validation | 1 |
| Integrations | Cross-chain | 1 |
Trading
Which sale types exist
Turning one off does not cancel anything already in flight. Existing auctions still settle, outstanding offers can still be accepted and confirmed, and live fixed-price listings can still be bought. The switch only closes the door on new ones.
All three are checked with a string-aware helper, because a raw
settings.get("nftEnableAuctions") returns the string "false" when the
switch is off and "false" is truthy. If yours were saved before that fix and
appear to do nothing, save the tab once to rewrite them.
Auction window
Both are entered in seconds — the screen steps the minimum in 60s increments (floor 60) and the maximum in 3600s increments (floor 3600). The defaults are one hour and seven days.
POST /api/nft/listing computes endTime − startTime and refuses anything
outside the pair, quoting the bound back in hours or days. Changing either
value does not touch auctions already running.
Bid increment
This is a percentage, not an amount, and it is a validator rather than the
increment itself. When a seller creates an auction, the listing route computes
price × nftBidIncrementPercentage / 100 and rejects the listing if the
minBidIncrement the seller submitted is below it — the error names the figure
they need. The accepted value is then stored on the row as
nft_listing.minBidIncrement.
Everything downstream reads that column, never this setting:
POST /api/nft/bidrequires a new bid to clear the current highest ACTIVE bid by at least the column's value.GET /api/nft/auction/{id}publishesminimumNextBidfrom it.POST /api/nft/auction/deploypasses it into the auction contract, which then enforces it on chain for the rest of that auction's life.
The screen exposes it as a slider from 1 to 25 in whole percents, so you cannot set it to zero from the interface. Raising it does not change any auction already created — those rows keep the increment they were validated against.
Anti-snipe
One number does two jobs: a bid arriving with less than nftAntiSnipeExtension
seconds left pushes endTime out to now plus that same extension.
With it on, an auction's advertised end is a floor rather than a deadline. Say
so on your terms page — bidders who expect a hard close will complain otherwise.
The bid response carries auctionExtended: true so the interface can announce
it.
Offer settlement
Accepting an offer moves no money. The sale settles only when either party calls
POST /api/nft/offer/{id}/confirm with a verifiable on-chain transfer hash.
Until then the buyer's funds sit on hold in their own SPOT wallet and the token
is off the market.
The expireOffers cron — every five minutes — sweeps ACCEPTED offers older than
this many hours: it releases the hold, marks the offer EXPIRED, and puts the
token back on sale. A non-numeric or non-positive value falls back to 24.
The seller is usually the party holding the transaction hash, because they are
the one who executes transferFrom. Shorten this and you unwind real sales
whose on-chain transfer was merely slow. Lengthen it and a buyer's money stays
locked for longer when the transfer never happens.
If the release itself fails, the offer is flagged instead of unwound — see When money or an NFT is stuck.
Fees
This slider governs the custodial path: it sizes the buyer's escrow when an
offer is made (amount + fee), and it is the rate applied at offer settlement
and at auction settlement.
A fixed-price purchase reads it too, but does not always use it.
POST /api/nft/listing/{id}/buy computes the fee from this slider up front,
then tries the marketplace contract. If one is deployed for the collection's
chain and the token is genuinely listed on it, buyItem splits the payment
using the feePercentage baked into that contract at deployment, in basis
points, and those are the figures written to the nft_sale row. If any part of
that is untrue — no contract for the chain, the token was never listed on it, or
the call throws — the route falls back to a direct safeTransferFrom and the
sale row records this slider's figure instead.
Nothing reconciles the two. Deploy at 2.5%, move this slider to 5%, and the same item is recorded two different ways depending on which path the purchase took. The purchase response quotes this slider's figure either way, so it can disagree with the sale row it has just created.
On the fallback path the fee is a record, not a collection: the direct transfer moves the token only, and the buyer paid the seller with their own transaction, so nothing debits a wallet and nothing accrues to the platform.
PUT /api/nft/marketplace/config is the only thing that changes the contract,
and it costs gas. See Marketplace contracts.
Two more places read this key that are easy to miss:
POST /api/nft/auction/deployseeds each new auction contract with it. That contract then keeps that rate for its whole life, so auctions deployed before a change keep the old fee.POST /api/nft/listingquotes it back in the listing response asmarketplaceFee, alongside a clampedroyaltyFeeand anestimatedTotal.
The screen offers 0–10 in half-percent steps for the fee and 0–25 for the royalty ceiling. The backend accepts anything numeric and non-negative and only falls back (2.5 and 10) when the stored string cannot be parsed.
nftListingFee is a flat amount, not a percentage. The listing route reads it
and returns it to the seller — it debits no wallet, and nothing collects it
anywhere. The shipped NFTMarketplace contract has no listing fee at all: its
constructor takes only a fee recipient and a fee percentage, and listItem is
not payable. The separate listingFee that the marketplace deploy call accepts
is written to the nft_marketplace row and echoed back by
GET /api/nft/marketplace/info — it never reaches a contract either. Set this
to a non-zero value and you are quoting sellers a charge nobody takes; no
redeploy changes that.
Where the money lands, and the worked example, are in Fees and royalties.
Verification
The high-value pair
POST /api/nft/listing/{id}/buy converts the sale price to USD and, when
nftRequireKycForHighValue is on and nftHighValueThreshold is above zero,
requires the buyer to clear the buy_nft KYC feature before the purchase
completes. Below the threshold the purchase runs with no extra check.
Two traps:
- The threshold is USD, converted from the listing currency at request time. A conversion that cannot be resolved leaves the purchase ungated.
- The backend's fallback for the switch is off, while the screen's initial value is on. On an install where the key was never saved the gate is not running. Save the Verification tab once so the row exists.
nftRequireKycForCreators is weaker than it reads
The only thing that reads this key is GET /api/nft/onboarding/status, the
launch-readiness endpoint behind /admin/nft/onboarding. It marks configure
verification complete once the key exists at all — whether it is on or off.
Collection creation is gated by the shared create_nft KYC feature, not by
this switch. If you need creators verified, set that feature's level in the core
KYC configuration; flipping this switch alone changes nothing about who may
create a collection.
The six NFT actions the KYC system can gate are view_nft, create_nft,
buy_nft, sell_nft, transfer_nft and deploy_nft_contract.
Content
One key, two very different enforcements:
| Route | What it checks when the switch is on |
|---|---|
POST /api/nft/token/mint-web3 |
The NFT has a non-empty name of at most 200 characters. Nothing else. |
POST /api/nft/listing |
Fetches the token's metadataUri and validates the real JSON |
The listing-time check is the strict one. It runs only when the token already
has both a metadataUri and an image, and it requires both to match one
of seven accepted IPFS URL shapes:
ipfs://<hash>https://ipfs.io/ipfs/<hash>https://gateway.ipfs.io/ipfs/<hash>https://<hash>.ipfs.<host>/— the dweb.link stylehttps://gateway.pinata.cloud/ipfs/<hash>https://<hash>.ipfs.nftstorage.linkhttps://<hash>.ipfs.w3s.link
It then fetches the metadata JSON over a gateway with a 10-second timeout and
requires a name, an image, and for that image to itself be a valid IPFS
URL. Anything else — a self-hosted image, an Arweave URL, a private gateway, a
slow pin — fails the listing with "NFT metadata validation failed".
When the key is absent, the mint route treats it as on and the listing route treats it as off. On a fresh install that has never saved this tab you get name validation at mint and no IPFS validation at listing. Save the Content tab once to make both paths agree with what you see on screen.
If your creators host media anywhere other than those seven gateway shapes, leave this off — otherwise nothing they mint can ever be listed.
Integrations
This switch is written by the settings screen and read by no backend route, no cron and no frontend component. There is no bridge in this build. Leave it alone; it changes nothing either way.
Chain availability is not controlled here — a chain becomes selectable only once
a marketplace contract is deployed on it, which is the nft_marketplace table.
See Marketplace contracts.
Which key is read where
| Key | Read by |
|---|---|
nftEnableFixedPriceSales |
POST /api/nft/listing (FIXED_PRICE), readiness checklist |
nftEnableAuctions |
POST /api/nft/listing (AUCTION), readiness checklist |
nftEnableOffers |
POST /api/nft/offer, readiness checklist |
nftMinAuctionDuration |
POST /api/nft/listing (AUCTION) |
nftMaxAuctionDuration |
POST /api/nft/listing (AUCTION) |
nftBidIncrementPercentage |
POST /api/nft/listing (AUCTION) — validates minBidIncrement |
nftEnableAntiSnipe |
POST /api/nft/bid |
nftAntiSnipeExtension |
POST /api/nft/bid |
nftTransferConfirmGraceHours |
The expireOffers cron's stale-accepted-offer sweep |
nftMarketplaceFeePercentage |
Offer escrow sizing, offer settlement, auction settlement, auction contract deployment, listing quote, POST /api/nft/listing/{id}/buy — its quoted fee always, and the fee written to nft_sale whenever the marketplace-contract path is not taken |
nftMaxRoyaltyPercentage |
Every path that quotes or pays a royalty |
nftListingFee |
POST /api/nft/listing — response only, no debit |
nftRequireKycForCreators |
Readiness checklist only |
nftRequireKycForHighValue |
POST /api/nft/listing/{id}/buy |
nftHighValueThreshold |
POST /api/nft/listing/{id}/buy |
nftRequireMetadataValidation |
POST /api/nft/token/mint-web3, POST /api/nft/listing |
nftEnableCrossChain |
Nothing |
Keys that are not on this screen
Three more NFT settings rows exist that this screen never shows and never
writes. All of them are per-chain, and all of them are lower-cased snake case
with the chain appended — nft_marketplace_address_eth, ..._bsc and so on:
| Key | Written by | Read by |
|---|---|---|
nft_marketplace_address_<chain> |
Nothing | Pause, unpause, config, whitelist, withdraw, readiness checklist |
nft_marketplace_paused_<chain> |
Pause and unpause | Pause and unpause |
nft_marketplace_pause_reason_<chain> |
Pause and unpause | Unpause, which reports it back |
The first one is the reason the emergency controls answer "Marketplace contract not found" on a fresh install. The recovery is in When money or an NFT is stuck.
Related
- Fees and royalties — where each percentage is read, and the two places revenue accumulates.
- Listings, sales, auctions and offers — the mechanics these settings govern.
- Marketplace contracts — the on-chain half of the fee.
- Permissions —
access.nft.settingsandedit.settings.