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
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:
# 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
Server-side Provider
Client-side Provider
// 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:
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:
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
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.
Mint Function
Usage Example
// 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:
Check Ownership
Get Metadata
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:
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:
Connect Wallet
Transfer Ticket
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:
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 || 0 n ;
// 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:
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