How to set token index on Aptos


In this post, we will further enhance mint allowlist security and add a personalized index to the token.

You can skip the post and found the full code here.

Limit the number of mints

We can restrict each off-chain allowlisted user to mint only one NFT. It is very simple, just add the off-chain user identifier to the proof challenge and record the token index.

First, add the user identifier to the MintProofChallenge struct:

struct MintProofChallenge has drop {
  receiver_account_sequence_number: u64,
  receiver_account_address: address,
  user_identifier: vector<u8>,

// ...

fun verify_proof(receiver_addr: address, user_identifier: vector<u8>, proof_signature: vector<u8>, public_key: ed25519::ValidatedPublicKey) {
  // ...
  let proof_challenge = MintProofChallenge {
    receiver_account_sequence_number: sequence_number,
    receiver_account_address: receiver_addr,
  // ...

Then add a table named mints to record the token index of every off-chain user:

// ...
use aptos_std::table_with_length::{Self, TableWithLength};

struct NFTMinter has key {
  signer_cap: account::SignerCapability,
  collection: String,
  public_key: ed25519::ValidatedPublicKey,
  mints: TableWithLength<vector<u8>, u64>,

/// error code specifies already minted
const EALREADY_MINTED: u64 = 2;

// ...
fun init_module(resource_account: &signer) {
  // ...
  move_to(resource_account, NFTMinter {
    signer_cap: resource_signer_cap,
    collection: collection_name,
    public_key: public_key,
    // don't forgot to initialize the table
    mints: table_with_length::new(),

At last, in the mint_nft function, we check if the off-chain user minted and record the token index.

public entry fun mint_nft(receiver: &signer, user_identifier: vector<u8>, proof_signature: vector<u8>) acquires NFTMinter {
  // ...
  verify_proof(receiver_addr, user_identifier, proof_signature, nft_minter.public_key);  
  // check if minted
  assert!(!table_with_length::contains(&nft_minter.mints, user_identifier), error::aborted(EALREADY_MINTED));

  // record the token index
  let index = table_with_length::length(&nft_minter.mints);
  table_with_length::add(&mut nft_minter.mints, user_identifier, index);

  // ...

Personalized token name

We can add the token index to the token name and token uri, so that the token mint from each user is different.

public entry fun mint_nft(receiver: &signer, user_identifier: vector<u8>, proof_signature: vector<u8>) acquires NFTMinter {
  // ...

  let token_name = string::utf8(b"IMCODING NFT");
  string::append_utf8(&mut token_name, b": ");
  string::append_utf8(&mut token_name, u64_to_bytes(index));
  let token_description = string::utf8(b"");
  let token_uri = string::utf8(b"");
  string::append_utf8(&mut token_uri, u64_to_bytes(index));
  string::append_utf8(&mut token_uri, b".json");

  // ...

fun u64_to_bytes(i: u64): vector<u8> {
  let v = vector::empty<u8>();
  while (i >= 10) {
    vector::push_back(&mut v, (48 + i % 10 as u8));
    i = i / 10;

  vector::push_back(&mut v, (48 + i as u8));
  vector::reverse(&mut v);

Mint NFT

Modify proof signature generate code (assume that the off-chain user id is 1000001):

// ...
class MintProofChallenge {
  // ...
  identifier: Uint8Array;

  constructor(sequenceNumber: number, address: string, identifier: Uint8Array) {
    // ...
    this.identifier = identifier;  

  serialize(serializer: BCS.Serializer) {
    // ...

const generateProofSignature = async (userId: string, address: string): Promise<string> => {
  // ...
  const identifier = Buffer.from(userId);
  console.log("identitifer:", identifier.toString("hex"));

  const proof = new MintProofChallenge(
  // ...

generateProofSignature("2333333", "35a18f9201d2d6a9e3c86c4b9a00cb4444129cd2dc2fff72719240f8cb394016").then(console.log);

Genarete the identifier and proof signature, got:

identitifer: 32333333333333

Let's republish the module and mint the nft:

aptos move run --function-id 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting::mint_nft --args hex:32333333333333 --args hex:977cf82a1f86879a0be90e33d72ec68b9c787a47ccfdd979d4735c25faf3ff8a469f3f40bbdb09eb7cb21bfcff79c4401d779d3172cb52fa9996c11b2366810b

After successful execution, the event data of 0x3::token::CreateTokenDataEvent is roughly as follows:

  "description": "",
  "id": {
    "collection": "IMCODING NFT Collection",
    "creator": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
    "name": "IMCODING NFT: 0"
  "maximum": "1",
  "mutability_config": {
    "description": false,
    "maximum": false,
    "properties": true,
    "royalty": false,
    "uri": false
  "name": "IMCODING NFT: 0",
  "property_keys": [],
  "property_types": [],
  "property_values": [],
  "royalty_payee_address": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
  "royalty_points_denominator": "1",
  "royalty_points_numerator": "0",
  "uri": ""

Each token has a unique token name and uri. If you use this off-chain user to mint NFT again, you will get the following error:

  "Error": "Simulation failed with status: Move abort in 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting: EALREADY_MINTED(0x70002): error code specifies already minted"

So far, the tutorial of mint nft on aptos has been completed, and the full code can be found here.