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.

7 min readUpdated 6 August 2026gas, master-wallet, ecosystem, deployment, auctions

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).

Reads every master wallet balance from chain and writes it back to the row

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-web3 verifies the receipt afterwards. The platform pays nothing.
  • Batch minting. POST /api/nft/token/batch-mint writes DRAFT rows in one database transaction and explicitly refuses mintToBlockchain, 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.

Current gas price and a cost table for the standard operations
A cost estimate for one named operation

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 } with slow/standard/fast tiers, a baseFee and a gasLimit for a transaction type;
  • the second answers chain, currentGasPrice, networkCongestion and a standardOperations table — 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_ESTIMATION is set to true.
  • 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: dataSource is live or smart_fallback, confidence is high or medium, and notes.liveEstimation reports 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.