Minting with thirdweb

thirdweb is a platform that offers tools and infrastructure for building decentralized applications on multiple blockchain networks. It simplifies blockchain integration with SDKs, APIs, and pre-built components.

In the case of NFT minting, thirdweb offers a polished dashboard and IPFS upload along with its audited contracts and extensive SDKs.

Minting a single NFT manually

If you just want to upload a single image and mint it, thirdweb offers a click-to-deploy interface.

First, create a thirdweb account using your Web3 wallet as the owner. This gives you access to your thirdweb dashboard.

From there, you can choose between two paths:

  • Use their TokenERC721 page and its "Deploy Now" button.

  • Or use their dashboard's "Create Token" interface.

Let's explore each real quick — the thirdweb Developer Portal will give you more information should you need it.

Using the TokenERC721 page

  1. Go to the TokenERC721 page. This relies on their contract for creating an NFT Collection.

  2. Click "Deploy Now" to access to contract deployment page.

  1. Fill in the "Contract metadata" section:

    1. Upload the image that you want to represent your NFT collection (or just the image that you want to mint).

    2. Give it a unique name and a symbol/ticker to your collection.

    3. Give it a description.

  2. thirdweb automatically fills the recipient addresses for "Primary Sales" and "Royalties", make sure to check that the Web3 addresses are indeed that of your wallet.

  3. In the "Deploy Options", choose Chiliz Chain (or Chiliz Spicy Testnet if this is just a test) and click "Deploy Now":

  1. thirdweb will start deploying the contract, triggered a confirmation modal from your Web3 wallet. Confirm it to finish deploying.

  2. Once done, click the "View Contract" button. You'll be taken to the contract's page on your thirdweb dashboard, with a checklist/progress bar:

  3. As you can see from the checklist, for now all you've done is deploy a contract, you still need to actually mint the NFT. On the left column, click on the "NFTs" menu item to open a new page, which is for now empty:

  4. Click the "Mint" button and a side-panel opens with field for each attribute of your NFT. This is were you create the metadata for your NFT:

  5. Fill in the various fields. You can create fields that are specific to your NFT collection, such as "Artist_name" or "Type_of_work", and explore advanced options, but for this test you can keep it minimal: name, media file, description.

  6. Click on the "Mint NFT" button at the bottom of the side-panel, and approve the transaction from your wallet.

  7. thirdweb will then display the "NFTs" page, with your NFT now visible, attached to your NFT contract.

DONE! You have minted your NFT!

To check that it is indeed on Chiliz Chain, open Chiliscan (or its Testnet variant if you're using Spicy Testnet) and copy-paste the NFT's hash in the search field. It should straight away confirm that your contract is on the chain, and its "Inventory" tab should list your NFT with you (or at least your wallet's ID) as the owner.

If you minted your NFT on Chiliz Chain Mainnet, you should see it appear in your account on marketplaces such as Rarible or OpenSea — and from there, you can start selling it!

Using the "Create Token" button

This is in fact a more straightforward way than the one above, and is a recent addition to thirdweb.

  1. From any project folder in your thirdweb dashboard, click the "Tokens" option in the left sidebar:

  2. In the "Tokens" page that open, click the "Create Token" button on the right handside. A modal window open with 2 options, choose "Create NFT Collection".

  3. This will take you to a step-by-step version of the TokenERC721 deployment page (in effect, it will deployer an ERC-721 Drop contract). Fill in all the fields necessary for this contract, then press the "Next" button.

  4. The next screen is about the NFT itself: upload the image and fill in the fields describing your image, then press the "Next" button.

  5. thirdweb now displays a summary of the NFT collection that you are about to launch. Check that everything is correct then click the "Launch NFT Collection" button.

  6. thirdweb takes charge of deploying the contract, minting the NFT, and setting the conditions, all in one shot. You will need to confirm three transactions through your Web3 wallet.

  7. Once this is done, click the "View NFT" button, and you'll be taken back to your thirdweb dashboard, displaying the page specific to this contract — and its attached NFTs.

From there, you can check that it's indeed on the chain through a block explorer, or start selling your NFT through a marketplace.

Minting a single NFT programmatically

Here is a code sample that using the thirdweb v5 SDK, which uploads the media file to IPFS, generates the metadata.json file, then uploads it to IPFS too.

This will require you to have:

  • An already-deployed ERC-721 contract on Chiliz Chain. In this case you should do it using thirdweb rather another tool.

  • A thirdweb API key, tied to the ERC-721 contract that you deployed through thirdweb. You can find it in the project page of the smart contract, under the name "Client ID".

  • A local installation of the thirdweb SDK. You can do it using npm:

npm install thirdweb dotenv

The dotenv parameter generates an .env file, which is necessary to store private values:

THIRDWEB_SECRET_KEY=YOUR_TW_SECRET_KEY # From your thirdweb project's page.
PRIVATE_KEY=0xabc...                   # The Web3 wallet that will send the transaction.
CONTRACT_ADDRESS=0xYour721Address      # Your deployed TokenERC721 contract.
CHAIN_ID=88882                         # 88888=Mainnet, 88882=Spicy
IMAGE_PATH=./art/image.png             # The local path to your media file.
NAME=My Chiliz NFT
DESCRIPTION=Minted on Chiliz Chain

Now that you're all set, you can take inspiration from this script, which will:

  1. Upload the media file to IPFS

  2. Build and upload the metadata.json file

  3. Mint the NFT using the contract's mintTo method.

  4. Display the resulting tokenId and tokenUrl to you.

import 'dotenv/config';
import fs from 'fs';
import path from 'path';

import { createThirdwebClient, getContract, sendTransaction, waitForReceipt, parseEventLogs } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { privateKeyToAccount } from "thirdweb/wallets";
import { mintTo, transferEvent, getNFT } from "thirdweb/extensions/erc721";
import { upload } from "thirdweb/storage";

function guessMime(p: string) {
  const ext = path.extname(p).toLowerCase();
  if (ext === ".png") return "image/png";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";
  if (ext === ".mp4") return "video/mp4";
  if (ext === ".webm") return "video/webm";
  return "application/octet-stream";
}

async function main() {
  const client = createThirdwebClient({ secretKey: process.env.THIRDWEB_SECRET_KEY! });
  const chain = defineChain(Number(process.env.CHAIN_ID)); // 88888 or 88882
  const account = privateKeyToAccount({ client, privateKey: process.env.PRIVATE_KEY! });

  const contract = getContract({
    client,
    chain,
    address: process.env.CONTRACT_ADDRESS as `0x${string}`,
  });

  // Upload media file to IPFS
  const filePath = process.env.IMAGE_PATH!;
  const buffer = fs.readFileSync(filePath);
  const file = new File([buffer], path.basename(filePath), { type: guessMime(filePath) });

  const [imageUri] = await upload({ client, files: [file] }); // returns ipfs://... 
  // console.log("imageUri:", imageUri);

  // Build & upload metadata.json referencing IPFS imageUri
  const metadata = {
    name: process.env.NAME!,
    description: process.env.DESCRIPTION!,
    image: imageUri
  };
  const metaBlob = new Blob([JSON.stringify(metadata, null, 2)], { type: "application/json" });
  const metaFile = new File([metaBlob], "metadata.json", { type: "application/json" });

  const [metadataUri] = await upload({ client, files: [metaFile] });
  // console.log("metadataUri:", metadataUri);

  // Prepare & send mintTo transaction using the metadata URI
  // mintTo accepts a string to use directly as tokenURI.
  const tx = mintTo({
    contract,
    to: account.address,
    nft: metadataUri, // The tokenURI
  });

  const { transactionHash } = await sendTransaction({ transaction: tx, account });
  //console.log("tx:", transactionHash);

  const receipt = await waitForReceipt({ client, chain, transactionHash });

  // Parse Transfer event to display tokenId
  const events = parseEventLogs({ logs: receipt.logs, events: [transferEvent()] });
  const minted = events.find(e => e.eventName === "Transfer" && e.args.from === "0x0000000000000000000000000000000000000000");
  const tokenId = minted ? (minted.args.tokenId as bigint) : undefined;
  //console.log("tokenId:", tokenId?.toString() ?? "(not found)");

  if (tokenId !== undefined) {
    const nft = await getNFT({ contract, tokenId });
    //console.log("tokenURI (read back):", nft.tokenURI);
  }
}

main().catch((err) => (console.error(err), process.exit(1)));

The important parts to take inspiration from are:

  • upload stores files (and JSON) to IPFS and returns ipfs://… URIs.

  • mintTo accepts a string to use directly as tokenURI.

  • We parse the standard ERC-721 Transfer event to read the minted tokenId.

Minting a collection of NFTs programmatically

Minting a collection of NFTs is not that different from minting a single one. The most notable difference is that your .env file points to the folder that contains all the images using IMAGE_DIR, rather than on a single image with IMAGE_PATH.

THIRDWEB_SECRET_KEY=YOUR_TW_SECRET_KEY # From your thirdweb project's page.
PRIVATE_KEY=0xabc...                   # The Web3 wallet that will send the transaction.
CONTRACT_ADDRESS=0xYour721Address      # Your deployed TokenERC721 contract.
CHAIN_ID=88882                         # 88888=Mainnet, 88882=Spicy
IMAGE_PATH=./art                       # The local path to your media files.
NAME_PREFIX=My Chiliz NFT              # This will become: "My Chiliz NFT <n>"
DESCRIPTION=Minted on Chiliz Chain

The sample code that we represents here therefore takes this into account:

import 'dotenv/config';
import fs from 'fs';
import path from 'path';

import { createThirdwebClient, getContract, sendTransaction, waitForReceipt, parseEventLogs } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { privateKeyToAccount } from "thirdweb/wallets";
import { mintTo, transferEvent, getNFT } from "thirdweb/extensions/erc721";
import { upload } from "thirdweb/storage";

function guessMime(p: string) {
  const ext = path.extname(p).toLowerCase();
  if (ext === ".png") return "image/png";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";
  if (ext === ".mp4") return "video/mp4";
  if (ext === ".webm") return "video/webm";
  return "application/octet-stream";
}

// One way to gather all files
function listMediaFiles(dir: string) {
  const allow = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".webm", ".svg"]);
  return fs
    .readdirSync(dir)
    .filter((f) => allow.has(path.extname(f).toLowerCase()))
    .map((f) => path.join(dir, f));
}

async function main() {
  const client = createThirdwebClient({ secretKey: process.env.THIRDWEB_SECRET_KEY! });
  const chain = defineChain(Number(process.env.CHAIN_ID)); // 88888 or 88882
  const account = privateKeyToAccount({ client, privateKey: process.env.PRIVATE_KEY! }); 

  const contract = getContract({
    client,
    chain,
    address: process.env.CONTRACT_ADDRESS as `0x${string}`,
  });

  // Upload media files in a folder to IPFS
  const dirPath = process.env.IMAGES_DIR!;
  const filePaths = listMediaFiles(dirPath);
  if (!filePaths.length) {
    throw new Error(`No media files found in ${dirPath}`);
  }

  const fileObjs = filePaths.map((p) => {
    const buffer = fs.readFileSync(p);
    return new File([buffer], path.basename(p), { type: guessMime(p) });
  });

  const imageUris = await upload({ client, files: fileObjs }); // returns ["ipfs://...", ...]
  //console.log("Uploaded images to IPFS:", imageUris.length, "files");

  // Build & upload each metadata.json referencing its imageUri
  // Uses NAME and DESCRIPTION envs; names become "<NAME> #<index>"
  const namePrefix = process.env.NAME!;
  const description = process.env.DESCRIPTION!;

  // Create an array of File objects for metadata JSONs
  const metaFiles = imageUris.map((imageUri, i) => {
    const metadata = {
      name: `${namePrefix} #${i + 1}`,
      description,
      image: imageUri
    };
    const metaBlob = new Blob([JSON.stringify(metadata, null, 2)], { type: "application/json" });
    return new File([metaBlob], `metadata-${i + 1}.json`, { type: "application/json" });
  });

  const metadataUris = await upload({ client, files: metaFiles }); // ["ipfs://.../metadata-1.json", ...]
  //console.log("Uploaded metadata files to IPFS:", metadataUris.length);

  // Prepare & send mintTo transactions using the metadata URIs (sequential)
  const ZERO = "0x0000000000000000000000000000000000000000";

  for (let i = 0; i < metadataUris.length; i++) {
    const metadataUri = metadataUris[i];

    const tx = mintTo({
      contract,
      to: account.address,
      nft: metadataUri, // The tokenURI
    });

    const { transactionHash } = await sendTransaction({ transaction: tx, account });
    console.log(`[${i + 1}/${metadataUris.length}] tx:`, transactionHash);

    const receipt = await waitForReceipt({ client, chain, transactionHash });

    // Parse Transfer event to get tokenId for this mint
    const events = parseEventLogs({ logs: receipt.logs, events: [transferEvent()] });
    const minted = events.find(
      (e) => e.eventName === "Transfer" && typeof e.args.from === "string" && e.args.from.toLowerCase() === ZERO
    );
    const tokenId = minted ? (minted.args.tokenId as bigint) : undefined;

    // console.log(`[${i + 1}/${metadataUris.length}] tokenId:`, tokenId?.toString() ?? "(not found)");
    if (tokenId !== undefined) {
      const nft = await getNFT({ contract, tokenId });
      // console.log(`[${i + 1}/${metadataUris.length}] tokenURI (read back):`, nft.tokenURI);
    }
  }
}

main().catch((err) => (console.error(err), process.exit(1)));

The detailed changes are:

  • IMAGES_DIR replaces IMAGE_PATH.

  • Added listMediaFiles to gather multiple inputs.

  • Batched upload for images and for metadata (keeps order).

  • Loop to mintTo each metadataUri, parsing each Transfer event for its tokenId.

  • Names are auto-numbered: NAME #1, NAME #2, … using your existing NAME/DESCRIPTION.

Again, please use this as an inspiration for your own code, do not use as-is!

Note: If you prefer to do lazy minting, thirdweb's ERC-721 contract has a lazyMint method.

Last updated

Was this helpful?