Skip to main content
GatePass uses Polygon (MATIC) blockchain for minting NFT-based event tickets with low gas fees and fast confirmation times.

Overview

GatePass leverages blockchain technology to create verifiable, transferable, and secure event tickets as NFTs (Non-Fungible Tokens). Each ticket is a unique digital asset stored on the Polygon blockchain.

Why Polygon?

Low Gas Fees

Transactions cost fractions of a cent, making it affordable for mass ticket distribution.

Fast Confirmation

Average block time of 2 seconds ensures quick ticket minting.

Ethereum Compatible

Full EVM compatibility with existing tools like MetaMask and ethers.js.

Network Configuration

Polygon Mainnet

Network Details
const polygonConfig = {
  chainId: 137,
  chainName: 'Polygon Mainnet',
  nativeCurrency: {
    name: 'MATIC',
    symbol: 'MATIC',
    decimals: 18
  },
  rpcUrls: ['https://polygon-rpc.com'],
  blockExplorerUrls: ['https://polygonscan.com']
};

Environment Variables

Configure blockchain settings in your .env file:
.env
# Blockchain Configuration
VITE_CHAIN_ID=137
VITE_RPC_URL=https://polygon-rpc.com

# Server-side configuration
RPC_URL=https://polygon-rpc.com
PRIVATE_KEY=your_deployer_private_key_hex
CHAIN_ID=137
Never expose your private key! Only use it server-side for contract deployment and ticket minting operations.

Connecting with ethers.js

GatePass uses ethers.js v6 for all blockchain interactions.

Install Dependencies

npm install ethers@^6.9.0

Provider Setup

// From: ~/workspace/source/src/packages/server/src/utils/blockchain.ts:1-15

import { ethers } from 'ethers';

let provider: ethers.JsonRpcProvider | null = null;

export function getProvider(): ethers.JsonRpcProvider {
  if (provider) return provider;
  
  const rpcUrl = process.env.RPC_URL || process.env.VITE_RPC_URL;
  if (!rpcUrl) {
    throw new Error('RPC_URL not configured');
  }
  
  provider = new ethers.JsonRpcProvider(rpcUrl);
  return provider;
}

// Usage
const provider = getProvider();
const blockNumber = await provider.getBlockNumber();
console.log('Current block:', blockNumber);

Switch Network

Ensure users are connected to Polygon:
Switch to Polygon
async function switchToPolygon() {
  const chainId = parseInt(import.meta.env.VITE_CHAIN_ID);
  const hexChainId = `0x${chainId.toString(16)}`;

  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: hexChainId }],
    });
  } catch (switchError: any) {
    // Chain not added, add it
    if (switchError.code === 4902) {
      await window.ethereum.request({
        method: 'wallet_addEthereumChain',
        params: [
          {
            chainId: hexChainId,
            chainName: 'Polygon Mainnet',
            nativeCurrency: {
              name: 'MATIC',
              symbol: 'MATIC',
              decimals: 18,
            },
            rpcUrls: [import.meta.env.VITE_RPC_URL],
            blockExplorerUrls: ['https://polygonscan.com'],
          },
        ],
      });
    } else {
      throw switchError;
    }
  }
}

Smart Contract Interaction

Ticket Contract ABI

GatePass uses ERC-721 compatible contracts for ticket NFTs:
Contract ABI
const TICKET_ABI = [
  'function mintFor(address to, uint256 quantity) external',
  'function tokenCounter() view returns (uint256)',
  'function balanceOf(address owner) view returns (uint256)',
  'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)',
  'function tokenURI(uint256 tokenId) view returns (string)',
  'function ownerOf(uint256 tokenId) view returns (address)',
  'function transferFrom(address from, address to, uint256 tokenId) external',
  'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'
];

Server-side Signer

The backend uses a dedicated wallet for minting tickets:
// From: ~/workspace/source/src/packages/server/src/utils/blockchain.ts:17-26

import { ethers } from 'ethers';
import { logger } from './logger';

let signer: ethers.Wallet | null = null;

export function getSigner(): ethers.Wallet {
  if (signer) return signer;
  
  const privateKey = process.env.PRIVATE_KEY;
  if (!privateKey) {
    throw new Error('PRIVATE_KEY not configured');
  }
  
  signer = new ethers.Wallet(privateKey, getProvider());
  logger.info(`Blockchain signer loaded: ${signer.address}`);
  
  return signer;
}

// Usage
const signer = getSigner();
const balance = await signer.provider.getBalance(signer.address);
console.log('Signer balance:', ethers.formatEther(balance), 'MATIC');
The deployer wallet must have sufficient MATIC tokens to pay for gas fees when minting tickets.

Contract Deployment

Each event has its own ERC-721 contract deployed on Polygon:
Example Contract (Simplified)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract EventTicket is ERC721, Ownable {
    uint256 public tokenCounter;
    string private baseTokenURI;

    constructor(
        string memory name,
        string memory symbol,
        string memory _baseTokenURI
    ) ERC721(name, symbol) Ownable(msg.sender) {
        tokenCounter = 0;
        baseTokenURI = _baseTokenURI;
    }

    function mintFor(address to, uint256 quantity) external onlyOwner {
        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = tokenCounter;
            _safeMint(to, tokenId);
            tokenCounter++;
        }
    }

    function _baseURI() internal view override returns (string memory) {
        return baseTokenURI;
    }
}

Deploy Script

Deploy Contract
import { ethers } from 'ethers';
import { getSigner } from './blockchain';

async function deployTicketContract(
  eventName: string,
  symbol: string,
  baseURI: string
) {
  const signer = getSigner();

  // Contract bytecode and ABI (from compilation)
  const factory = new ethers.ContractFactory(
    CONTRACT_ABI,
    CONTRACT_BYTECODE,
    signer
  );

  console.log('Deploying contract...');
  const contract = await factory.deploy(eventName, symbol, baseURI);
  
  console.log('Waiting for deployment...');
  await contract.waitForDeployment();
  
  const address = await contract.getAddress();
  console.log('Contract deployed at:', address);
  
  return {
    address,
    txHash: contract.deploymentTransaction()?.hash
  };
}

// Usage
const result = await deployTicketContract(
  'GatePass Concert 2026',
  'GPC2026',
  'https://api.gatepass.app/metadata/event-123/'
);

Minting NFT Tickets

When a user purchases tickets, the backend mints NFTs to their wallet.
// From: ~/workspace/source/src/packages/server/src/utils/blockchain.ts:28-42

import { ethers } from 'ethers';

export async function mintTicketsFor(
  eventContractAddress: string,
  abi: any[],
  to: string,
  quantity: number
): Promise<{ txHash: string; tokenIds: number[] }> {
  const wallet = getSigner();
  const contract = new ethers.Contract(eventContractAddress, abi, wallet);
  
  // Get starting token ID
  const start: bigint = await contract.tokenCounter();
  
  // Mint tickets
  const tx = await contract.mintFor(to, quantity);
  const receipt = await tx.wait();
  
  // Calculate minted token IDs
  const first = Number(start);
  const tokenIds: number[] = Array.from(
    { length: quantity },
    (_, i) => first + i
  );
  
  return {
    txHash: receipt?.hash || tx.hash,
    tokenIds
  };
}
Gas Optimization: Minting multiple tickets in a single transaction saves gas fees compared to individual mints.

Reading Ticket Data

Query ticket ownership and metadata from the blockchain:
import { ethers } from 'ethers';

async function getUserTickets(
  contractAddress: string,
  userAddress: string
): Promise<number[]> {
  const provider = getProvider();
  const contract = new ethers.Contract(
    contractAddress,
    TICKET_ABI,
    provider
  );

  const balance = await contract.balanceOf(userAddress);
  const tokenIds: number[] = [];

  for (let i = 0; i < balance; i++) {
    const tokenId = await contract.tokenOfOwnerByIndex(userAddress, i);
    tokenIds.push(Number(tokenId));
  }

  return tokenIds;
}

// Usage
const tickets = await getUserTickets(
  '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
  '0x1234...5678'
);
console.log('User owns ticket IDs:', tickets);

Transaction Monitoring

Listen for blockchain events and monitor transaction status:
Event Listeners
import { ethers } from 'ethers';

async function monitorTicketTransfers(contractAddress: string) {
  const provider = getProvider();
  const contract = new ethers.Contract(
    contractAddress,
    TICKET_ABI,
    provider
  );

  // Listen for Transfer events
  contract.on('Transfer', (from, to, tokenId, event) => {
    console.log(`Ticket #${tokenId} transferred from ${from} to ${to}`);
    console.log('Transaction:', event.log.transactionHash);
    
    // Update database
    updateTicketOwner(Number(tokenId), to, event.log.transactionHash);
  });

  console.log('Monitoring ticket transfers...');
}

// Check transaction status
async function waitForTransaction(txHash: string) {
  const provider = getProvider();
  
  console.log('Waiting for transaction confirmation...');
  const receipt = await provider.waitForTransaction(txHash, 1);
  
  if (receipt?.status === 1) {
    console.log('Transaction successful!');
    console.log('Block number:', receipt.blockNumber);
    console.log('Gas used:', receipt.gasUsed.toString());
  } else {
    console.error('Transaction failed');
  }
  
  return receipt;
}

Wallet Integration (Frontend)

Connect user wallets and interact with contracts:
import { ethers } from 'ethers';
import { useState } from 'react';

function WalletConnect() {
  const [address, setAddress] = useState<string | null>(null);
  const [signer, setSigner] = useState<ethers.Signer | null>(null);

  const connectWallet = async () => {
    if (typeof window.ethereum === 'undefined') {
      alert('Please install MetaMask!');
      return;
    }

    try {
      const provider = new ethers.BrowserProvider(window.ethereum);
      
      // Request account access
      await provider.send('eth_requestAccounts', []);
      
      // Switch to Polygon
      await switchToPolygon();
      
      // Get signer
      const signer = await provider.getSigner();
      const address = await signer.getAddress();
      
      setSigner(signer);
      setAddress(address);
      
      // Save to backend
      await fetch('/api/users/update-wallet', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ walletAddress: address })
      });
      
      console.log('Wallet connected:', address);
    } catch (error) {
      console.error('Failed to connect wallet:', error);
    }
  };

  return (
    <div>
      {address ? (
        <div>Connected: {address.slice(0, 6)}...{address.slice(-4)}</div>
      ) : (
        <button onClick={connectWallet}>Connect Wallet</button>
      )}
    </div>
  );
}

Gas Estimation

Estimate transaction costs before execution:
Gas Estimation
import { ethers } from 'ethers';

async function estimateMintingCost(
  contractAddress: string,
  quantity: number
) {
  const provider = getProvider();
  const signer = getSigner();
  const contract = new ethers.Contract(contractAddress, TICKET_ABI, signer);

  // Estimate gas for minting
  const gasEstimate = await contract.mintFor.estimateGas(
    await signer.getAddress(),
    quantity
  );

  // Get current gas price
  const feeData = await provider.getFeeData();
  const gasPrice = feeData.gasPrice || 0n;

  // Calculate cost
  const gasCost = gasEstimate * gasPrice;
  const costInMatic = ethers.formatEther(gasCost);

  console.log('Estimated gas:', gasEstimate.toString());
  console.log('Gas price:', ethers.formatUnits(gasPrice, 'gwei'), 'gwei');
  console.log('Total cost:', costInMatic, 'MATIC');

  return {
    gasEstimate: gasEstimate.toString(),
    gasPrice: gasPrice.toString(),
    totalCost: costInMatic
  };
}

Error Handling

Handle common blockchain errors gracefully:
Error Handling
import { ethers } from 'ethers';

async function handleBlockchainError(error: any) {
  if (error.code === 'INSUFFICIENT_FUNDS') {
    console.error('Insufficient MATIC for gas fees');
    return 'Please add MATIC to your wallet';
  }
  
  if (error.code === 'NETWORK_ERROR') {
    console.error('Network connection failed');
    return 'Please check your internet connection';
  }
  
  if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
    console.error('Transaction would fail');
    return 'This transaction would fail. Please check contract conditions';
  }
  
  if (error.message?.includes('user rejected')) {
    console.error('User rejected transaction');
    return 'Transaction cancelled by user';
  }
  
  console.error('Blockchain error:', error);
  return 'An unexpected error occurred';
}

// Usage
try {
  await mintTicketsFor(contractAddress, abi, userAddress, quantity);
} catch (error) {
  const message = await handleBlockchainError(error);
  alert(message);
}

Best Practices

Retry Logic

Implement exponential backoff for failed transactions. Network congestion can cause temporary failures.

Transaction Timeouts

Set reasonable timeouts (30-60 seconds) for transaction confirmations.

Balance Checks

Always verify deployer wallet has sufficient MATIC before minting.

Event Monitoring

Use blockchain events instead of polling for real-time updates.

Resources

Polygon Docs

Official Polygon documentation

ethers.js Docs

ethers.js v6 documentation

PolygonScan

Block explorer for Polygon
For complete blockchain integration code, refer to:
  • ~/workspace/source/src/packages/server/src/utils/blockchain.ts
  • ~/workspace/source/src/packages/server/src/routes/webhooks.ts

Build docs developers (and LLMs) love