TokenId in lazy minting

I was following this tutorial from openzeppelin on lazy minting
link to the video[youtube link]
by means of which a voucher is generated without modifying the state of the blockchain and hence making it gasless. What i don't understand is how is the tokenId of the voucher being kept track of(since making a counter and using that to make tokens would imply gas fees which is what i'm trying to avoid for the token creator)

    const { voucher, signature } = await lazyMinter.createVoucher(

the number 1 is the tokenId that i have to provide in my tests which i'm not sure how to unload to the contract gas-lessly

This is the function used to create and redeem the voucher

const ethers = require("ethers");
const { TypedDataUtils } = require("ethers-eip712");

const SIGNING_DOMAIN_NAME = "LazyNFT-Voucher";

class LazyMinter {
  constructor({ contractAddress, signer }) {
    this.contractAddress = contractAddress;
    this.signer = signer;

    this.types = {
      EIP712Domain: [
        { name: "name", type: "string" },
        { name: "version", type: "string" },
        { name: "chainId", type: "uint256" },
        { name: "verifyingContract", type: "address" },
      NFTVoucher: [
        { name: "tokenId", type: "uint256" },
        { name: "minPrice", type: "uint256" },
        { name: "uri", type: "string" },

  async _signingDomain() {
    if (this._domain != null) {
      return this._domain;
    const chainId = await this.signer.getChainId();
    this._domain = {
      verifyingContract: this.contractAddress,
    return this._domain;

  async _formatVoucher(voucher) {
    const domain = await this._signingDomain();
    return {
      types: this.types,
      primaryType: "NFTVoucher",
      message: voucher,

  async createVoucher(tokenId, uri, minPrice = 0) {
    const voucher = { tokenId, uri, minPrice };
    const typedData = await this._formatVoucher(voucher);
    const digest = TypedDataUtils.encodeDigest(typedData);
    const signature = await this.signer.signMessage(digest);
    return {

module.exports = {

The contract code to verify this is

  function _hash(NFTVoucher calldata voucher) internal view returns (bytes32) {

    return _hashTypedDataV4(keccak256(abi.encode(
      keccak256("NFTVoucher(uint256 tokenId,uint256 minPrice,string uri)"),

  /// @notice Verifies the signature for a given NFTVoucher, returning the address of the signer.
  /// @dev Will revert if the signature is invalid. Does not verify that the signer is authorized to mint NFTs.
  /// @param voucher An NFTVoucher describing an unminted NFT.
  /// @param signature An EIP712 signature of the given voucher.
  function _verify(NFTVoucher calldata voucher, bytes memory signature) internal view returns (address) {
    bytes32 digest = _hash(voucher);
    return digest.toEthSignedMessageHash().recover(signature);

  function supportsInterface(bytes4 interfaceId) public view virtual override (AccessControl, ERC721) returns (bool) {
    return ERC721.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);

If i understand correctly we have to store this voucher id in a centralised location off chain and retrieve as and when needed? I am new to web3 so pardon me if i missed out on any necessary details i'll be happy to provide them when asked for.

The token creator needs to generate a signed voucher, and the token buyer needs to receive both the voucher and the signature. The token buyer then has the tokenId (as it is part of the voucher) and they are the ones who have to submit it to the smart contract.

Does this answer the question?