I missed a drop I’d been waiting three weeks for. The website crashed the second minting went live. By the time I refreshed enough to get through, sold out. 8,000 NFTs gone in under 90 seconds. Floor hit 5x mint cost within an hour.
So I stopped relying on frontends. I built a bot that mints directly on-chain — no website, no UI, no clicking. Just a script that fires a transaction the instant the candy machine goes live.
How Solana NFT Mints Actually Work
Most Solana NFT collections use Metaplex Candy Machine. It’s an on-chain program holding all the NFT metadata. When minting opens, your wallet sends a mint instruction, pays the price, and gets back a freshly minted NFT.
The frontend on a project’s website? Just a wrapper around this instruction. You don’t need it. You can send the mint transaction directly — faster than any website can load.
Finding the Candy Machine Details
You need the candy machine ID, mint price, and go-live date. Projects announce these in Discord, but sometimes you have to dig through their mint page source code. Here’s how I read the state:
import { Connection, PublicKey } from "@solana/web3.js";
import { Metaplex } from "@metaplex-foundation/js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const metaplex = Metaplex.make(connection);
const CANDY_MACHINE_ID = new PublicKey("CandyMachineAddressHere...");
async function getCandyMachineState() {
const cm = await metaplex
.candyMachines()
.findByAddress({ address: CANDY_MACHINE_ID });
console.log("Remaining:", cm.itemsRemaining.toString());
console.log("Minted:", cm.itemsMinted.toString());
if (cm.goLiveDate) console.log("Go-live:", new Date(cm.goLiveDate.toNumber() * 1000));
return cm;
}
I run this every few hours before a drop to make sure nothing changed.
Building the Countdown Loop
I used to set a timer and hope the clock was right. That failed me more than once. Now I poll the candy machine directly:
async function waitForMintLive() {
while (true) {
try {
const candyMachine = await metaplex
.candyMachines()
.findByAddress({ address: CANDY_MACHINE_ID });
const now = Math.floor(Date.now() / 1000);
const goLive = candyMachine.goLiveDate?.toNumber() || 0;
if (candyMachine.itemsRemaining.toNumber() === 0) return null;
if (now >= goLive) return candyMachine;
const secondsLeft = goLive - now;
const delay = secondsLeft > 60 ? 10000 : secondsLeft > 10 ? 1000 : 200;
await new Promise((r) => setTimeout(r, delay));
} catch (err) {
await new Promise((r) => setTimeout(r, 2000));
}
}
}
Polling ramps up as we approach go-live — every 200ms in the last 10 seconds.
The Mint Transaction
Once it’s live, fire the mint instruction:
import { Keypair } from "@solana/web3.js";
import { keypairIdentity } from "@metaplex-foundation/js";
import bs58 from "bs58";
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_KEY));
metaplex.use(keypairIdentity(wallet));
async function mint() {
const candyMachine = await waitForMintLive();
if (!candyMachine) return;
const { nft } = await metaplex.candyMachines().mint({
candyMachine: candyMachine.address,
collectionUpdateAuthority: candyMachine.authorityAddress,
});
console.log("Minted!", nft.mint.address.toString());
return nft;
}
No frontend, no wallet popup. Done.
Retry Logic for Congested Drops
Popular drops are chaos. My first version sent one transaction and gave up. I missed three straight drops before adding retries:
async function mintWithRetries(maxAttempts = 5) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const { nft } = await metaplex.candyMachines().mint({
candyMachine: CANDY_MACHINE_ID,
collectionUpdateAuthority: candyMachine.authorityAddress,
});
console.log(`Minted on attempt ${attempt}!`, nft.address.toString());
return nft;
} catch (err) {
if (err.message.includes("not enough SOL")) break;
if (attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, 500 * attempt));
}
}
}
return null;
}
Backoff increases with each attempt. If the wallet’s broke, bail immediately.
Priority Fees: Cutting the Line
Validators process higher-fee transactions first. Priority fees are your fast pass. I thought the base fee was enough. It’s not.
import { ComputeBudgetProgram, Transaction } from "@solana/web3.js";
function addPriorityFee(transaction, microLamports = 100000) {
transaction.add(
ComputeBudgetProgram.setComputeUnitPrice({ microLamports }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 })
);
return transaction;
}
I set 50,000-200,000 microlamports depending on hype. For a hyped 10k PFP? Max it out.
Pre-Building Transactions
Build and sign before the mint goes live. The instant it opens, just send. No build time, no signing delay.
async function preBuildMintTransaction() {
const builder = metaplex.candyMachines().builders().mint({
candyMachine: CANDY_MACHINE_ID,
collectionUpdateAuthority: new PublicKey("AuthorityHere..."),
});
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
const tx = builder.toTransaction({ blockhash, lastValidBlockHeight });
addPriorityFee(tx);
tx.sign(wallet);
return tx;
}
const preSigned = await preBuildMintTransaction();
await waitForMintLive();
await connection.sendRawTransaction(preSigned.serialize());
The blockhash expires after ~60 seconds, so build 10-15 seconds before go-live.
Multi-Wallet Minting
One wallet, one mint — that’s the limit on most drops. I run 3-5 wallets on big drops.
const WALLETS = [
Keypair.fromSecretKey(bs58.decode(process.env.WALLET_1)),
Keypair.fromSecretKey(bs58.decode(process.env.WALLET_2)),
Keypair.fromSecretKey(bs58.decode(process.env.WALLET_3)),
];
async function multiMint(candyMachine) {
const results = await Promise.allSettled(
WALLETS.map(async (kp, i) => {
const mx = Metaplex.make(connection).use(keypairIdentity(kp));
const { nft } = await mx.candyMachines().mint({
candyMachine: candyMachine.address,
collectionUpdateAuthority: candyMachine.authorityAddress,
});
return nft;
})
);
console.log(`Minted ${results.filter((r) => r.status === "fulfilled").length}/${WALLETS.length}`);
}
All wallets fire simultaneously. Even 2 out of 5 is a win. Fund each wallet beforehand — I’ve had wallets fail because I forgot.
The Flip: Listing on a Marketplace
Minting is half the play. List immediately while hype is peak. I use Tensor:
import { createListInstruction } from "@tensor-foundation/marketplace";
async function listOnTensor(mintAddress, priceInSol) {
const listIx = createListInstruction({
owner: wallet.publicKey,
mint: new PublicKey(mintAddress),
amount: priceInSol * 1e9,
currency: null,
});
const transaction = new Transaction().add(listIx);
addPriorityFee(transaction);
await connection.sendTransaction(transaction, [wallet]);
}
// After minting, list at 2x mint price
const nft = await mintWithRetries();
if (nft) await listOnTensor(nft.mint.address.toString(), mintPrice * 2);
I price at 1.5-2x mint cost right away. I’ve watched too many floors crater while people diamond-handed for 10x. Take the quick profit.
Picking Drops and Avoiding Garbage
The code took me a weekend. Knowing which drops to bot is the actual hard part. I look for active Discord communities (not bot-filled channels), quality art, doxxed teams, and supply under 5k. If mint is 2 SOL and similar collections floor at 1.5, skip it.
This isn’t free money. Failed transactions still cost gas — I’ve burned 0.5 SOL on failed attempts during one mint. I’ve minted NFTs that went to zero within a day. Some projects blocklist bots with captchas, so this works on maybe 60-70% of drops. And free RPCs choke under load — I pay $50/month for a Helius node.
The bot is maybe 20% of the edge. The other 80% is research. I could hand you this exact code and you’d still lose money minting everything on your timeline. The bot gets you in the door. Your judgment decides if that door leads somewhere worth going.