How To Build an App NFT

Using Pinata, Polygon, and OpenSea

Justin Hunter
Pinata
15 min readSep 1, 2022

--

NFTs were games. Then they were JPEGs. Then they were interactive text-based games. Then they were utilities. Then they were keys. Then…then…then…

It’s clear that NFTs can be anything. So, as we explore a future in which NFTs are constantly evolving, it’s interesting to think long-term about what NFTs can really be. Collectibles are fun, but how do I run my digital coffee shop in a way where the value I accrue in this business can be sold as easily as I might sell a Bored Ape. The answer might be apps as NFTs. Or, more narrowly, full businesses as NFTs.

If my business is an NFT, and if I’m building real value with the business I’ve created, there is a public record of that value and there’s a potential market for me to sell that value. Where in a more traditional world, I might have to go through a materials and acquisition firm to sell my company, in web3, if my business is an NFT, it might be as easy as listing it for sale on OpenSea.

So, let’s take a look at what it might look like to create an app as an NFT. It might not be the next big business, but it’ll be a jumping off point that I hope will inspire curiosity, creativity, and boldness.

This tutorial won’t focus on building the app. Instead, we’ll use the React starter app as an example. The tutorial will focus on how you can turn that app into an NFT.

Let’s get started!

Getting Started

To complete this tutorial, you’ll need to sign up for a free Pinata account. While using a dedicated gateway will exponentially improve the experience for people who use your app (and you can even connect a custom domain to your app), we will be using Pinata’s free plan features for the tutorial to get you started.

You will also need an Alchemy account as that’s what we’ll use to handle connecting to a Polygon node, deploying our contract, and minting our app NFT.

Outside of those two accounts, all you’ll need is some good old fashion developer tools:

  • Node.js v16 or higher
  • NPM or Yarn
  • Code editor

Hardhat makes managing our blockchain interactions much easier, and OpenZeppelin is a great starting point for audited smart contracts. We’ll be making use of both, but you won’t need to do anything special beyond installing some dependencies.

This tutorial will be broken into two parts. First, we’ll get our smart contract set up. Second, we’ll link the app to our smart contract and mint the NFT. Let’s get started with part one.

Creating The Contract

The first thing we will want to do is set up our Hardhat environment and create a project. Follow the guide here for the most up-to-date documentation, but I’ll provide the steps (as of writing this) below.

The first step will be to create your project directory (this is specifically for the smart contract, not the app). Run the following from your command line:

mkdir nft-app-contract && cd nft-app-contract

We’ll need to initialize the project directory with this command:

npm init

You can answer each prompt however you’d like, and the outcome will result in a package.json file being created in your project’s directory.

Next, we’ll install hardhat as a development dependency.

npm install --save-dev hardhat

With this installed, we can make use of the hardhat CLI and we can initialize a new hardhat project.

npx hardhat

You’ll be presented with some template options for the project. I like to start with a simple starter project because it lays the groundwork. So select either “Create a Javascript project” or “Create a Typescript project”. You can hit enter through the remaining questions and accept the defaults.

This will create a folder structure that because immediately useful and has some example files. You’ll see a contracts folder, a scripts folder, and a test folder. We won’t actually use the exact files in these folders, but they help get us started.

Before we continue, let’s install OpenZeppelin so we get access to their library of smart contracts. Something to note here: OpenZeppelin is a great choice for security because their contracts are audited, but the contracts are not always the most gas efficient. Because we’re using Polygon in this tutorial, we’re not going to be focused on gas efficiency. And because this NFT is not like a 10k PFP drop, the number of on-chain transactions required will be much lower, making gas less of a concern.

Run the following command to install OpenZeppelin’s library:

npm install @openzeppelin/contracts

Now, it’s time to get down to business. Let’s write our smart contract, but let’s make sure we define what we want this contract to do. This is an app NFT. There should only ever be one owner address. That owner address might represent a multisig wallet for multiple people, but the app NFT will always just belong to one address. So, requirements:

  • One NFT minted on the contract
  • Access controls metadata mapping (this will make more sense soon)
  • Function to update the tokenURI (for updates to the app itself)
  • Mapping of app versions (IPFS CIDs representing each version)

We can begins by setting up a standard ERC721 contract. If you open your contracts folder, you will see an example contract has already been created. Let’s re-title the file to something like AppNFT.sol. Then, if you open the file, you’ll notice this contract is not anything like an NFT contract. We’ll need to make a few modifications. To streamline this, I’ve made those changes and you can see them below. We’ll walk through them next.

60 lines of code to build an app NFT smart contract. Not too shabby. Let’s take a look at what’s going on here because we made a few changes to how a standard ERC721 NFT contract works.

Starting at our constructor, you can see we do two things. We set the appOwner to the address that is deploying the contract. This is a simplified assumption, but if needed, an address could be passed in as an argument in the constructor and when the contract is deployed, the appOwner variable could be set to that address. The second thing we do is mint an NFT immediately.

Remember, this contract handles a single NFT—the app. So the mint function is actually a private function. It can only be called by the contract itself. If you look at the mint function, we once again make the assumption that the contract deployer is the app owner, but if this is not the case for you, you would again just use an argument in the contract deployment to specify the address of the app owner.

Since we’ve already started talking about the mint function, we’ll walk through what’s going on there. We have a versions variable to track all our application’s versions. We immediately increment this because the variable starts as a default of 0. Note: this is not using semantic versioning in any sense. We then set the tokenId to 1. This will be the only tokenId ever minted. We grab the current version because we’ll need it soon. Then, we mint the NFT. Next, we set our builds mapping to link the version number with the tokenURI.

That last point is incredibly important. As this is an app NFT, we need to be able to update the app. Just specifying a new version is not enough. That new version needs to point to a new tokenURI. So, with our builds variable we can track all versions historically.

Speaking of which, let’s just back up to the beginning of our contract’s functionality. There is a function called updateApp. This function takes in a new tokenURI which should represent the IPFS CID of the updated application code. We check that the person calling the function is the app owner. This is another area, where you might want to modify things. If you wanted a list of developers to be able to update the app version, you could add their addresses to an allowlist and allow them to make changes. You could even do some on-chain/off-chain magic to make this possible just the same. But for simplicity, we’re going to assume only the app owner can deploy new changes.

Back to the function. The updateApp function takes a new URI for the new version of the app, it updates the version for the app, and it maps the new version to the new URI in the builds mapping variable. It also updates the minted NFT’s tokenURI. This is crucial, because it’s the only way to let the rest of the world know that the NFT has changed.

We have just two more functions on the contract to go through. The first of those two is the getPreviousBuilds function. Anyone can see the current version number for the app. When there is a version higher than 1, it means there are older versions of the app. This means people or other apps even may be able to use older versions of the app if they wish. This function returns the URI that maps to the version passed into the function as an argument.

And the final function is actually an override of a core ERC721 function. Let’s say you end up selling this app because it has gotten extremely popular. People love that it’s an NFT, the data is verifiable, it can easily be moved, etc. Well, the ease of moving the NFT is all well and good, but everything within the code is predicated on the appOwner variable mapping to the correct owner. To ensure this happens, we override the default transferFrom function and we simply set the new owner’s address as the appOwner on the contract.

And with that, you have yourself an app NFT contract. But before we go and launch this thing and start taking over the world, we should probably test it.

If you look at your root project folder, you’ll see another folder in there called test. Open that up, and there will be one example test in it. Rename that file to something more appropriate like AppNFT.js. We’re going to replace everything in that file with the following:

We’re not going to go line by line through the test code, but just know that it’s a start. You can extend these tests as much as you’d like, test core ERC721 functions, or implement alternative variations of these tests. But as a starting point, we can use this to make sure our core functionality works.

With these tests written and saved, we can make use of the hardhat CLI to compile our contract and run the tests in memory. From the command line, in the root of the project directory, run the following command:

npx hardhat test

If all goes well, you should see something like this:

Testing has passed, let’s take a break. Get yourself a cold beverage, and when you come back, we’re going to get our application code uploaded to IPFS so that we can move forward with deploying our contract and minting our app NFT.

The Application

For the application, we’re just going to just the standard React Starter. So, from your command line, open a new window or change out of your smart contract directory. Then, run the following command:

npx create-react-app app-nft-frontend

When the install is complete, change into the directory and open it in your code editor.

We’re not going to spend time customizing the app for this tutorial. Instead, we’ll just get it into a state where we can upload the project to Pinata. And for grins and giggles, we’ll create a deploy script that will allow the project to be built and uploaded to Pinata from the command line.

So, the first thing we need to do is make it so that IPFS understands how this React app works and can render the page routing properly. To do so, all we need to update is our package.json. Open that file, and add the following key/value pair to the JSON:

"homepage": "./"

That’s it.

In order to create a script to build and deploy our app, we need to get a Pinata API key. So, log into your Pinata account and go to the API Keys page. There, you can click the New Key button. We’re going to use a CLI tool from Pinata that needs admin access, so create an admin key.

Give your key a name and then create it. You’ll see a modal with the API Key, API Secret, and the JWT. Copy down the JWT. Now, let’s install a helpful CLI tool from Pinata to upload files:

npm i -g pinata-upload-cli

This will install the CLI globally so you can use it anywhere within any project. We need to authenticate, so after the tool has installed, run:

pinata-cli -a JWT_HERE

Once authenticated, we can write our shell script to execute the upload. Go back to your React app project and open the package.json file again. In the scripts section, add a new script called “deploy” like this:

"deploy": "npm run build && sh ./upload.sh"

This script, builds the app, then it used sh to execute a shell script that we still need to write. Note: you may need to use bash to execute this, so if this doesn’t work, remove the sh and replace it with bash.

Now, in the root of the project, create a file called upload.sh. Inside that file, add this one line:

pinata-cli -u ./build

We’re using the Pinata upload CLI tool to upload our build folder. Simple as that.

Now, run the following command:

npm run deploy

First, the build script will run, then the upload CLI will upload the build folder. When things are complete, you’ll see an output that includes an IPFS CID (hash). Guess what that hash is?

That’s right, it’s going to help you make your tokenURI for the NFT.

This hash isn’t quite the token’s URI, though. We need to create a metadata file for that. We can do that in the contract directory soon.

You would likely want to create your own React app rather than use the starter project. So code away, friend. When you’re done, this script will work just the same.

Let’s finish this whole thing up by deploying our smart contract and minting our NFT.

Making an App NFT

We have the IPFS CID to use for our tokenURI. So now, we need to deploy our contract. We’ll use Alchemy to talk to a testnet version of Polygon, so log into your Alchemy account and create a new app. Choose Polygon and choose the Mumbai testnet.

Once you’ve done so, you’ll be able to click the View Keys button and you’ll see an HTTPS URL. Copy that URL, you’ll need it soon.

Open up your smart contract project directory and find the hardhat.config.js file. You’ll want to update the config section to look like this:

require("@nomicfoundation/hardhat-toolbox");module.exports = {
solidity: "0.8.9",
};
module.exports = {
solidity: "0.8.9",
networks: {
mumbai: {
url: `https://polygon-mumbai.g.alchemy.com/v2/YOUR_ALCHEMY_KEY`,
accounts: ["YOUR POLYGON MUMBAI WALLET PRIVATE KEY"]
}
}
};

Make sure to use the HTTPS URL you copied from Alchemy in the url field. You’ll notice you also need a private key. Anytime you’re testing and working on tutorials, it’s generally best to create a whole new wallet. If you end up accidentally committing the private key to Github, you’re only committing the key you made specifically for the project.

You can create a new wallet in Metamask. To export your private key from that wallet, click Account Details, then click Export Private Key. You’ll put that key in the array for accounts in your config file.

AGAIN, BE VERY CAREFUL NOT TO USE THIS KEY FOR ANYTHING REAL. ALWAYS PROTECT YOUR PRIVATE KEYS.

Once you’ve updated your config file, it’s time to get some testnet Matic. You can do that here.

Remember, we need to create the metadata file for our NFT. The metadata is what will describe the NFT to the world. It links everything together. This is where you can include pretty much anything you want that will be of importance for your app NFT. There are a couple of things we absolutely must include though:

  • Name
  • Descriptions
  • Image
  • Animation URL

The first three are pretty straightforward. They are the things you would expect to need if you were submitting an application to an app store or promoting it somewhere. The same is true for app NFTs. You need to provide info so people can discover and understand what it is.

That fourth property, though, what is that? That property is something OpenSea first started recognizing and is part of their NFT metadata standards. This is how we will ensure our app NFT (in a mainnet environment) would show up and be usable within OpenSea itself. This is where we will put the app’s IPFS CID (the hash we receive when building and uploading our app to Pinata).

Create a file in the root of your contract project directory called metadata.json and add the following:

{
"name": "App NFT",
"description": "A full application, accessible as an NFT, sold as an NFT, and transferred as an NFT",
"image": "ipfs://CID_FOR_IMAGE_REPRESENTING_YOUR_APP",
"animation_url": "ipfs://HASH_FROM_APP_BUILD"
}

For the image property, find an image that represents your app and upload it to Pinata. Then, you can fill the image value with ipfs://CID_FOR_THAT_IMAGE.

Now, let’s use our handy Pinata upload tool to upload our metadata file.

Run this in your command line from the root of your contract project directory:

pinata-cli -u ./metadata.json

Hang on to the hash from the upload. We’ll need it in a second.

We now have everything in place to write our deploy script and deploy our contract and mint our app NFT. The deploy script should be pretty straightforward since it’s just a variation of the deploy code you already wrote for your test cases. We also have an example file in our scripts folder and, would you look at that, it’s called deploy.js. Let’s modify that file to look like this:

const hre = require("hardhat");
const URI = "ipfs://METADATA_CID"
async function main() {
const AppNFT = await ethers.getContractFactory("AppNFT");
const appNft = await AppNFT.deploy(URI);
await appNft.deployed(); console.log(`Contract deployed to ${appNft.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

The hash you copied can be put where the METADATA_CID placeholder is. And now, you’re ready to deploy. Let’s do a test run with Hardhat’s in-memory Ethereum EVM. To do so, we just need to run the following command:

npx hardhat run scripts/deploy.js

No gas will be spent because this is just a local testnet on your computer that you are deploying to (and it’s only in memory). The result of that should look something like this:

Contract deployed to 0x5FbDB2315678afecq367f032d93F642f64170aa3

Everything’s looking good. Assuming you got your testnet matic already and followed the hardhat config file instructions above, you’re ready to deploy your smart contract! Let’s modify our deploy command just a bit. Run the following:

npx hardhat run scripts/deploy.js --network mumbai

When this is done, you will see the same output as before but with a different contract address. The other difference is this contract was actually deployed to the Polygon Mumbai testnet! You can verify it here. Search for the contract address there and you’ll see that the contract was deployed. You’ll also be able to click into the deployment transaction and see that the one and only NFT on this contract was minted.

We just launched an app NFT! All that’s left now is to go see it on OpenSea. We’ll need to view this from their testnet site. If you go here and connect your Metamask using the same wallet you used to deploy the contract, you’ll be able to easily find your NFT. Click the profile button in the top-right, then click My Collections. It should automatically be there. Here’s what mine looks like:

And when I click on the collection and click into the one minted NFT, I see this:

As you can see, I didn’t customize the React starter app, but assuming you did, you’ll see a much cooler app. This app is an NFT. It works (minus chrome extension access) right within OpenSea, but it can also work by visiting the app’s IPFS CID through an IPFS gateway.

Wrapping Up

For performance and stability, I highly recommend considering a paid Pinata plan so that you can create a Dedicated Gateway. A Dedicated gateway will give you a global CDN which means your app will load quickly every time, which is especially important when it goes viral.

This was a simple example of an app NFT, but hopefully is shows you what’s possible. Imagine a multi-million dollar business being created this way. Imagine being able to sell the business just by listing this app NFT on OpenSea. A whole new world of opportunity and creativity can be unlocked if we allow ourselves to think about NFTs as more than just images.

--

--

Justin Hunter
Pinata

Writer. Lead Product Manager, ClickUp. Tinkerer.