Skip to content

EternaHybridExchange/nft-minter-cli

Repository files navigation

nft-minter-cli

Open-source NFT image minter for Ethereum — CLI + web UI. Deploy an ERC-721A contract, upload images/metadata to IPFS (Pinata), and mint single or batch NFTs.

Stack

Layer Tool
Contracts Foundry, Solidity 0.8.30, ERC-721A (Chiru Labs)
CLI Node 20, TypeScript, commander, viem v2
Web Next.js 14 (App Router), wagmi v2, RainbowKit v2
Storage IPFS via Pinata
Monorepo pnpm workspaces + Turborepo

Quickstart

# 1. Install deps
pnpm install

# 2. Install Foundry deps
cd contracts && forge install && cd ..

# 3. Copy env + fill in keys
cp .env.example .env

# 4. Run contract tests
pnpm contracts:test

# 5. Build the CLI
pnpm --filter @nft-minter/cli build

# 6. Start the web app
pnpm --filter @nft-minter/web dev

Drive it with an AI agent

The fastest path to shipping a drop: hand this repo to a coding agent and say:

"Deploy my NFT collection to Sepolia"

A project-level skill at .claude/skills/nft-drop/SKILL.md walks the agent through preflight checks (env vars, Foundry toolchain, build), scaffolding, image placement, and the full drop command — including the real-world gotchas that usually eat an afternoon: Pinata free-tier quotas, missing Sepolia ETH, and OpenSea's metadata-indexing lag.

Works with any AI coding agent, not just Claude Code — the skill file is plain markdown, not a Claude-only format:

  • Claude Code autoloads it from .claude/skills/nft-drop/ when your prompt matches the skill's triggers. Open the repo and ask.
  • Cursor, Cline, Windsurf, Continue, Aider, Codex CLI, etc. — point the agent at .claude/skills/nft-drop/SKILL.md (via @file, a project rules entry, or your system prompt) and ask it to do the drop. The runbook reads the same to any agent that can execute shell commands.

CLI Usage

The 3-step path (recommended)

# 1. Scaffold a fresh collection folder with manifest + images/ dir
nft-mint init my-drop --name "My Drop" --count 10

# 2. Drop your 10 images into my-drop/images/ (named 1.png … 10.png)

# 3. One command: pin to IPFS, deploy, and batch-mint to the deployer
nft-mint drop \
  --manifest my-drop/manifest.json \
  --network sepolia \
  --mint

nft-mint drop prints the contract address, Etherscan link, and OpenSea URL when it's done. That's it — no Pinata dashboard, no copying CIDs by hand.

Individual commands

# Pin images + metadata as two IPFS folders (baseURI is saved next to the manifest)
nft-mint upload --manifest ./my-drop/manifest.json

# Deploy — auto-reads name + baseURI + maxSupply from the manifest + saved upload
nft-mint deploy --manifest ./my-drop/manifest.json --price 0.01 --network sepolia

# Batch-mint the entire manifest in one tx (ERC-721A: cheap regardless of qty)
nft-mint batch-mint --contract 0x… --manifest ./my-drop/manifest.json --network sepolia --owner

# Mint a single token
nft-mint mint --contract 0x… --to 0xRecipient --quantity 1 --network sepolia

# Verify on Etherscan (deploy --verify does this automatically)
nft-mint verify --address 0x… --network sepolia --name "My Drop" --symbol MD \
  --base-uri "ipfs://<cid>/" --max-supply 10 --price 0 --owner 0x…

manifest.json

{
  "name": "My Drop",
  "tokens": [
    {
      "file": "images/1.png",
      "name": "Token #1",
      "description": "First token",
      "attributes": [{ "trait_type": "background", "value": "blue" }]
    }
  ]
}

After upload / deploy, a .nft-mint.json file is written next to the manifest. It caches imagesFolderCid, metadataFolderCid, baseUri, and the deployed contract address so later commands (deploy, drop, batch-mint) can pick them up automatically. Safe to commit or delete.

Machine-readable output

mint and batch-mint support --json, which silences human-friendly output and emits a single JSON line to stdout containing txHash, blockNumber, tokenIds, contract, recipient, network. Useful when driving the CLI from a shell script:

RESULT=$(nft-mint batch-mint --contract 0x… --manifest ./m.json --network sepolia --owner --json)
echo "$RESULT" | jq '.tokenIds[]'

SDK (programmatic use from Node / Next.js backends)

The same logic is importable as an ES module. Use this when you want to drive mints and transfers from a backend (pre-minting into a holding wallet, user-claim flows, airdrop jobs, etc.) instead of running the CLI.

import {
  runBatchMint,
  runTransferNft,
  createClients,
  type MintResult,
  type TransferResult,
} from "@nft-minter/cli";
import { createWalletClient, createPublicClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// The caller builds its own viem clients — the SDK never touches process.env,
// so the signing key can come from Secrets Manager, a KMS wrapper, or any other
// signer you want to plug in.
const signer = privateKeyToAccount(process.env.SIGNER_KEY as `0x${string}`);
const transport = http(process.env.RPC_URL_SEPOLIA);
const publicClient = createPublicClient({ chain: sepolia, transport });
const walletClient = createWalletClient({ account: signer, chain: sepolia, transport });
const clients = { publicClient, walletClient, address: signer.address, chainName: "sepolia" as const };

// Pre-mint a batch to the signer.
const mint: MintResult = await runBatchMint({
  contract: "0x2Fcde6D4cc5711326dC1b693E93C4e9fFF9d5206",
  quantity: 10n,
  ownerMint: true,
  clients,
});
// mint.tokenIds → [1n, 2n, …, 10n]  ← parsed from Transfer logs
// mint.txHash, mint.blockNumber, mint.recipient

// Later, transfer an already-minted token to a claimer.
const xfer: TransferResult = await runTransferNft({
  contract: "0x2Fcde…",
  from: signer.address,
  to: userAddress,
  tokenId: 3n,
  clients,
});

Local install (the package is not published to npm yet):

// your-app/package.json
{
  "dependencies": {
    "@nft-minter/cli": "file:../nft-minter-cli/packages/cli"
  }
}

Then in your app: pnpm install && pnpm build. The package's main points at dist/sdk.js — CLI binary at dist/index.js is unaffected by imports.

Idempotency

The SDK does not enforce idempotency — that's the caller's job, and it matters whenever the mint/transfer request comes from an automated source (e.g., a winner list, a claim queue) that could fire twice. Recommended pattern:

Before sdk.runBatchMint:
  1. INSERT INTO mint_jobs (batch_id, status='minting', requested_quantity, requested_at)
     ON CONFLICT (batch_id) DO NOTHING RETURNING id;
  2. If no row returned → another job is already running, abort.

After sdk.runBatchMint succeeds:
  3. UPDATE mint_jobs SET status='minted', tx_hash, token_ids WHERE batch_id = …;
  4. INSERT INTO nft_inventory (batch_id, token_id, token_uri, status='available') …;

If the caller crashes between steps 2 and 3:
  5. Reconciliation job polls `status='minting'` rows older than N minutes.
  6. For each, re-fetch receipt using the broadcasted tx hash (capture it inside
     sdk.runBatchMint before awaiting the receipt by wrapping the call — or
     instrument a DB write inside a viem walletClient middleware).
  7. Alternatively, compare `contract.totalSupply()` before/after the window
     to detect the mint happened without a known tx hash.

Same pattern for runTransferNft on claim flow — persist status='claiming' before the SDK call, write claim_tx_hash on success. A claim attempt must never re-execute if a prior attempt hasn't been reconciled.

The SDK returns typed MintResult / TransferResult objects (including tokenIds parsed from Transfer logs) so persistence is a straightforward database write — no log scraping on your side.

Architecture

nft-minter-cli/
├── contracts/          # Foundry — EternaImageNFT (ERC-721A)
└── packages/
    ├── shared/         # ABI, chain config, metadata schema (zod)
    ├── cli/            # `nft-mint` binary
    └── web/            # Next.js app

Development

pnpm lint            # lint JS/TS packages (eslint)
pnpm test            # test JS/TS packages
pnpm build           # build JS/TS packages
pnpm format          # format with prettier

# Contracts live in their own Foundry toolchain:
pnpm contracts:lint  # forge fmt --check
pnpm contracts:build # forge build
pnpm contracts:test  # forge test -vvv

Deploying to Mainnet

Use a fresh hardware-wallet-backed key, not your dev wallet. The CLI's deploy command reads PRIVATE_KEY from .env; for mainnet, prefer using cast wallet import + forge script --account instead of a raw private key. See Foundry keystore docs.

License

MIT — see LICENSE.

About

Open-source NFT image minter for Ethereum — CLI + web UI. Deploy ERC-721A, pin images/metadata to IPFS, mint single or batch.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors