Daily NFT auction protocol. Every 24 hours, a new NFT is minted with on-chain generative art, auctioned, and settled automatically via Chainlink Automation. Proceeds go to a DAO treasury. After the initial unpause, the protocol runs on its own.
Unpause
|
v
Mint NFT (on-chain SVG + deterministic traits)
|
v
Start 24h auction
|
v
Users place bids (5% min increment, anti-sniping extension)
|
v
Auction expires --> Chainlink Keeper calls performUpkeep()
|
v
Settle: NFT to winner, ETH to treasury
|
v
Repeat (mint next NFT, start next auction)
Once unpaused, this cycle repeats indefinitely. Chainlink Keepers handle settlement and kick off each new auction.
Monorepo with three packages:
nightmint/
packages/
contracts/ Solidity smart contracts (Foundry)
web/ Auction frontend (Next.js 16)
indexer/ Blockchain indexer and REST API (Ponder)
docs/
DESIGN.md Design system specification
| Package | Stack |
|---|---|
contracts |
Solidity 0.8.28, Foundry, OpenZeppelin, Chainlink Automation |
web |
Next.js 16, React 19, wagmi, viem, Tailwind CSS v4 |
indexer |
Ponder, Hono, zod |
Four contracts, narrow scope each:
NightMintAuctionHouse runs the auction cycle: bidding, refunds, anti-sniping, settlement, Chainlink integration.
- Auction duration: 24 hours (configurable)
- Reserve price: 0.01 ETH
- Min bid increment: 5%
- Anti-sniping buffer: 5 minutes - if a bid lands near the end, the auction extends
NightMintToken is the ERC-721. It can only mint through the AuctionHouse and delegates metadata entirely to the Descriptor.
NightMintSeeder generates a deterministic seed per token from keccak256(blockhash(block.number - 1), tokenId, descriptor). Each seed maps to five traits: background, shape, shapeColor, accentShape, accentColor.
NightMintDescriptor stores the trait arrays and builds the full SVG + JSON metadata on-chain through the NFTDescriptor library. No IPFS. No external storage.
Most NFT projects store a URL pointing to IPFS. If the pinning service stops, the art disappears. NightMint generates everything on-chain. Costs more gas to read, but the metadata doesn't depend on anyone keeping a server running.
If a bid arrives within 5 minutes of auction end, the end time extends by 5 minutes. Without this, last-second bids would dominate since on-chain auctions have natural latency from block times.
When outbid, the previous bidder gets refunded immediately via a low-level call capped at 50k gas. If that fails (e.g. the bidder is a contract that reverts on receive), funds go to a pendingReturns mapping for manual withdrawal. This stops a malicious bidder from bricking the auction by reverting every refund.
Ponder watches contract events and syncs them to a database:
AuctionCreated- new auction recordAuctionBid- records the bid, increments auction bid countAuctionExtended- updates auction end timeAuctionSettled- marks auction settled, records winner and final bid
REST API built with Hono:
| Endpoint | What it returns |
|---|---|
GET /api/auctions |
Paginated list, sortable by recent, highest, lowest |
GET /api/auctions/:tokenId |
Single auction |
GET /api/bids/:tokenId |
Bid history for an auction |
GET /api/profile/:address/bids |
All bids by an address |
GET /api/profile/:address/wins |
Auctions won by an address |
GraphQL also available at /graphql.
The UI pulls from two sources:
- wagmi contract reads - polls active auction state every 4 seconds (current bid, bidder, end time)
- Ponder API - historical data for bid history, gallery, and profiles
useBidHistory merges indexed bids from the API with live bids from wagmi's event watcher. When you bid, it shows up immediately - before the indexer even processes the block.
Pages:
/- live auction with NFT display, bid input, countdown, bid history/gallery- infinite scroll of past auctions with sorting and total volume stats
The design system uses a dark base (#0e0e0e) with color-coded semantics: green (#9cff93) for ETH/value, cyan (#00cffc) for time, purple (#ac89ff) for accents. No borders anywhere - sections separate through surface tone shifts. Full spec in docs/DESIGN.md.
# Clone with submodules (OpenZeppelin, Chainlink deps)
git clone --recurse-submodules <repo-url>
cd nightmint
# Install dependencies
pnpm install
# Install Foundry dependencies
cd packages/contracts && forge install && cd ../..Contracts (packages/contracts/.env):
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
ETHERSCAN_API_KEY=YOUR_KEY
ACCOUNT=your-foundry-account
ADDRESS=0x...
TREASURY_ADDRESS=0x...
Frontend (packages/web/.env.local):
NEXT_PUBLIC_AUCTION_HOUSE_ANVIL=0x...
NEXT_PUBLIC_TOKEN_ANVIL=0x...
NEXT_PUBLIC_INDEXER_URL=http://localhost:42069
Indexer (packages/indexer/.env.local):
PONDER_CHAIN=anvil
# Override defaults only if needed:
# PONDER_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
# PONDER_CONTRACT_ADDRESS=0x...
# PONDER_START_BLOCK=10487672
# Start local Anvil node (4s block time)
anvil --block-time 4
# Deploy contracts to Anvil
cd packages/contracts
forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast
# Start the indexer
pnpm indexer:dev
# Start the frontend
pnpm dev| Command | What it does |
|---|---|
pnpm dev |
Start frontend dev server |
pnpm build |
Build frontend for production |
pnpm lint |
Lint frontend code |
pnpm forge:build |
Compile contracts |
pnpm forge:test |
Run contract tests |
pnpm indexer:dev |
Start indexer (dev mode) |
pnpm indexer:start |
Start indexer (production) |
The deploy script (packages/contracts/script/Deploy.s.sol) does:
- Deploy Seeder, Descriptor (populates traits), Token, AuctionHouse
- Wire the AuctionHouse as the Token's minter
- Unpause to start the first auction
Works on Anvil (local) and Sepolia (testnet).
MIT