Skip to content

NFT Operations Guide

How the Diamond NFT system works end-to-end — deploying infrastructure, creating collections, minting tokens, and serving metadata.


Environment Setup

The NFT system uses the monorepo's profile-based env system. All env vars are generated by pnpm env:build <profile>, which writes .env.<profile> files to each package directory. The deploy scripts pick up the right profile automatically.

VariableServicePurpose
NFT_DEPLOYER_PRIVATE_KEYnft-contracts, emprops-apiPrivate key for the deployer/minter wallet
EMPROPS_API_URLmonitorURL to emprops-api (e.g., http://localhost:3335)
ARBITRUM_SEPOLIA_RPC_URLnft-contractsRPC endpoint for Arbitrum Sepolia
ARBITRUM_ONE_RPC_URLnft-contractsRPC endpoint for Arbitrum One

For local development, the Hardhat node uses the default account 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 with the well-known private key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80.


Infrastructure Deployment (One-Time Per Chain)

Infrastructure = DiamondFactory + all shared facets. This is deployed once per chain and rarely changes. The deploy command handles everything — compiling contracts, deploying them on-chain, and writing the deployment record.

Local (Hardhat Node)

bash
# Terminal 1: Start the Hardhat node
pnpm nft:node

# Terminal 2: Deploy factory + facets
pnpm nft:deploy:local

This deploys all facets (ERC721, Mint, ContractURI, MetadataUpdate), the CollectionInit initializer, and the DiamondFactory. It writes all contract addresses to packages/nft-contracts/deployments/localhost.json.

Warning

The Hardhat node is in-memory. When you restart it, all contracts are gone. You'll need to redeploy and reset any stale collections in the DB (see Troubleshooting).

Arbitrum Sepolia (Testnet)

bash
pnpm nft:deploy:sepolia

Loads .env.staging by default. To use a different profile:

bash
pnpm nft:deploy:sepolia production

Writes packages/nft-contracts/deployments/arbitrum-sepolia.json.

Arbitrum One (Mainnet)

bash
pnpm nft:deploy:mainnet

Loads .env.staging by default. To use a different profile:

bash
pnpm nft:deploy:mainnet production

Writes packages/nft-contracts/deployments/arbitrum-one.json.

What the Deploy Writes

Each deployment produces a JSON file at packages/nft-contracts/deployments/{network}.json:

json
{
  "chainId": 421614,
  "deployer": "0x...",
  "timestamp": "2026-02-09T16:46:39.871Z",
  "contracts": {
    "DiamondCutFacet": "0x...",
    "DiamondLoupeFacet": "0x...",
    "OwnershipFacet": "0x...",
    "ERC721Facet": "0x...",
    "MintFacet": "0x...",
    "ContractURIFacet": "0x...",
    "CollectionInit": "0x...",
    "DiamondFactory": "0x..."
  }
}

These files should be committed to the repo. They are static config — the factory address never changes once deployed. The monitor and emprops-api read them at runtime to know which factory to call when creating collections.

Docker Considerations

The monitor reads deployment JSONs from ../../packages/nft-contracts/deployments/ relative to process.cwd(). For Docker:

  • pnpm nft:build (part of build:docker:all) compiles contracts and produces artifacts
  • Deployment JSON files are committed to the repo and included in the Docker build context
  • No ENV vars needed for factory addresses — they come from the committed JSON files

Collection Lifecycle

1. Create a Collection (Deploy Diamond Proxy)

Collections are created through the Monitor NFT Admin UI (Collections tab). You enter an EmProps Collection UUID, and the monitor:

  1. Calls GET /nft/collections/:id on emprops-api to fetch collection info
  2. Pre-fills contract params from the collection record (name, symbol from title, baseURI, maxSupply from editions)
  3. On Deploy Collection, calls the DiamondFactory's createCollection() on-chain
  4. On success, calls POST /nft/collections/:id/deploy-status on emprops-api to write the contract address back to the DB

After deploy, the collection record in the DB has:

  • contract_address — the Diamond proxy address
  • chain_id — which chain it's on
  • deploy_tx_hash — the deployment transaction
  • base_token_uri — metadata base URL
  • mint_status — "deployed"

2. Mint Tokens

Via emprops-api (metadata pipeline — the standard path):

POST /nft/collections/:collectionId/mint
{
  "recipient": "0x...",
  "chainId": 31337,
  "outputId": "uuid"  // optional — links to generated artwork
}

This endpoint handles the full mint flow:

  1. Looks up the collection's contract address from DB
  2. Gets the next token ID from the contract (totalSupply)
  3. Builds ERC-721 metadata with trait attributes from is_feature variables
  4. Calls MintFacet.mintTo(recipient) on-chain
  5. Creates/updates a deck record with mint state
  6. Stores token_metadata on the output record

Via Monitor Admin UI (direct on-chain, no metadata):

Monitor → Mint tab → enter collection address, recipient, quantity → mint. This calls the contract directly, bypassing the metadata pipeline. Useful for testing.

3. Serve Metadata

Public endpoints (no auth — NFT marketplaces read these):

GET /nft/metadata/:collectionId/collection.json  → ERC-7572 contract metadata
GET /nft/metadata/:collectionId/:tokenId          → ERC-721 token metadata

The on-chain tokenURI() resolves to these URLs. Token metadata includes trait attributes extracted from is_feature variables in the collection's generation config.

4. Check Mint Status

GET /nft/decks/:deckId/mint-status
GET /nft/collections/:collectionId/mints

API Endpoint Reference

emprops-api (shared backend — port 3335)

All authenticated endpoints use jwtOrApiKeyMiddleware (passthrough when ENABLE_AUTH=false).

MethodPathAuthPurpose
GET/nft/collections/:collectionIdYesCollection info + deployment status
POST/nft/collections/:collectionId/deploy-statusYesWrite-back after deploy
POST/nft/collections/:collectionId/resetYesReset stale deployment
POST/nft/collections/:collectionId/mintYesMint with metadata pipeline
GET/nft/decks/:deckId/mint-statusYesPoll mint status
GET/nft/collections/:collectionId/mintsYesList minted tokens
GET/nft/metadata/:collectionId/collection.jsonNoContract metadata (ERC-7572)
GET/nft/metadata/:collectionId/:tokenIdNoToken metadata (ERC-721)

monitor (admin UI — port 3333)

Monitor routes proxy to emprops-api for DB operations. On-chain operations (deploy, direct mint, admin) use viem directly.

MethodPathPurpose
GET/api/nft/collections/lookup?collectionId=...Proxy → emprops-api collection lookup
POST/api/nft/collections/resetProxy → emprops-api reset
GET/api/nft/collections?chainId=&addresses=Read on-chain collection info
POST/api/nft/collectionsDeploy new collection (on-chain + DB writeback)
POST/api/nft/mintDirect on-chain mint (no metadata)
POST/api/nft/adminAdmin operations (setMinter, setMintActive, etc.)

Data Model

Collection (DB)                    Diamond Proxy (on-chain)
┌─────────────────────┐           ┌──────────────────────┐
│ id (UUID)           │──────────→│ address              │
│ title               │           │ name()               │
│ contract_address ───┼──────────→│ symbol()             │
│ chain_id            │           │ totalSupply()        │
│ deploy_tx_hash      │           │ maxSupply()          │
│ base_token_uri      │           │ mintActive()         │
│ mint_status         │           │ owner()              │
│ editions            │           │ minter()             │
│ data.variables[]    │           │ tokenURI(id)         │
└─────────┬───────────┘           └──────────────────────┘
          │ has many

Deck (DB) = NFT Token
┌─────────────────────┐
│ id (UUID)           │
│ collection_id       │
│ token_id            │
│ mint_tx_hash        │
│ mint_recipient      │
│ mint_status         │
│ minted_at           │
│ metadata_uri        │
│ cover_output_id ────┼───→ Output.token_metadata (ERC-721 JSON)
└─────────────────────┘

Troubleshooting

Stale Deployments

Symptom: Collection shows as "deployed" in the DB but the contract doesn't exist on-chain (e.g., after Hardhat node restart).

Fix — Monitor UI: Enter the collection UUID → the UI detects the stale deployment → click "Reset Collection".

Fix — API:

bash
curl -X POST http://localhost:3335/nft/collections/{collectionId}/reset

This nulls out contract_address, chain_id, deploy_tx_hash, base_token_uri on the collection and resets all deck mint state.

Contract Artifacts Missing

Symptom: Deploy gives Unexpected token '<', "<!DOCTYPE"... error (HTML instead of JSON).

Cause: pnpm nft:clean deletes contract artifacts. The monitor imports bytecodes from these JSON files for ABI encoding.

Fix:

bash
pnpm nft:build    # clean + recompile

Docker Image Not Found

Symptom: Docker image not found: emprops/emprops-api:latest-local-docker

Fix: Build the image with the matching profile:

bash
pnpm build:docker:emprops-api local-docker

Images are tagged latest-{profile} (e.g., latest-local-docker, latest-production).

Monitor Can't Reach emprops-api

Symptom: Collection lookup returns EMPROPS_API_URL not configured.

Fix: Ensure EMPROPS_API_URL is set in the monitor's environment. For local dev: http://localhost:3335.


pnpm Scripts Reference

ScriptPurpose
pnpm nft:buildClean + compile contracts
pnpm nft:testRun contract tests (31 tests)
pnpm nft:nodeStart local Hardhat node
pnpm nft:deploy:local [profile]Deploy factory to localhost (default: local)
pnpm nft:deploy:sepolia [profile]Deploy factory to Arbitrum Sepolia (default: staging)
pnpm nft:deploy:mainnet [profile]Deploy factory to Arbitrum One (default: staging)
pnpm nft:devStart node + monitor together
pnpm nft:cleanClean artifacts + delete deployment JSONs
pnpm build:docker:allBuild all Docker images (includes nft:build)

Released under the MIT License.