- Creates a Music NFT using a custom ERC721 contract (via OpenZeppelin Wizard) with metadata pointing to audio and cover files on IPFS
- Deploys contract with Hardhat to testnet; verifies it on Etherscan
- Pins the audio and cover image plus NFT metadata using Chainstack IPFS Storage
- Mints the NFT from your wallet, allowing you to see your music NFT on MetaMask/OpenSea with embedded audio
Main article
In the digital age, artists are constantly seeking innovative ways to share, monetize, and protect their work. Enter the world of non-fungible tokens (NFTs)—a revolutionary way for musicians to create, sell, and own their masterpieces. As the music industry evolves, NFTs are becoming the driving force behind a paradigm shift in how artists generate income and maintain control over their creations. The music industry has always been a hotbed of innovation and change, from vinyl records to streaming services. Now, the advent of blockchain technology and the rise of NFTs are opening up an entirely new frontier for musicians and music enthusiasts alike. With the potential to revolutionize how we create, own, and trade music, NFTs are rapidly becoming an essential tool for artists looking to embrace the digital era. But how does one go about minting a music NFT? If you’ve been wondering about this very question, you’ve come to the right place. In this comprehensive guide, we’ll walk you through the process step by step, demystifying the world of music NFTs and empowering you to join the revolution. So, whether you’re an aspiring musician, a dedicated collector, or simply curious about this exciting new development, read on and get ready to make some noise in the NFT space.How to mint a music NFT?
Minting NFTs through coding can be slightly more challenging than relying on a marketplace platform to handle the process for you. However, it provides you with greater flexibility and allows you to engage with the core mechanics of minting. Let’s explore how you can get started:Step 1: Get started with the basics
1.1: Obtain a node endpoint
Running your own node can be a time-consuming process. Instead, you can quickly and effortlessly deploy a node with Chainstack, saving both time and effort. To get started, visit the Chainstack website, create a free account, deploy a Sepolia node, and obtain its HTTPS endpoint. Don’t worry about your testnet choice, the code base in this tutorial is structured in a way that will allow you to switch between Sepolia, and Mainnet freely, without having to rewrite everything. If you’re unsure about the process, follow the steps in our easy-to-understand quickstart guide.1.2: Install core dependencies
If you haven’t installed node.js yet, go ahead and do so. Once that’s done, you’ll need to set up a workspace for your codebase. Create a new directory in your preferred location and initialize your node.js project by entering the following commands in your CLI (command line interface):The
-y flag indicates that all the default values should be used without prompting the user for input.web3 and hardhat-verify plugins, which will provide you with the functionality required to interact with your node, as well as verify your deployed contract:
hardhat.config.js file in your project root. Open it and replace the contents with the following:
1.3: Securely store your secrets
To securely store all values, which are best left away from prying eyes, such as yourendpoint you’ll utilize a .env file. But before you set off to create such a file, go ahead and install the dotenv package using npm via CLI:
.env file in your project root and transfer over your endpoint URL there as the first key-value pair. If you want to use all three Ethereum networks (Sepolia, and Mainnet) you can set it up like so:
SEPOLIA, or MAINNET value from your .env file in any script in your project that has the dotenv dependency processed appropriately.
So, go ahead and break the ice by adding require('dotenv').config(); in your hardhat.config.js script as early as possible. Referencing dotenv before any and all other dependencies prevents possible conflicts, which may arise due to an inappropriate loading order.
Proceed by initializing each network with its corresponding endpoint in hardhat.config.js as the values for the url keys, using the dotenv process.env method. You can also set a PRIVATE_KEY value for the accounts keys already, considering you will be getting a new one in the next section. Here’s how your hardhat.config.js should look like, once you’ve set everything up correctly:
.env file to any public repository, as it will serve as a container for all your sensitive information moving forward.
Apart from your endpoint URL, you will also store things like your private key inside the .env file, which could expose your entire wallet if leaked.
Having this in mind, go ahead and create a .gitignore file if you don’t have one already, so you can add .env to it before you publish. Refer to the following example for how you can do that:
1.4: Create a new wallet and fund it
Once the initialization is complete, create a new/scripts directory and add a new wallet.js file in it. When you’ve created the new script, include the following lines of code to initialize the necessary dependencies:
web3.eth.accounts.create();, just make sure you return the address and privateKey values at the end of your async function:
async function that will fund your wallet with testnet ETH from the Chainstack faucet. To do this you will first need to take care of a few things:
-
Create a Chainstack API key and copy it, which will be similar to
Bearer y0urChainstackAPIkeyHer3, then store it in your.envfile asCHAINSTACK: -
Install axios library to be able to send HTTP requests to the faucet:
-
Require
axiosat the end of yourwallet.jsdependencies like so:
fundWallet function, by adding address and apiKey as its required parameters. Next, create a new const called apiUrl, add the faucet API URL, and place ${network.name} at the end of it to be able to select the right testnet version:
Using the Hardhat
network.name variable allows you to switch between networks with the --network parameter whenever you are running a particular script with Hardhat.fundWallet function, it is time for you to close it off with a try-catch loop that will contain your axios request to the Chainstack faucet. Inside the try part of the loop, add a new const called response, where you will set the axios settings and return the response:
POST request to the Chainstack faucet apiURL with your address, your apiKey as the Authorization header, and application/json as the Content-Type.
And with the try part of the loop taken care of, you can wrap up the entire thing by catching and then throwing an error, should it occur. Here’s how your wallet.js script should look like at this point:
wallet.js script—some of the variables are not set yet and neither are the calls that will run the two functions you have set. So, go ahead and create them but do so in a new async function called main.
And while you’re at it, why not add some visual feedback to make it easy for you to process their results. Since there is little new to learn with this function, let’s straight up recap with the full wallet.js script until now:
wallet.js script fully set up, the time has come for you to launch it via CLI using Hardhat:
.env file as values for the WALLET and PRIVATE_KEY keys. You can also use a pre-existing wallet address with the appropriate private key attached to it, by setting them in your .env file instead.
1.5: Verify your wallet balance
Now it’s time to verify if your balance has been updated. Create a new file namedbalance.js within the /scripts directory and copy over the first two dependencies from your wallet.js script, without adding the one for axios.
Then, create a new address constant and set its value to the WALLET you just copied in your .env file, using process.env.WALLET:
address constant as a parameter and within it call the web3.js getBalance method.
Since it will take more time for your node to process this request than that of executing the rest of the function’s code locally, make sure you add await prior to the method:
getBalance method will return a very barebones balance value in wei, so go ahead and add some extra visual feedback by calling the web3.js fromWei method to convert the wei output to ETH units.
Once ready, go ahead and wrap things up by calling your getbalfunction with the address parameter at the end of your script:
--network parameter:
Step 2: Prepare and deploy the smart contract
2.1: Draft an NFT smart contract
Now that you’ve completed all the necessary preparations, it’s time to create your smart contract. While this might seem intimidating at first, you can rest easy knowing that OpenZeppelin provides pre-built, security-audited contract templates. The cherry on top? You can use their wizard to create a customized contract that perfectly fits your needs. For our sample project, we will be using the following settings:Figure 1: music NFT project settings
Mintable option with Auto Increment Ids enabled. This feature allows privileged accounts (e.g., your account) to mint new tokens, which can represent new additions to your collection.
We also need to enable the URI Storage option, as it allows us to attach media files like images to our NFTs. Additionally, we’ll incorporate the Ownable option to enable administrative actions.
While there are more parameters available in the wizard, they fall outside the scope of this tutorial. However, don’t hesitate to experiment with them if you wish to explore additional functionalities beyond the basic ones we’ve implemented so far.
Finally, ensure that the OpenZeppelin dependencies are available in your project by installing its contracts library with the following:
2.2: Compile the minter smart contract
With your contract ready, copy the code from the OpenZeppelin wizard into a new file, such asMyMusicNFT.sol, or click Download in the top-right corner. Then, create a new directory called contracts in your project root and place your smart contract in it.
Hardhat automatically compiles a new contract when it needs it, without additional input from your end but you can also do that manually using
npx hardhat compile. This will compile all contracts located in the contracts directory, so if you have more than one, they will all be process accordingly.- Create an Etherscan account and API key, so you can verify the contract once it is deployed.
-
Save the API key in your
.envfile as value for theETHERSCANkey: -
Create a
deploy.jsscript in your/scriptsdirectory and add the following:
fs and path, both of which ship by default with node.js.
The former, fs, is a file system module, that allows you to interact with local files, while the latter is used for handling and transforming file paths.
As the next step, create a few constants called contractName, artifactPath, contractArtifact, contractABI, and contractBIN. The first one will be used by Hardhat to determine which contract you will be interacting with, while the second and third to locate and read the compiled contract artifact. This artifact contains your smart contract’s ABI and its bytecode , or BIN.
ABI an BIN
The application binary interface (ABI) facilitates interaction between software modules, translating Solidity contract calls for Ethereum’s Virtual Machine (EVM) and decoding transaction data. On the other hand, bytecode (BIN) is the binary output of Solidity code and consists of machine-readable instructions including one-byte “opcodes”, hence the name.ABI and BIN is automatically generated when you compile it with Hardhat as /artifacts/contracts/YourContractName.sol/YourContractName.json, where YourContractName is the name of your contract. In this tutorial’s case, it is MyFirstMusicNFT, so go ahead and set the contractName constant’s value to your smart contract’s actual name.
To help your code discover the location of the artifact, you can use the path.resolve method with __dirname as the first parameter, and the JSON path mentioned in the paragraph above, preceded by .. to indicate the parent directory.
About __dirname
In node.js, __dirname is an environment variable that gives the absolute path to the directory of the currently running file. Unlike ./, which denotes the current directory of a file, or ../, which refers to its parent directory, __dirname always points to the precise directory where the executing file resides.fs.readFileSync method as the first parameter but make sure you have set utf-8 as the encoding type in the second. Lastly, pass the entire thing as a JSON object by wrapping it with JSON.parse. Here’s how this part of your script should look like:
2.3: Deploy your smart contract
Next, it’s time for you to put together the rest of thedeploy.js script by adding a new function to handle the actual deployment process.
To do that, define a fresh asynchronous main function first and create a new contract object constant contractNFT by calling the web3.js Contract method with contractABI and address as parameters, respectively.
Then, deploy the contractNFT contract object as a constant contractTX transaction object by applying the deploy method to it with a data parameter set to contractBIN.
contractTX. To do that, just slap the web3.js estimateGas method at the back of the contractTX object, and add some relevant visual feedback.
Then, continue by calling the web3.js signTransaction method with two parameters, the first being an object, and the second—privKey. Compose the object parameter using the key from set to address, data as contractTX with the encodeABI method applied to it, and gasCost as value for thegas one as the third.
createReceipt constant with value calling the web3.js sendSignedTransaction method with a single parameter, preceded by an await operator.
Set the createTransaction constant as a value for it, apply the rawTransaction method to it, and then add some visual feedback to display the receipt as the closing statement for the entire deploy function. Don’t forget to run it either!
getBlockNumber method in a given interval, so just copy over the following outside the main function loop:
verifyContract function and inside it call the waitForBlocks and run("verify:verify"), while waiting for them to execute first.
Make sure to also add the address and constructorArguments parameters, with createReceipt.contractAddress as value for the former and a blank array [] as one for the latter:
deploy.js script via CLI with npx hardhat run scripts/deploy.js --network $NETWORK to have your very own smart contract deployed on the testnet of your choice.
But still, the steps that led you here were indeed a tad bit more complex than what you had done so far in this tutorial, so let’s recap by reviewing the entire deploy script’s code:
Step 3: Pin the metadata and mint your NFTs
3.1: Getting started with Chainstack IPFS Storage
Before you can mint any NFT, you will first need to pin all relevant media files to the Interplanetary File System (IPFS). Thanks to this, you won’t have to rely on the availability of any centralized provider, instead having it permanently accessible. In IPFS, uploading is adding a file to the network, while pinning is ensuring its persistent availability by preventing its removal on a specific node. To make the entire process a real walk in the park, you can use Chainstack IPFS Storage, which will provide a seamless interface and API for you to do that. You can use the interface to create the bucket and folder that you need to pin the files manually, or follow this process to do so via the API:- Sign in to your Chainstack account via the console and select IPFS Storage from the navigation on the left.
- Click the Create bucket and enter a name of your choice.
- Inside the bucket, click New folder with a name of your choice.
- Open the folder and bucket and examine the URL in the address bar for each of them.
- Copy the bucket ID starting with
BUCK, for example,BUCK-1337-8085-1337. - Copy the folder ID starting with
FOLD, for example,FOLD-1337-8085-1337. - Paste the three values in your
.envfile for theBUCKET_ID,FOLDER_ID, andCHAINSTACKkeys like so:
3.2: Pin your NFT media with Chainstack IPFS Storage
With the prerequisites taken care of, it is time for you to pin the media files with Chainstack IPFS Storage. In the tutorial repo, you will find a tutorial audio file and cover image in thesrc directory that you can use freely to test the waters.
Should you prefer to pin them with Chainstack IPFS Storage via the interface manually, you can do so already and jump to the next step, where you will set up the JSON metadata. Otherwise, it is time for you to create a new pin.js file to do that via the API.
To get started with the new pin script, process the dependencies first. You will need the dotenv, fs, axios, and the FormData packages, the latter of which you can install with the following command:
addFiles function as a constant with source and single = false as the two parameters it seeks:
single = false will serve to differentiate between pinning single and multiple files, as the API URLs for them are not the same. Set up the differentiation in the code like so:
data constant with a value equal to new FormData() and follow up with an if-else statement, checking for the state of the single parameter:
if statement, add some visual feedback to display which file you are attempting to pin by using the JSON.stringify method with the source[0].title value. Afterward, add the bucket_id, folder_id, file, and title to the data object via the append method.
You can load the bucket_id and folder_id values from your .env file by setting them as the first parameter for the append method and using process.env.BUCKET_ID and process.env.FOLDER_ID, respectively as the second.
In turn, do so for the file and title values but referencing source[0].file and source[0].title accordingly:
else part of the statement, the only difference being that you need to use the forEach method to make sure all entries are referenced correctly:
config constant, which will set up the axios configuration. Here you will also make a reference to your Chainstack API authorization token in a fashion similar to this:
response constant which will store the axios response, once again differentiating between single and multiple files:
addFiles function successfully set up, it is time to do so for the findCIDs one. Its purpose will be to obtain the content IDs (CIDs) of the files you have pinned, which are unique identifiers that allow universal access via any IPFS client.
So, go ahead and create it as an asynchronous function by defining a new findCIDs constant and set fileID and single = false as the two parameters it would accept. Then, proceed by removing possible excess characters like so:
CID will not be ready when your script moves on to find them, so let’s set up a redundancy process that will automatically retry the search.
Start by defining a new maxRetries constant, as well as a retryTimeout one, setting the former’s value to 5 and the latter to 22000. Simply put, this will make the function retry for a maximum of 3 times with an 11-second timeout between each retry.
Next, create an if-else statement with the !single parameter in the if part. Inside it, create a new cid and name temporary array variables. After that, create a for loop that will use the push method to store the CID and title values of the freshly pinned files:
findCIDs function should look like in the end:
writeJSON function as a constant with the pinCID and pinName parameters as the ones it should accept.
Create new temporary audioIPFS and coverIPFS variables and then an if-else statement with pinCID && pinName as its parameters.
Inside the if statement, create a for loop which will piece together the appropriate URLs of your media pins:
ipfsgw.com which will make your pins available faster, however, it is generally recommended to use ipfs:// for truly universal access, even if it takes much longer to propagate this way.
That being said, finish off the rest of the function by writing the metadata you collected earlier to a JSON file in the src directory. The entire writeJSON function should be set up in a fashion similar to this:
pinNFT function that will queue all the relevant functions in the correct order to have your media files and metadata JSON ready for minting:
pin.js script via CLI, so you can watch the fireworks!
If the script returns Error during NFT pinning: Request failed with status code 400 as response.
If you encounter this error, there is some degree of duplication with existing files you have pinned previously.To resolve it, you must either set new filenames and titles for the interrupted pins, or delete the previous ones.Do note that once you delete them, any previously minted NFTs using them as source for metadata will no longer display correctly and will be left as blank containers permanently.pin.js code:
3.3: Create the script for minting
By the time you reach this step, you should have successfully pinned your NFT media files with Chainstack IPFS Storage, and have their CIDs referenced in a JSON file that was also pinned there. The JSON file must contain animage key to store the NFT cover, an animation_url one for the audio file, name for the track title, and optionally a description, as well as an external_url for a link to your profile for example. It should look similar to this:
mint.js file inside the scripts directory for the minting script. Begin by processing the dependencies for the dotenv, hardhat-web3, and fs modules.
Proceed by initializing your wallet address, private key, deployed smart contract ABI, and JSON metadata URL, as well as the appropriate deployed contract address for each network via a simple if loop:
from parameter it will return an error.
scripts/deploy.js using the web3.js estimateGas method with the safeMint method of your contract as its target:
startMint function as a constant. Inside it, start by adding some visual feedback for the mint address target and the estimate response like so:
signTransaction method, using as the first parameter an object with the address value for the from key, the contractAdrs one for to.
Then, for data, call the contract object’s safeMint method with the encodeABI method attached to it, and gas set to gasCost. Use the privKey constant as the second parameter for a final result like this:
createReceipt function like the following and call the startMint function to truly make the mix complete:
mint.js script with Hardhat using npx hardhat run scripts/mint.js --network NETWORK_NAME, you should now have minted the first music NFT in your collection!
To add more NFTs, simply follow the process to create a new JSON file with different parameters and rerun the relevant functions, making sure to select the appropriate JSON file for the metadata variable in the mint.js function.
3.4: View your NFTs on MetaMask and OpenSea
If you’ve managed to complete the previous steps, this one should be quite simple. Begin by opening your MetaMask wallet or downloading it if you haven’t already. Log in and choose the Sepolia network at the top. Click the icon in the top right corner (not the MetaMask logo) and select Import Account. Opt for Private Key, as that’s how you initially set up your address, and paste it into the designated field. Afterward, head to the NFTs tab and select Import NFTs at the bottom. Paste your music NFT contract’s address into the first field, and the token ID in the next. If it’s your first mint the token ID will be 0. Finish the process by clicking Add, and there you have it—your music NFT is now visible in MetaMask!
Figure 2: Music NFT on MetaMask; Source:

Figure 3: Music NFT on OpenSea; Source:

