Create game assets using the preset contract
Learn how to create Web3 assets for your game in the form of ERC721 tokens. This guide will teach you how to create, deploy and mint your first NFT collection.


This is intended to be a beginner guide - we'll take you through the process step-by-step so don't worry if you're new to this. If you'd like to familiarise yourself further with the concepts of minting, ERC721 smart contracts and gas fees, see our section on Minting.
In general, when deploying contracts on Immutable zkEVM, we strongly recommend users deploy our recommend contract presets.
Install developer tools
There are a few tools required for this tutorial:
(For macOS users) Homebrew
Homebrew is a package manager allows you to easily install the packages needed for this tutorial. Go to the Homebrew website for instructions on how to install and use it.
Node.js
Node.js allows us to use JavaScript to build and run applications.
...or you may experience issues following this tutorial.
- Windows
- macOS
For Windows users, you can verify that Node.js is properly installed and working by opening PowerShell or the Command Prompt and running:
npm -v
For Mac users, open up the terminal and run:
brew install node
Visual Studio Code
Visual Studio Code is a code editor we will be using for this tutorial. It is known for its user-friendly interface and extensive extension library.
Get a wallet that supports Ethereum-based assets
As our Minting article explains, minting NFTs on zkEVM requires gas. Test $IMX will be used as gas on Immutable zkEVM Testnet in this example. Content creaters are required to hold Test $IMX in a wallet in order to mint an NFT (and to deploy the smart contract).
For this tutorial, we will use Metamask as our wallet. Other wallets can be used for this purpose.
Download Metamask wallet and store seed phrase safely
- Download the Metamask browser wallet
- Follow the steps in the plugin to create a new wallet, then record and store your seed phrase in a safe location
Connect to Immutable's zkEVM testnet
- Once you have installed the Metamask browser extension, click on it to open.
- Click on the network dropdown:
- Select
Add network
: - Select
Add a network manually
at the bottom of the screen and fill in the following details:- Network name:
zkEVM-testnet
- RPC URL: https://rpc.testnet.immutable.com/
- Explorer URL: https://explorer.testnet.immutable.com/
- Chain ID:
13472
- Currency symbol:
tIMX
- Network name:
- Save this and ensure that your selected network is the Immutable zkEVM Testnet
Fork the boilerplate repository
Immutable's zkevm-boilerplate
repository provides a standard set of libraries and uses Immutable's pre-set contracts. It is your starting point for deploying Immutable's recommended ERC721 Preset contract and familiarising yourself with the zkEVM blockchain.
Follow these Github guides on how to clone a repository:
At the end of this process you should have a copy of the repository installed locally on your machine with the path ../{YOUR_GITHUB_USERNAME}/zkevm-boilerplate
Once you have the repository downloaded onto your local machine, run the following command from the root directory:
npm install
Configure Hardhat
Update your project's hardhat.config.js/ts
file with the following code.
This file is located in your smart contract repository.
const config = {
solidity: {
version: '0.8.17',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
};
Create a project and install the Immutable SDK
Create project
Create a folder for your project and go to the root directory:
mkdir myproject && cd myproject
Initialize a JavaScript project:
npm init
# After running `npm init`, it will ask you for a bunch of values.
# You can leave the fields empty or run the following which uses the defaults:
npm init -y
Prerequisites
The Immutable SDK requires Node v18 (Active LTS version) or higher. Node v18 can be installed via nvm
which allows to quickly install and use different versions of node via the command line.
The installation steps for nvm
for Linux, Mac, and Windows Install & Update Script. You are now ready to install the Node LTS version:
nvm install --lts
Install the Immutable SDK
Run the following command in your project root directory.
- npm
- yarn
npm install -D @imtbl/sdk
yarn add --dev @imtbl/sdk
The Immutable SDK is still in early development. Should complications arise during the installation, please use the following commands to ensure the most recent release of the SDK is correctly installed:
- npm
- yarn
rm -Rf node_modules
npm cache clean --force
npm i
rm -Rf node_modules
yarn cache clean
yarn
Dependencies
- npm
- yarn
npm install -D typescript ts-node
yarn add --dev typescript ts-node
Add metadata
Set up a metadata hosting service
The contract we are able to deploy will specify the location of where the collection's metadata is hosted. If you are unsure as to what metadata is, or why we recommend it to be hosted off-chain, please see our section on Metadata.
When using Pinata, you should be aware of the following:
- Hosting images on IPFS means that they will be publicly accessible on a decentralized network, enabling anyone to view and access them
- Pinata is a public gateway to IPFS and it may be slower to respond because of increased network congestion and limited resources on the publicly accessible server
- Immutable's requests are built to time out after 5 seconds. As such, it is not advisable to use Pinata in production.
- For better performance, we recommend using services like AWS S3.
Pinata is just one of several providers that allows users to host files on IPFS. Metadata is often hosted on IPFS networks, however there are pros and cons depending on your project's needs.
One of the pros of IPFS storage is that the integrity (immutability) of the file can be verified as each image is represented by a unique content ID (CID). Additionally, as it is a decentralized network, your image is hosted by several nodes, further ensuring its availability.
Follow the steps below to prepare and upload your collection's metadata to Pinata:
- Sign up for Pinata
- Prepare one image for your NFT and another image to represent the entire collection
- Upload the images by pressing
Add Files
and selectingFile

- Note down the URL for the files by clicking on the eye icon

Your URL should have the format: https://peach-dear-wildfowl-698.mypinata.cloud/ipfs/{UNIQUE_HASH}
Create metadata
We will now be creating metadata for your collection.
Please note the following distinction:
- Contract metadata is information about the entire NFT collection, such as its
description
andname
. A collection can have many NFTs. - NFT metadata is characteristics and properties of the specific NFT you intend to mint. They represent the unique attributes of a particular token.
Define and upload your collection and NFT metadata to Pinata with the following steps:
- Create folders and files by running these commands in your terminal:
mkdir -p myproject/contract-metadata
touch myproject/contract-metadata/contract.json
mkdir -p myproject/nft-metadata
touch myproject/nft-metadata/1
touch myproject/nft-metadata/42
touch myproject/nft-metadata/340282366920938463463374607431768211456
This will create files in following tree structure:
myproject
├── contract-metadata
│ └── contract.json
└── nft-metadata
└── 1
└── 42
└── 340282366920938463463374607431768211456
[int]
.The collection metadata file can contain any values as long as it is in JSON format.
- In the folder
nft-metadata
populate the1
file with the metadata for the corresponding NFT, using the following example. Make sure to replace eachimage
with the URLs that you created earlier to host your NFT images.
{
"id": 1,
"image": "<replace this with your own IPFS link from step #1>",
"token_id": "1",
"background_color": null,
"animation_url": null,
"youtube_url": null,
"name": "Test zkEVM NFT",
"description": "This NFT is my first zkEVM NFT created on Immutable",
"external_url": null,
"attributes": [
{
"trait_type": "Hat",
"value": "Top Hat"
},
{
"trait_type": "Coat",
"value": "Tails"
},
{
"trait_type": "Neck",
"value": "Bow Tie"
}
]
}
Copy the contents of the
1
file into the42
and340282366920938463463374607431768211456
files, changing the values of"id"
to match.In the folder
contract-metadata
populate thecontract.json
file with the metadata for the collection, using the following example. Replaceimage
with the URL for the specific NFT image you uploaded when you created your collection metadata.
{
"name": "My collection",
"description": "Some description",
"image": "<replace this with your own IPFS link from step #1>",
"external_link": "https://some-url"
}
- Ensure files are saved before uploading both folders into Pinata. Double check this by closing the file and re opening - make sure that the values have been saved. Then upload
myproject
directory to Pinata by selecting Add Files and Folder. Name your foldermyproject
in Pinata.

Click on the folder
myproject
and then thenft-metadata
folder to obtain your collectionsbaseURI
. This is used by Immutable's Blockchain Data APIs to fetch the NFT metadata from your hosting service when you mint your NFTs. Note down this URL, which should have the following format:https://peach-dear-wildfowl-698.mypinata.cloud/ipfs/QmVhfy6QYWBzwUZfDSJGvScLScaWKHEyUKBax8AmYqd4Sr/nft-metadata/
Go back and click on the
contract-metadata
followed bycontract.json
and note down the URL. This will be used to fillexternal_link
in the contract. This will have this format:https://peach-dear-wildfowl-698.mypinata.cloud/ipfs/QmVhfy6QYWBzwUZfDSJGvScLScaWKHEyUKBax8AmYqd4Sr/contract-metadata/contract.json
To get a better understanding, dive deeper into our metadata guides:
Set up minting wallet
Obtain test $IMX tokens to pay for gas fees
To deploy our smart contract to the test network, we’ll need some test $IMX.
To get test $IMX you will need to create a Immutable Hub account and go to this faucet. Enter your wallet's address in the text field requesting it and then click the Receive Test-IMX button. It may take a few minutes for the test $IMX to arrive.
Your test $IMX balance will be visible in your wallet:

zkEVM-testnet
network. To add this network please see this guide.Locate your wallet's private key
- Open Metamask
- Make sure the network at the top of the screen is the test network you wish to deploy to
- Open Account details by clicking on the following button and selecting
Account Details

- Select
Export Private Key
and enter your password - Copy the long string provided (your private key) as this will be needed in the next step.
Configure the private key
We now need to configure the application with your private key in order to grant it the requisite authorisation to deploy your contract.
- Open the
.env.example
file located in your project's (forkedzkEVM-boilerplate
) root directory. - Update the
PRIVATE_KEY
variable with your private key string that you obtained in the previous step. - Save the file
- Rename the file from
.env.example
to.env
ETHERSCAN_API_URL
and GOERLI_URL
.env.example
file as they are not needed for this tutorial.Set royalty fees (optional)
Royalties will be paid to a specified wallet each time the NFT is sold on a 3rd party marketplace. This is an optional step if you wish to learn how to incorporate royalties in your project.
- Go to the root directory of your contract deployment environment that you created during the project setup process. This should be something like
../{YOUR_GITHUB_USERNAME}/zkevm-boilerplate
- Go to the
../zkevm-boilerplate/scripts/
directory - Open the
deploy.ts
file in your code editor - Update the following variables with the correct details in the
deploy.ts
file:DEPLOYER_ADDRESS
- The owner of the contract's wallet, i.e. youROYALTY_PAYMENT_ADDRESS
- The wallet that will receive the royalty feesCOLLECTION_NAME
- The name of your collectionCOLLECTION_SYMBOL
- The symbol of your collection - typically 2-3 characters longbaseURI
- The baseURI from step 2contractURI
- The contractURI from step 2FEE
- The royalty fee % to a factor of x1000. e.g. 2000 is 2%
This is what your deploy.ts
file should look like:
const contract: MyERC721 = await MyERC721.connect(deployer).deploy(
`[DEPLOYER_ADDRESS]`, // owner
`[COLLECTION_NAME]`, // name
`[COLLECTION_SYMBOL]`, // symbol
`[baseURI]`, // baseURI
`[contractURI]`, // contractURI
operatorAllowlist, // operator allowlist contract address
`[ROYALTY_PAYMENT_ADDRESS]`, // royalty recipient
ethers.BigNumber.from(`[FEE]`) // royalty fee
);
- Run the following command to commit your changes:
npx hardhat compile
Contract deployment
Deploy the contract
Fill in the values in scripts/deploy.ts
as appropriate, and then run the following command from your project's root directory:
# Deploy to Immutable zkEVM Testnet
npx hardhat run --network immutableZkevmTestnet scripts/deploy.ts
Make sure you copy the CONTRACT_ADDRESS
which is returned after the successful deployment:
MyERC721 contract deployed to 0x6A4C106A6571533dE17d60D171a2747F549ae3C9
Verify the contract has been deployed
Immutable's testnet explorer can be used to verify your contract has been deployed to the network.
Entering the CONTRACT_ADDRESS
returned from the previous step in the search bar should return a record if it has been successfully deployed.
Mint tokens
Enable Minter role for yourself
We will now be using the Immutable SDK that you installed earlier.
Before you can mint from your newly deployed collection, you need to assign yourself the role of being an eligible minter. The following steps will enable this:
Go to the
myproject
directory that you set up earlierCreate a file called
grantMinterRole.ts
at the root directoryAdd the following code to your file, with the following variables updated:
CONTRACT_ADDRESS
- The address you received after deploying the contractPRIVATE_KEY
- Your wallet's private key
import { getDefaultProvider, Wallet } from 'ethers'; // ethers v5
import { Provider, TransactionResponse } from '@ethersproject/providers'; // ethers v5
import { ERC721Client } from '@imtbl/zkevm-contracts';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]';
const PRIVATE_KEY = '[PRIVATE_KEY]';
const provider = getDefaultProvider('https://rpc.testnet.immutable.com');
const grantMinterRole = async (
provider: Provider
): Promise<TransactionResponse> => {
// Bound contract instance
const contract = new ERC721Client(CONTRACT_ADDRESS);
// The wallet of the intended signer of the mint request
const wallet = new Wallet(PRIVATE_KEY, provider);
// Give the wallet minter role access
const populatedTransaction = await contract.populateGrantMinterRole(
wallet.address
);
const result = await wallet.sendTransaction(populatedTransaction);
return result;
};
grantMinterRole(provider);
- Run the following script:
./node_modules/.bin/ts-node grantMinterRole.ts
Mint Option 1: Mint a single NFTs by specifying the token ID
Do the following to mint a single NFT using the Immutable SDK, specifying the ID of the token that requires minting:
If using Immutable's recommended ERC721 preset contract the Token IDs of the desired NFT must be unique to a collection and below 2^128, the mintBatchByQuantityThreshold
(i.e. 340,282,366,920,938,463,463,374,607,431,768,211,455 or lower).
- Create a file called
mintNFT.ts
- Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS
- The second address of the wallet that will own the NFTs being minted in the batchTOKEN_ID
- A free token ID in the collection that is less than 340,282,366,920,938,463,463,374,607,431,768,211,456
import { getDefaultProvider, Wallet } from 'ethers'; // ethers v5
import { Provider, TransactionResponse } from '@ethersproject/providers'; // ethers v5
import { ERC721MintByIDClient } from '@imtbl/zkevm-contracts';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]';
const PRIVATE_KEY = '[PRIVATE_KEY]';
// Specify who we want to receive the minted token
const RECIPIENT = 'ACCOUNT_ADDRESS';
// Choose an ID for the new token
const TOKEN_ID = 0;
const provider = getDefaultProvider('https://rpc.testnet.immutable.com');
const mint = async (provider: Provider): Promise<TransactionResponse> => {
// Bound contract instance
const contract = new ERC721MintByIDClient(CONTRACT_ADDRESS);
// The wallet of the intended signer of the mint request
const wallet = new Wallet(PRIVATE_KEY, provider);
// We can use the read function hasRole to check if the intended signer
// has sufficient permissions to mint before we send the transaction
const minterRole = await contract.MINTER_ROLE(provider);
const hasMinterRole = await contract.hasRole(
provider,
minterRole,
wallet.address
);
if (!hasMinterRole) {
// Handle scenario without permissions...
console.log('Account doesnt have permissions to mint.');
return Promise.reject(
new Error('Account doesnt have permissions to mint.')
);
}
// Rather than be executed directly, contract write functions on the SDK client are returned
// as populated transactions so that users can implement their own transaction signing logic.
const populatedTransaction = await contract.populateMint(RECIPIENT, TOKEN_ID);
const result = await wallet.sendTransaction(populatedTransaction);
console.log(result); // To get the TransactionResponse value
return result;
};
mint(provider);
- Run the following script:
./node_modules/.bin/ts-node mintNFT.ts
Mint Option 2: Mint a batch of NFTs by specifying token IDs
Do the following to mint NFTs using the Immutable SDK, where you are able to specify the ID of each token to be minted. Minting in batches is more gas efficent than single mint requests.
If using Immutable's recommended ERC721 preset contract the Token IDs of the batch must be unique to a collection and below the mintBatchByQuantityThreshold
.
Immutable's recommended ERC721 preset contract has multiple batch minting strategies tailored for different minting scenarios. Significant gas savings can be realised if minting multiple NFTs to a single wallet using the mintBatchByQuantity()
function. This function will assign Token IDs of the mintBatchByQuantityThreshold
and above to the NFTs minted with this function.
For more information on batch minting check out the batch minting product page.
- Create a file called
mintNFTsByID.ts
- Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS1
- The first address of the wallet that will own the NFTs being minted in the batchACCOUNT_ADDRESS2
- The second address of the wallet that will own the NFTs being minted in the batchTOKEN_ID1
- A free token ID in the collection that is less than 340,282,366,920,938,463,463,374,607,431,768,211,456TOKEN_ID2
- A free token ID in the collection that is less than 340,282,366,920,938,463,463,374,607,431,768,211,456TOKEN_ID3
- A free token ID in the collection that is less than 340,282,366,920,938,463,463,374,607,431,768,211,456TOKEN_ID4
- A free token ID in the collection that is less than 340,282,366,920,938,463,463,374,607,431,768,211,456
import { getDefaultProvider, Wallet } from 'ethers'; // ethers v5
import { Provider, TransactionResponse } from '@ethersproject/providers'; // ethers v5
import { ERC721Client } from '@imtbl/zkevm-contracts';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]';
const PRIVATE_KEY = '[PRIVATE_KEY]';
const TOKEN_ID1 = 0;
const TOKEN_ID2 = 0;
const TOKEN_ID3 = 0;
const TOKEN_ID4 = 0;
const provider = getDefaultProvider('https://rpc.testnet.immutable.com');
const mint = async (provider: Provider): Promise<TransactionResponse> => {
// Bound contract instance
const contract = new ERC721Client(CONTRACT_ADDRESS);
// The wallet of the intended signer of the mint request
const wallet = new Wallet(PRIVATE_KEY, provider);
// We can use the read function hasRole to check if the intended signer
// has sufficient permissions to mint before we send the transaction
const minterRole = await contract.MINTER_ROLE(provider);
const hasMinterRole = await contract.hasRole(
provider,
minterRole,
wallet.address
);
if (!hasMinterRole) {
// Handle scenario without permissions...
console.log('Account doesnt have permissions to mint.');
return Promise.reject(
new Error('Account doesnt have permissions to mint.')
);
}
// Construct the mint requests
const requests = [
{
to: '[ACCOUNT_ADDRESS1]',
tokenIds: [TOKEN_ID1, TOKEN_ID2],
},
{
to: '[ACCOUNT_ADDRESS2]',
tokenIds: [TOKEN_ID3, TOKEN_ID4],
},
];
// Rather than be executed directly, contract write functions on the SDK client are returned
// as populated transactions so that users can implement their own transaction signing logic.
const populatedTransaction = await contract.populateMintBatch(requests);
const result = await wallet.sendTransaction(populatedTransaction);
console.log(result); // To get the TransactionResponse value
return result;
};
mint(provider);
- Run the following script:
./node_modules/.bin/ts-node mintNFTsByID.ts
Mint Option 3: Mint a batch of NFTs without specifying token ID (gas efficent)
Do the following to mint NFTs using the Immutable SDK in the most gas optimised method available. This method is more gas efficient than mintBatch()
however the ID of each token is system generated.
For more information on how this is achieved check out our mintBatchByQuantity()
function on the batch minting product page.
If using Immutable's recommended ERC721 preset contract the Token IDs of the batch must be unique to a collection and equal or above the mintBatchByQuantityThreshold
.
- Create a file called
mintNFTsByQuantity.ts
- Add the following code to your file with the following variables:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractPRIVATE_KEY
- The private key of the account used for signing the mint requestACCOUNT_ADDRESS1
- The first address of the wallet that will own the NFTs being minted in the batchACCOUNT_ADDRESS2
- The second address of the wallet that will own the NFTs being minted in the batch
import { getDefaultProvider, Wallet } from 'ethers'; // ethers v5
import { Provider, TransactionResponse } from '@ethersproject/providers'; // ethers v5
import { ERC721Client } from '@imtbl/zkevm-contracts';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]';
const PRIVATE_KEY = '[PRIVATE_KEY]';
// Specify who we want to receive the minted token
const provider = getDefaultProvider('https://rpc.testnet.immutable.com');
const mint = async (provider: Provider): Promise<TransactionResponse> => {
// Bound contract instance
const contract = new ERC721Client(CONTRACT_ADDRESS);
// The wallet of the intended signer of the mint request
const wallet = new Wallet(PRIVATE_KEY, provider);
// We can use the read function hasRole to check if the intended signer
// has sufficient permissions to mint before we send the transaction
const minterRole = await contract.MINTER_ROLE(provider);
const hasMinterRole = await contract.hasRole(
provider,
minterRole,
wallet.address
);
if (!hasMinterRole) {
// Handle scenario without permissions...
console.log('Account doesnt have permissions to mint.');
return Promise.reject(
new Error('Account doesnt have permissions to mint.')
);
}
const mints = [
{
to: '[ACCOUNT_ADDRESS1]',
quantity: 3,
},
{
to: '[ACCOUNT_ADDRESS2]',
quantity: 3,
},
];
// Rather than be executed directly, contract write functions on the SDK client are returned
// as populated transactions so that users can implement their own transaction signing logic.
const populatedTransaction = await contract.populateMintBatchByQuantity(
mints
);
const result = await wallet.sendTransaction(populatedTransaction);
console.log(result); // To get the TransactionResponse value
return result;
};
mint(provider);
- Run the following script:
./node_modules/.bin/ts-node mintNFTsByQuantity.ts
Verify successful mints
Blockscout's Immutable zkEVM testnet explorer can be used to verify your contract has been deployed to the network.
Entering the CONTRACT_ADDRESS
in the search bar will allow you to view transactions by collection. The mint request you just generated will produce a Contract Call
record.
Using the below script you can verify the minting request was successful.
- Create a file called
reviewTransaction.ts
- Add the following code to your file with the following variable:
CONTRACT_ADDRESS
- The address of the deployedERC721
contract
import { config as immutableConfig, blockchainData } from '@imtbl/sdk';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]'; // The address of the contract you deployed
const config: blockchainData.BlockchainDataModuleConfiguration = {
baseConfig: new immutableConfig.ImmutableConfiguration({
environment: immutableConfig.Environment.SANDBOX,
}),
};
const client = new blockchainData.BlockchainData(config);
async function getData() {
try {
const response = await client.listActivities({
chainName: 'imtbl-zkevm-testnet',
contractAddress: CONTRACT_ADDRESS,
});
for (const result in response.result) {
console.log(result);
}
return response.result;
} catch (error) {
console.error(error);
}
}
getData();
- Run the following script:
./node_modules/.bin/ts-node reviewTransaction.ts
View minted NFT
To validate that a mint was successful, you can view an NFT using the getNFT
method, which will show the NFT with metadata details via Immutable's indexer:
- Create a file called
getNFT.ts
- Add the following code to your file with the following variable populated:
CONTRACT_ADDRESS
- The address of the deployedERC721
contractTOKEN_ID
- The ID of the token. Minted token IDs can be retrieved from the output of the previous step.
import { config as immutableConfig, blockchainData } from '@imtbl/sdk';
const CONTRACT_ADDRESS = '[CONTRACT_ADDRESS]'; // The address of the deployed collection contract
const TOKEN_ID = ''; // The ID of the minted token
const config: blockchainData.BlockchainDataModuleConfiguration = {
baseConfig: new immutableConfig.ImmutableConfiguration({
environment: immutableConfig.Environment.SANDBOX,
}),
};
const client = new blockchainData.BlockchainData(config);
async function getData() {
try {
const response = await client.getNFT({
chainName: 'imtbl-zkevm-testnet',
contractAddress: CONTRACT_ADDRESS,
tokenId: TOKEN_ID,
});
console.log(response.result);
return response.result;
} catch (error) {
console.error(error);
}
}
getData();
- Run the following script:
./node_modules/.bin/ts-node getNFT.ts
Next steps
Now that you've created game assets, you can perform a primary sale.