Gas and the ecosystem master wallet
Every NFT operation that signs with the Ecosystem master wallet, what each one costs, the two that spend the sale price and not just gas, and how to budget an auction-heavy marketplace.
This addon holds no key of its own. Every transaction it sends — contract deployments, transfers, auction settlements — is signed by the Ecosystem addon's master wallet for that chain, and paid for out of that wallet's native balance.
"The master wallet ran dry" is not one failure. It is a different failure per operation, and several of them are silent, so this page is the list of what spends, what it spends, and what you see when there is nothing left.
Which wallet, exactly
Each service (NFTMarketplaceService, NFTAuctionService,
NFTBlockchainService) starts the same way: find the ecosystem_master_wallet
row where chain matches and status is true, decrypt its data blob, take
privateKey, and build an ethers.Wallet signer on the chain's provider.
That has four consequences worth stating plainly:
- NFT Marketplace has a hard dependency on Ecosystem. No master wallet row for the chain, and every write path answers "Master wallet not found for the specified chain" (404) or "No active master wallet found for {chain}. Please set up master wallet first."
- The vault has to be unlocked. Key decryption is Ecosystem's; if the vault is locked, decryption fails with "Failed to decrypt master wallet data" (500). See Master wallets and the vault.
- Funding is per chain. ETH, BSC and POLYGON are three separate balances.
- The platform is the sender of record on chain for everything below. Block explorers will show your master wallet address, not the customer's.
You refresh and read the balances at Admin → Ecosystem → Wallets → Master
Wallets (/admin/ecosystem/wallet/master).
Everything that spends the master wallet
| Operation | Route or job | Gas limit | Also sends value? |
|---|---|---|---|
| Deploy the marketplace contract | POST /api/nft/marketplace/deploy |
4,000,000 | No |
| Deploy a collection contract | POST /api/nft/contract/deploy |
5,000,000 | No |
| Deploy an auction contract | POST /api/nft/auction/deploy |
3,000,000 | No |
| Settle an auction | POST /api/nft/auction/{id}/settle and the settleAuctions cron |
500,000 | No |
| Place a bid on an on-chain auction | POST /api/nft/bid |
300,000 | Yes — the whole bid |
| Extend an auction (anti-snipe) | POST /api/nft/bid |
100,000 | No |
| Buy through the marketplace contract | POST /api/nft/listing/{id}/buy |
400,000 | Yes — the whole sale price |
| Transfer the NFT on the fallback purchase path | POST /api/nft/listing/{id}/buy |
150,000 ERC-721 / 200,000 ERC-1155 | No |
| Transfer a token | POST /api/nft/token/{id}/transfer |
100,000 ERC-721 / 150,000 ERC-1155 | No |
| Transfer the NFT at auction settlement | POST /api/nft/auction/{id}/settle |
100,000 / 150,000 | No |
| Change the contract fee or fee recipient | PUT /api/nft/marketplace/config |
estimated | No |
| Withdraw accumulated contract fees | POST /api/nft/marketplace/withdraw |
estimated | No |
Two things are not on that list, and it is worth knowing why:
- Minting. There is no custodial signer for mints. A creator signs the mint
in their own browser wallet and
POST /api/nft/token/mint-web3verifies the receipt afterwards. The platform pays nothing. - Batch minting.
POST /api/nft/token/batch-mintwritesDRAFTrows in one database transaction and explicitly refusesmintToBlockchain, because each mint needs its own wallet signature. It costs no gas, and it puts nothing on chain.
A marketplace-contract purchase. When a fixed-price listing is genuinely
listed on your deployed marketplace contract, POST /api/nft/listing/{id}/buy
calls buyItem with value set to the full sale price — from the master
wallet. The contract then splits that payment to the seller, the royalty
recipient and the fee recipient. The buyer's own funds are not what moved on
chain.
A bid on an on-chain auction. If a listing has an auctionContractAddress,
POST /api/nft/bid calls bid() with value set to the bid amount, again from
the master wallet. The auction contract records your platform's address as the
bidder.
Before you deploy auction contracts or list items on the marketplace contract, work out who is meant to be funding those transfers on your install, and size the master wallet accordingly. A busy day on either path is a balance withdrawal, not a gas bill.
What an empty wallet looks like
There is no pre-flight balance check anywhere in this addon. The transaction is built, sent, and rejected by the node, and the RPC error is what surfaces:
| Where | What the operator sees |
|---|---|
| Marketplace deploy | 500, "Failed to deploy marketplace contract: <raw ethers error>" |
| Collection deploy | 500, "Contract deployment failed" |
| Auction deploy | 500, "Failed to deploy auction contract: <raw ethers error>" |
| Auction settlement, manual | 500, "Auction settlement on blockchain failed" |
| Auction settlement, cron | The listing claim is reverted to ACTIVE and the run logs "Blockchain settlement failed for auction …; will retry next run" |
Not one of those tells you the wallet is empty. The only function that would —
NFTBlockchainService.mintNFT, which turns the node's rejection into a 400
reading "Insufficient funds in wallet to mint NFT" — is called from nowhere in
the backend. It is left over from a custodial-mint era this build no longer has,
which is why minting is absent from the spend table above, and no operator will
ever see that message. Treat an empty balance as the first thing to check when a
deployment or a settlement fails, not something the error will name.
The cron case is the dangerous one, because it is quiet: settleAuctions will
retry every ten minutes indefinitely, so an auction with a winner simply never
pays out and nobody is told. Check the settleAuctions row on the cron console
(/admin/system/cron) when auctions stop completing.
Chain availability is derived from the nft_marketplace table — deploy a
contract on a chain and it appears in the collection-create form. Nothing checks
the master wallet balance after that.
So a chain with a deployed marketplace and an empty wallet still accepts
collections, listings, offers and bids. Every one of them fails at the moment
money or an asset has to move, which is the worst moment to find out. Fund every
chain you have deployed on, and take a chain out of service by deprecating its
nft_marketplace row rather than by letting it run out.
Budgeting
Two rules of thumb, both from the gas limits above.
A fixed-price marketplace is cheap after setup. The marketplace contract is one 4,000,000-gas deployment per chain, and each collection is one 5,000,000-gas deployment paid once. After that a sale costs a transfer.
Auctions are not. Every auction is its own contract:
POST /api/nft/auction/deploy deploys a fresh NFTAuction at a 3,000,000 gas
limit, per listing, and settlement costs another 500,000 plus a transfer. Ten
auctions a day on Ethereum is ten contract deployments a day. An auction-heavy
marketplace burns native currency at a rate a fixed-price one never approaches,
and on Ethereum mainnet that is the difference that decides whether the model
works at all.
There is no cheaper way to run them. An auction with no deployed contract
cannot be settled at all: nothing holds the winner's money, so settleAuctions
refuses to move the token, stamps nft_listing.settlementBlockedAt and leaves
the listing ACTIVE for a human. That is the Blocked auction settlements
queue on the moderation dashboard, and the only thing that clears it is
deploying a contract for the listing after the fact.
So the decision is binary: fund auctions properly, or turn them off with
nftEnableAuctions on Admin → NFT → System → Settings → Trading.
The gas estimation endpoints
These are quotes for the interface. They do not reserve, spend or check anything, and no admin screen consumes them — the creator-facing minting and listing flows do.
Neither requires authentication.
gas/estimate.get.ts and gas/estimate/index.get.ts both register GET /api/nft/gas/estimate, and they return different shapes:
- the first answers
{ success, data }withslow/standard/fasttiers, abaseFeeand agasLimitfor a transactiontype; - the second answers
chain,currentGasPrice,networkCongestionand astandardOperationstable — which is the shape the gas estimator component actually reads.
Which one wins is a route-registration detail, not a setting. If the gas panel in the creator flow renders blank, this is why: it received the other shape. You can tell them apart by their fields.
The POST is the one to reason about, because its default is not live data:
- It uses hardcoded fallback gas prices per chain (15 gwei ETH, 3 gwei BSC,
20 gwei POLYGON, and others) unless the environment variable
NFT_GAS_LIVE_ESTIMATIONis set totrue. - With that set, it tries the RPC provider with a 3-second health check and a 5-second estimation timeout, and falls back silently on either.
- The response says which happened:
dataSourceisliveorsmart_fallback,confidenceishighormedium, andnotes.liveEstimationreports whether the variable is on.
The USD figures are the weakest part. The POST converts through the real spot
price of the chain's native token and falls back to fixed prices (ETH 2400, BNB
320, MATIC 0.80) when that lookup fails. The standardOperations GET multiplies
by a flat 2000 regardless of chain, so its BSC and Polygon dollar figures are
meaningless. Quote the native amount to your users, not the dollar one.
The parts that are inert
Say these out loud before somebody builds a report on them.
gas_history is never written. The table exists (chain, gasPrice,
baseFee, priorityFee, timestamp, indexed on (chain, timestamp)), the
gas optimisation service has a storeGasPriceSnapshot method to fill it, and
nothing in the codebase calls that method. The historical-price analysis that
reads it therefore always sees an empty set, and the "gas is currently above
average" suggestion it would produce never fires. A full backup that includes
gasHistory includes nothing.
Optimisation suggestions degrade to nothing without Ecosystem. The gas
helper imports Ecosystem's getAdjustedGasPrice through a guarded require. If
that module is not present the import is swallowed and the helper falls back to
the provider's own fee data, and then to a fixed 20 gwei if even that fails.
Nothing is logged at the point of degradation. The optimisation service's
optimizationSuggestions array is only ever written to the debug log during a
mint, so it is not an operator-facing feature in this build.
Related
- Marketplace contracts — the screen that spends, and the traps in each of its tabs.
- Master wallets and the vault — how the key is encrypted and what a locked vault stops.
- Collections and contracts — the deployment a creator triggers and you pay for.
- Troubleshooting — when a deployment or a sale fails.