Why the naive filter fails: floor price minus mint price ignores marketplace fee, creator royalty, and gas on both ends
The first version of my screen ranked drops with this expression:
headline_profit = floor_price - mint_price
That is not profit. It is the spread shown by two unrelated interfaces.
A mint transaction costs gas. Selling costs another transaction or settlement path. The marketplace takes a fee. The collection may enforce or signal a creator royalty. If the floor is stale, the executable sale price may be lower again.
I now calculate:
net = executable_sale_price
- mint_price
- marketplace_fee
- creator_royalty
- mint_gas_cost
- sale_gas_cost
“Executable” matters. I use the highest bid I can actually accept or a conservative sale estimate from current orders. I do not treat the lowest listing as guaranteed revenue.
My decision rule is simple: reject unless net remains positive after every known cost is included. Unknown fees are not assumed away; they make the candidate incomplete.
| Input | Source | Failure rule |
|---|---|---|
| Mint price | Contract call or transaction simulation | Reject if the page disagrees |
| Sale proceeds | Fillable marketplace order | Reject stale or invalid orders |
| Marketplace fee | Order consideration | Include before ranking |
| Royalty | Order consideration or collection policy | Include before ranking |
| Gas on entry | Mint simulation | Reject if estimation fails |
| Gas on exit | Sale path estimate | Include before ranking |
With that accounting, drops that looked profitable under floor_price - mint_price became negative. The screen was finding displayed spreads, not arbitrage.
The volume filter that removes drops nobody trades
A floor only proves that somebody listed an NFT. It does not prove that anybody buys it.
Before evaluating price, I query recent Transfer events and separate mint transfers from wallet-to-wallet transfers. A collection with no recent secondary transfers has no demonstrated exit. Its floor can stay visible while every holder waits for a buyer.
The filter is:
SELECT collection
FROM transfers
WHERE chain = 'Base'
AND block_time >= :recent_cutoff
AND from_address <> :zero_address
GROUP BY collection
HAVING COUNT(*) >= :minimum_secondary_transfers;
Both parameters are configuration, not universal constants. I set them from the holding period and turnover I am willing to accept. A faster strategy needs a stricter cutoff and a higher transfer requirement.
I run this filter before contract probing because event queries are cheaper than preparing and simulating mint calls. No recent secondary activity means rejection regardless of floor price. This removes collections where the apparent upside depends on finding the first buyer after I mint.
Probing a contract before trusting its mint page
The mint page is presentation. The contract is the executable state.
For every surviving candidate, I build the exact mint calldata and simulate it from the wallet that would submit it. I include the intended value and use the current Base state.
cast call "$NFT" "$MINT_SIGNATURE" $MINT_ARGUMENTS \
--from "$BUYER" \
--value "$MINT_VALUE" \
--rpc-url "$BASE_RPC"
cast sig-error "$ERROR_SIGNATURE"
If simulation succeeds, I keep the estimated gas and continue. If it reverts, I capture the revert data, take its selector, and match that selector against custom errors declared in verified source or recovered from the ABI.
The reason determines the result. A supply error, wallet-limit error, inactive-sale error, invalid-payment error, or proof error is a rejection until the required state or arguments are available. I do not override that decision because the page still shows an enabled mint button.
I apply the same fact-gate pattern in my local NFT pipeline: generated copy cannot contradict contract state. The collection is available at https://thunderous-sunflower-a910b4.netlify.app.
Practical takeaway: replace your floor-minus-mint filter today with ordered gates: recent secondary transfers, successful mint simulation, then net proceeds after fees, royalties, and gas.
Top comments (0)