A command-line tool for sniping public NFT mints on OpenSea's SeaDrop, across Ethereum, Base, Ink and Robinhood Chain mainnets, or Sepolia and Amoy testnets for rehearsing a mint before it counts.
It builds the mint transaction from on-chain data only - price, fee recipient and per-wallet limit all come straight from the SeaDrop contract. That means:
- No OpenSea account, login, or access token.
- No API rate limits to lose a mint to.
- Faster. Every transaction is signed and serialised before the stage opens, so at the exact start time the only work left is writing bytes to the network.
Multi-wallet: paste as many keys as you like, or have the tool generate and save a set for you — hundreds of wallets fire in parallel with the same timing precision as one.
- Node.js 18 or newer — check with
node --version. Get it from nodejs.org if you don't have it. - A wallet with some ETH on the chain you're minting on.
Run these one line at a time, pressing Enter after each:
git clone https://github.com/morsyxbt/nft-public-mint.git
cd nft-public-mint
npm install
npm run buildBefore running npm install, check your prompt ends in nft-public-mint — if the
cd didn't take, everything after it fails.
Then confirm the build worked:
npm start -- --helpInstall errors
Set-Location : A positional parameter cannot be found (Windows PowerShell)
Two commands got pasted onto one line — you'll see something like
cd nft-public-mintnpm install. Run each line separately.
Could not read package.json / Missing script: build
You're in the wrong folder. Run cd nft-public-mint first.
npm or git is not recognised
Install Node.js 18+ and Git,
then reopen your terminal.
cp .env.example .envOpen .env and add a private RPC URL for the chain you'll mint on, e.g.:
RPC_URL_BASE=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
A free Alchemy key takes two minutes and is the single
biggest factor in whether you win a contested mint. You can also paste it at the
prompt instead — the tool works without any .env at all, falling back to public
nodes.
Never put private keys in
.env. You paste them into the CLI at run time. They're held in memory for that run only and never written to disk.
npm startThe wizard asks you eight things:
| Step | What it wants |
|---|---|
| 1. Wallets | Paste existing private keys, generate fresh wallets, or load a saved set. See Wallets at scale below. |
| 2. Network + chain | Mainnet or Testnet first, then only that network's chains — Ethereum/Base/Ink/Robinhood on mainnet, Sepolia/Amoy on testnet. Rehearse a mint on testnet before it counts for real. |
| 3. Quantity | How many NFTs per wallet. |
| 4. NFT link | An OpenSea collection link, an item link, a slug, or the raw 0x contract address. A contract address or item link always works with no API key; a collection slug needs a lookup that usually works unauthenticated. |
| 5. RPC | Paste a full URL, or just your Alchemy key and it expands automatically. Blank uses .env, or public nodes. |
| 6. Gas | Ceiling and tip. The live base fee is shown right above the prompt. |
| 7. Timing | Wait for the stage to open, or fire now if it's already live. |
Then it shows a summary and asks Fire?. Nothing is sent until you type y.
If the drop opens later, choose "Wait for the stage". It pre-signs everything,
holds, and fires at the exact start time — no supervision needed. The Fire?
confirmation comes before the wait, so there's no second prompt at mint time.
Two things to get right if you're leaving it running:
- Stop your computer from sleeping. A sleeping machine suspends the process and the countdown freezes.
- Leave the terminal window open. Closing it kills the run.
At the "Wallets" step you get three options:
| Option | What happens |
|---|---|
| Paste private keys | The original flow — one key per line, hidden as you type, kept in memory only. |
| Generate new wallets | Pick a count (1 to a few thousand). One fresh seed phrase is created and every wallet is derived from it at m/44'/60'/0'/0/i. The phrase is shown once — write it down — then only its AES-256-GCM ciphertext (password-derived key via scrypt) is saved to wallets/<name>.json. The plaintext phrase is never written to disk. |
| Load saved wallets | Pick a saved set, enter its password, and choose how many of the saved wallets to use this run — e.g. generate 300 once, then fire only 50 of them on a given drop. Deriving the same index from the same phrase always reproduces the same address, so this is exact and repeatable. |
Generated wallets start empty — fund each address before firing (the tool
tells you the shortfall per wallet before you confirm). Run
npm start -- --fund to distribute native currency from one funder wallet to
many mint wallets at once: pick a saved set (or paste addresses), an amount
per wallet, and it sends the batch. The suggested default amount is a
gas-only cushion — enough for the transaction fee alone, since not every mint
has a price. Add more if the drop you're targeting does.
Funding transfers all come from one sender, so unlike the mint blast they
can't all be independent — a node will only hold so many pending transactions
per account at once. The funding tool sends in batches (25 at a time by
default, FUNDING_BATCH_SIZE in .env) and waits for each batch before
moving to the next nonce range, rather than trying to queue all of them at
once and having the excess rejected.
Everything downstream treats every wallet identically regardless of count:
nonce and balance lookups are throttled and retried per-wallet (a rate-limited
or failed lookup for one wallet excludes only that wallet, it doesn't abort
the run for the rest), sockets get re-warmed a few seconds before fire time
rather than once at the start of a possibly hours-long wait, and receipts are
polled in a single shared batch instead of one independent poller per wallet.
Two env vars tune this for very large batches — see .env.example:
RPC_CONCURRENCY (in-flight RPC reads at once) and RPC_MAX_CONNECTIONS
(HTTP connection pool size per RPC host).
npm start -- --rpc-check times every candidate endpoint for a chain (your
.env entries, the public fallbacks in src/chains.ts, plus anything you
paste in) and ranks them fastest-first:
# median min max status endpoint
1 192 188 684 read+send PUBLICNODE
2 263 256 898 send-only mainnet-sequencer.base.org
3 286 273 1557 read+send mainnet.base.org
Each endpoint is hit 5 times with eth_chainId; median latency is what's
ranked on, since a single fast/slow sample can be noise. "send-only" means the
node rejected the read call but is still fine to blast a signed tx at (e.g. a
sequencer). Anything that answers with the wrong chain id is flagged, not
ranked. Run it before a drop to decide which URL to paste at the RPC prompt —
it doesn't change any config itself.
Three different numbers, and mixing them up is the most common way to lose a mint:
| Term | What it is | Who sets it |
|---|---|---|
| Base fee | The network's price. Burned. | The chain |
| Priority fee (tip) | Paid on top, to the block producer | You |
| Max fee | The ceiling you'll tolerate | You |
You pay base fee + tip. The max fee is only a cap — if the base fee is 0.006 gwei and your tip is 0.05, you pay 0.056 gwei no matter whether your ceiling is 2 or 200.
But the ceiling is not free. Before a node will even accept your transaction,
it checks that your wallet holds gasLimit × maxFee + mint price. So a high
ceiling with a thin wallet gets rejected outright, even though the real cost is
tiny. The tool checks this before firing and tells you the highest ceiling your
balance can support.
A SeaDrop mint uses roughly 135,000 gas for quantity 1.
Each of these is checked before anything is broadcast:
- Ceiling below the base fee — rejected by every node. Can't be entered.
- Tip above the ceiling — invalid under EIP-1559. Can't be entered.
- Wallet can't cover the upfront reservation — refuses to fire and tells you the ceiling you can afford.
- Firing before the stage opens — SeaDrop reverts with
NotActiveand burns gas. "Fire now" isn't offered when the stage opens later. - Wrong network — every RPC is checked for its chain ID; any on the wrong chain is dropped rather than blasted at.
- Quantity above the per-wallet cap — warned before you fire.
If a transaction is rejected by every endpoint, it says so plainly instead of waiting for a receipt that will never exist.
| Chain | Chain ID | Network | Explorer |
|---|---|---|---|
| Ethereum | 1 | mainnet | etherscan.io |
| Base | 8453 | mainnet | basescan.org |
| Ink | 57073 | mainnet | explorer.inkonchain.com |
| Robinhood Chain | 4663 | mainnet | robinhoodchain.blockscout.com |
| Sepolia | 11155111 | testnet | sepolia.etherscan.io |
| Amoy | 80002 | testnet | amoy.polygonscan.com |
The wizard's first prompt is Network (Mainnet / Testnet); picking one
filters the chain menu to just that network's entries, so a live-operations
run can't accidentally land on a chain whose currency is worthless (or the
reverse — firing a real mint at a testnet contract). NETWORK in .env sets
which one is pre-selected.
To add another chain, append one entry to src/chains.ts —
set testnet: true for a test network — no other code changes needed.
- Pasted private keys are kept in memory and never written to disk or transmitted anywhere except as a locally-signed transaction.
- Generated wallet sets are the one thing that does touch disk: their seed
phrase is encrypted (AES-256-GCM, scrypt-derived key from a password you
set) before being saved to
wallets/. Nobody without that password and file can recover it — but if you'd rather nothing ever touches disk, use "Paste private keys" instead. .env,wallets/and*.keyare all git-ignored.- Use dedicated hot wallets funded with only what you intend to spend.
- Read
src/local-mint.tsandsrc/keystore.tsif you want to verify exactly what gets signed, sent, and encrypted.
Not supported here, and it isn't an oversight. An allowlist stage uses SeaDrop's
mintSigned(), which needs a signature that OpenSea generates per wallet — that
genuinely requires an authenticated OpenSea session. There's no way to build it
from on-chain data, which is the whole premise of this tool.
MIT