Fees and royalties

Where every percentage in the NFT marketplace is read, where it is clamped, and the two entirely separate places your revenue accumulates — the on-chain marketplace contract and the Super Admin wallet.

6 min readUpdated 3 August 2026fees, royalties, revenue, escrow, adminprofit

Three numbers decide who gets paid: the marketplace fee, the creator royalty, and the listing fee. Each one is read in more than one place, and two of them exist in two copies that do not synchronise. Getting this wrong does not throw an error — it quietly pays the wrong people.

The three numbers

Platform fee percentage, applied on custodial settlements
The ceiling every royalty in the product is clamped to
Flat fee for creating a listing

The settings screen exposes the fee as a 0–10 slider in half-percent steps and the royalty ceiling as a 0–25 slider. The backend accepts anything numeric and non-negative; it falls back to 2.5 and 10 only when the stored value cannot be parsed.

The fee exists in two copies

This is the single most important thing on this page.

Copy Set by Applies to
The contract's feePercentage The marketplace deploy call, in basis points Every on-chain sale — fixed price and auction
nftMarketplaceFeePercentage The admin settings screen Every custodial settlement — accepted offers

They are written at different times, by different people, into different systems, and nothing reconciles them. Deploy at 2.5% and later move the slider to 5% and you have a marketplace that charges 2.5% on a direct purchase and 5% on an accepted offer for the identical item.

PUT /api/nft/marketplace/config is the only thing that changes the deployed contract's fee. It sends a transaction, so it costs gas, and it needs the chain and the contract address. Changing the settings slider alone leaves the contract untouched.

Change both, and record which value you deployed at — the contract is the authority for on-chain sales and there is no screen that compares the two.

Where the money actually lands

On-chain sales

A fixed-price purchase or an auction settlement pays out inside the contract. The buyer's transaction sends the full price; the contract keeps its fee, pays the royalty recipient, and forwards the remainder to the seller. No Bicrypto wallet balance changes.

Your fee therefore accumulates as a native-token balance held by the marketplace contract. It is not income until you withdraw it:

Withdraws accumulated on-chain marketplace fees
Reads the contract's current balance

The withdrawal requires chain, contractAddress and a reason (recorded, up to 500 characters). Amount and destination are optional — omit the amount to withdraw everything, omit the address to send to the configured fee recipient.

There is no alert, no dashboard tile and no scheduled sweep. The balance shows on the Marketplace admin screen only when you open it. Put a calendar reminder against it.

Custodial offer settlements

An accepted offer settles entirely inside your platform, in the offer's currency, against SPOT wallets. Three legs run in one database transaction:

  1. The buyer's escrow is consumed. executeFromHold takes exactly what the hold row says was reserved — the offer amount plus the fee that applied when the offer was made.

  2. The seller is credited the offer amount less the creator royalty. Note that the seller does not pay the marketplace fee out of their proceeds; the buyer paid it on top.

  3. The creator is credited the royalty, resolved through nft_collection.creatorIdnft_creator.userId to reach a real account.

The fee then goes to the platform through collectPlatformFee, which credits the Super Admin's SPOT wallet in that currency and writes an adminProfit row of type NFT_SALE. It never throws — a failure is logged and the sale still completes.

collectPlatformFee resolves the oldest user holding the "Super Admin" role. If that role or that user does not exist, it logs a critical error and returns — the sale succeeds and the fee vanishes. Check that the role exists and has exactly one obvious owner before you take real volume.

Every leg carries a stable idempotency key derived from the offer id (nft_offer_execute_<id>, nft_offer_credit_seller_<id>, nft_offer_royalty_<id>), so a retried confirmation cannot double-pay.

Why the hold is read, not recomputed

The escrow was sized when the offer was made. Settlement reads that figure back from the hold row rather than recalculating it from the current fee, because recomputing diverges the moment an admin moves the slider between the offer and its acceptance. Over-releasing drains other operations' held funds; under-releasing strands the residue in the buyer's wallet forever.

Creator royalties

A royalty percentage lives on the collection (nft_collection.royaltyPercentage, default 2.5) and optionally on individual tokens. Settlement uses the collection's value, clamped to nftMaxRoyaltyPercentage:

effectiveRoyalty = min(collection.royaltyPercentage, nftMaxRoyaltyPercentage)

That clamp is applied in every path that quotes or pays a royalty — collection creation, minting, batch preparation, listing creation, direct purchase, offer acceptance, manual auction settlement and the settlement cron — so lowering the ceiling immediately reduces payouts on existing collections. It does not rewrite the stored percentage; it just caps what is paid.

A creator who launched at 10% and later finds the ceiling at 5% starts receiving half as much, with no notification and no change visible on their collection. If you intend to lower it, tell your creators first.

On-chain royalties are set once, at deployment

The collection contract is deployed with the royalty in basis points and a royalty recipient address baked in. Those cannot be changed afterwards, and they are what an external marketplace would honour. The platform's clamp only governs what this marketplace pays.

If the creator had no linked wallet address when the contract was deployed, the recipient is your master wallet — permanently. See Collections.

The royalty ledger is dormant

nft_royalty has a full PENDING / PAID / FAILED lifecycle and nothing writes a row to it. The single insert that existed was removed because it was being handed an offer id where a sale id belonged and an nft_creator id where a user id belonged; both foreign keys failed and the error was swallowed.

Royalties are paid in cash at settlement, so there is no debt to track. Do not build reporting on that table — a "royalties owed" figure read from it can only ever be zero.

The listing fee

nftListingFee is a flat amount, not a percentage, and it defaults to 0. The listing route reads it and returns it in the response so the seller sees it, but the platform does not debit any wallet for it — a non-zero listing fee is charged by the marketplace contract, which takes its own listingFee parameter at deployment.

If you set the settings value without redeploying or reconfiguring the contract, sellers are shown a fee that is never collected.

A worked example

A 100 USDT offer on a collection with a 7.5% royalty, on a platform with the fee at 2.5% and the ceiling at 5%:

Step Amount
Buyer's escrow when the offer is made 102.50 USDT
Royalty percentage after the clamp 5% — not 7.5%
Creator receives 5.00 USDT
Seller receives 95.00 USDT
Platform receives (Super Admin wallet, adminProfit type NFT_SALE) 2.50 USDT

The same item bought at a fixed price for 100 USDT would instead pay the contract's deployed fee, pay the royalty to the address baked into the collection contract, and never touch a Bicrypto wallet at all.

Where to see the numbers

  • Admin → NFT → Trading → Sales (/admin/nft/sale) lists every nft_sale row with marketplaceFee, royaltyFee, totalFee and netAmount.
  • Admin → NFT → System → Analytics (/admin/nft/analytics) aggregates volume and revenue.
  • The core revenue reporting reads adminProfit, so custodial NFT fees show up alongside every other platform revenue stream as type NFT_SALE. On-chain fees do not appear there — they are in the contract until you withdraw them.

The analytics endpoint fabricates its fourteen-point sparkline trends from the current total multiplied by a random factor, and falls back to a hardcoded Art / Gaming / Music / Sports category split when the real group-by is empty. The totals are real; those two decorations are not. The moderation dashboard at /admin/nft deliberately does not use them.