How To Create Generative Art NFTs

Non-Fungible Tokens, or NFTs, are digital tokens on a blockchain, each of which represents something unique, such as a piece of digital art, a special in-game item, a rare trading card collectible, or any other distinct digital/physical asset. NFTs stand in contrast to fungible tokens: Each is unique and cannot be swapped for another version of itself. Holders care about which token they hold rather than how many.

In this technical tutorial, you’ll learn how to develop a generative art NFT collection and host it on IPFS. Your NFTs will be different dog breeds. The generative art for the NFTs will be generated using verifiably random numbers from Chainlink VRF.

Let’s get started.

Generative dog NFTs

Clone the Repo

The first step is to clone the Chainlink Smart Contract Examples repository. Once you’ve done this, navigate to the “The Ultimate NFT Repo” directory and install the necessary dependencies.

git clone
cd smart-contract-examples/ultimate-nft-repo


Then, go ahead and open the project in your code editor of choice. Follow the instructions for setting the required environment variables in the project’s “Readme” file (you will need to sign up for a free Alchemy account and for a free Etherscan API key). In this tutorial, we’ll deploy to the Sepolia testnet on Ethereum.


Using Chainlink VRF v2

To get the random values we’ll need to power our generative art NFTs to the blockchain we’ll use the recently released Chainlink VRF v2. The new version of VRF includes several improvements to how smart contract randomness is requested and funded.

To start, navigate to the VRF subscription page, select the Sepolia network, connect your wallet, and click “Create subscription”. Then, save your subscriptionId—we’ll need to pass it as a constructor argument later.

Once you’ve finished with the development and deployed your smart contract to Sepolia, head back to the VRF subscription page, navigate to your subscription, click the “Add consumer” button, and paste the address of the recently deployed contract. 

Finally, fund your subscription with a couple of Sepolia test LINK tokens. You can get them at

Developing the NFT Smart Contract

Create a new Solidity file called RandomIpfsNft.sol. We will inherit a couple of smart contracts from the OpenZeppelin library and also use Chainlink VRF. 

Solidity version 0.8.4 introduced so-called “custom errors,” a more gas-efficient way to report errors from the codebase. While it was already possible to use strings to provide more information about failures, this is rather expensive, and it is difficult to use dynamic information in them.

The syntax of custom errors is similar to that of events. They are defined using the error statement and have to be used together with the revert statement. require is not supported.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";

error AlreadyInitialized();
error NeedMoreETHSent();
error RangeOutOfBounds();

contract RandomIpfsNft is ERC721URIStorage, VRFConsumerBaseV2, Ownable {
    // Types
    enum Breed {

    // Chainlink VRF Variables
    VRFCoordinatorV2Interface private immutable i_vrfCoordinator;
    uint64 private immutable i_subscriptionId;
    bytes32 private immutable i_gasLane;
    uint32 private immutable i_callbackGasLimit;
    uint16 private constant REQUEST_CONFIRMATIONS = 3;
    uint32 private constant NUM_WORDS = 1;

    // NFT Variables
    uint256 private i_mintFee;
    uint256 public s_tokenCounter;
    mapping(uint256 => Breed) private s_tokenIdToBreed;
    uint256 internal constant MAX_CHANCE_VALUE = 100;
    string[] internal s_dogTokenUris;
    bool private s_initialized;

    // VRF Helpers
    mapping(uint256 => address) public s_requestIdToSender;

    // Events
    event NftRequested(uint256 indexed requestId, address requester);
    event NftMinted(Breed breed, address minter);

We also need to deploy dog images to IPFS and store their URLs inside the contract. To do that, navigate to Pinata, and sign up for free account. Then go to your files tab, click “Upload”, and select the file or folder to upload.

To make sure that the smart contract will be deployed correctly, let’s add a constructor function and use “Random IPFS NFT” as the collection name and “RIN” as the ticker. Feel free to change these values and name your collection whatever you like.

Now we need subscriptionId, which we saved after registering on the VRF subscription page, along with the IPFS URLs of dog images. Finally, grab the other necessary VRF parameters from the official documentation.

        address vrfCoordinatorV2,
        uint64 subscriptionId,
        bytes32 gasLane, // keyHash
        uint256 mintFee,
        uint32 callbackGasLimit,
        string[3] memory dogTokenUris
    ) VRFConsumerBaseV2(vrfCoordinatorV2) ERC721("Random IPFS NFT", "RIN") {
        i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinatorV2);
        i_gasLane = gasLane;
        i_subscriptionId = subscriptionId;
        i_mintFee = mintFee;
        i_callbackGasLimit = callbackGasLimit;

    function _initializeContract(string[3] memory dogTokenUris) private {
        if (s_initialized) {
            revert AlreadyInitialized();
        s_dogTokenUris = dogTokenUris;
        s_initialized = true;

Now let’s add a method for minting new NFTs. The user needs to provide at least an i_mintFee amount of native coins, otherwise the transaction will revert. Our method will then request a random value from Chainlink VRF. Since Chainlink VRF is asynchronous, we will use s_requestIdToSender to map all Chainlink VRF requests to the minter of the token.

After the callback from the Chainlink decentralized oracle network, the fulfillRandomWords function will determine the dog’s breed and owner and mint a fresh NFT.

function requestNft() public payable returns (uint256 requestId) {
        if (msg.value < i_mintFee) {
            revert NeedMoreETHSent();
        requestId = i_vrfCoordinator.requestRandomWords(

        s_requestIdToSender[requestId] = msg.sender;
        emit NftRequested(requestId, msg.sender);

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
        address dogOwner = s_requestIdToSender[requestId];
        uint256 newItemId = s_tokenCounter;
        s_tokenCounter = s_tokenCounter + 1;
        uint256 moddedRng = randomWords[0] % MAX_CHANCE_VALUE;
        Breed dogBreed = getBreedFromModdedRng(moddedRng);
        _safeMint(dogOwner, newItemId);
        _setTokenURI(newItemId, s_dogTokenUris[uint256(dogBreed)]);
        emit NftMinted(dogBreed, dogOwner);

    function getBreedFromModdedRng(uint256 moddedRng) public pure returns (Breed) {
        uint256 cumulativeSum = 0;
        uint256[3] memory chanceArracy = getChanceArray();
        for (uint256 i = 0; i < chanceArracy.length; i++) {
            if (moddedRng >= cumulativeSum && moddedRng < cumulativeSum + chanceArracy[i]) {
                return Breed(i);
            cumulativeSum = cumulativeSum + chanceArracy[i];
        revert RangeOutOfBounds();

    function getChanceArray() public pure returns (uint256[3] memory) {
        return [10, 30, MAX_CHANCE_VALUE];

It’s good practice to make your storage and immutable variables private, mark them with prefixes “i_” & “s_”, and write getter functions. This approach will make your code much cleaner and more efficient. So let’s do it:

function getMintFee() public view returns (uint256) {
        return i_mintFee;

    function getDogTokenUris(uint256 index) public view returns (string memory) {
        return s_dogTokenUris[index];

    function getInitialized() public view returns (bool) {
        return s_initialized;

    function getTokenCounter() public view returns (uint256) {
        return s_tokenCounter;

Since minting this collection carries a cost of native coins, which we specified as the i_mintFee variable inside the constructor, the last functionality to implement is actually a method for enabling an admin DAO multisig wallet to withdraw those locked funds.

Since 2019, the call method has been the preferred way to send native coins in Solidity. This process is described below. For security reasons, avoid using transfer and send.

function withdraw() public onlyOwner {
        uint256 amount = address(this).balance;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Transfer failed");

That’s it, our NFT is ready to be deployed! Don’t forget to add this smart contract as a consumer of your VRF subscription after deployment.

Mint Your Tokens and Trade Them on OpenSea

Now you can easily mint your tokens, either by connecting your wallet to Etherscan and clicking on the “requestNft” function or by creating a dApp UI for interacting with your smart contract. After minting, go to OpenSea on Sepolia and search for your NFT collection or wallet address.


In this article, you’ve learned how to create a generative art NFT smart contract, and we’ve also covered Solidity custom errors, using Chainlink VRF, deploying files to IPFS, and safely sending native coins in Solidity. 

To learn more, head to the Chainlink Smart Contract Examples repository and start experimenting with this and the other example projects. If you’re aiming to build your own NFT project, explore five key steps necessary to building a successful NFT project.

A clickable link to a guide on how to build a successful NFT project.
A downloadable guide to building a successful NFT project.

Learn more about Chainlink by visiting or reading the documentation at To discuss an integration, reach out to an expert.

Need Integration Support?
Talk to an expert
Get testnet tokens
Read the Docs
Technical documentation