Skip to main content

Overview

GatePass provides a secure, blockchain-powered check-in system that validates tickets via QR codes and automatically mints Proof of Attendance (POA) NFTs to attendees. This guide covers the complete check-in process for event organizers.

Check-In Methods

Mobile Scanner App

Scan QR codes using the GatePass mobile scanner (recommended)

Smart Contract

Self-check-in via blockchain interaction

Manual Entry

Manually enter ticket IDs for offline validation

Assisted Check-In

Staff can check in attendees using organizer permissions

Mobile Scanner Setup

1

Access Scanner Interface

Navigate to Ticket Scanner from your organizer dashboard or go directly to /scanner.
2

Select Event

Choose the event you’re checking in attendees for:
Event Selection
const events = ['Tech Conference 2024', 'Summer Music Festival', 'Startup Pitch Night'];
const selectedEventId = '1'; // Event ID
3

Enable Camera

Click Start Scanning to activate your device camera. Grant camera permissions if prompted.
The scanner uses the html5-qrcode library for reliable QR code detection.
4

Scan Tickets

Point your camera at attendees’ ticket QR codes. The system automatically:
  1. Decodes QR code data
  2. Validates cryptographic signature
  3. Checks ticket ownership on blockchain
  4. Verifies ticket hasn’t been used
  5. Marks ticket as used in database
  6. Mints Proof of Attendance NFT

QR Code Validation

Ticket QR codes contain encrypted data validated through multiple security layers:

QR Code Format

QR Code Structure
const qrData = [
  ticketId,           // Unique ticket identifier
  eventId,            // Event identifier
  attendeeId,         // Attendee/wallet address
  timestamp,          // Purchase timestamp
  signature           // HMAC signature
].join('|');

// Example:
"TKT-001|event_123|0x742d35...f0bEb|1709234567|a1b2c3d4e5f6"

Validation Process

Ticket Verification
import { verifyQr, DEFAULT_QR_SECRET_SALT } from '@/utils/ticketing/security';

const result = await verifyQr(
  qrString,                           // QR code data
  DEFAULT_QR_SECRET_SALT,             // Secret salt
  eventId,                            // Expected event ID
  (ticketId) => usedTickets.has(ticketId),  // Check if used
  (ticketId) => usedTickets.add(ticketId),  // Mark as used
  eventStartTime                      // Event start timestamp
);

// Result status:
// - VALID: Ticket is authentic and unused
// - ALREADY_USED: Ticket has been checked in
// - FAKE: Invalid signature
// - TOO_EARLY: Check-in before event start
// - EXPIRED: Ticket expired

Security Features

Each QR code includes an HMAC-SHA256 signature that validates:
  • Ticket authenticity
  • Data integrity
  • Preventing ticket forgery
Signature Generation
import crypto from 'crypto';

const data = `${ticketId}|${eventId}|${timestamp}`;
const signature = crypto
  .createHmac('sha256', SECRET_SALT)
  .update(data)
  .digest('hex')
  .slice(0, 12);
For NFT tickets, the system verifies:
  • Token ownership via ownerOf(tokenId)
  • Ticket validity via isTicketValid(tokenId)
  • Token existence on the blockchain
Used tickets are tracked to prevent reuse:
  • In-memory cache during event
  • Database persistence
  • Blockchain state (ticketUsed mapping)
Tickets are validated against event timing:
  • Can’t check in before event start
  • Optional check-in window restrictions
  • Timezone-aware validation

Check-In via Smart Contract

Attendees can self-check-in by calling the contract:

Self Check-In

checkIn() Function
function checkIn(uint256 tokenId) external onlyEventStarted {
    // Verify ownership
    if (ownerOf(tokenId) != msg.sender) {
        revert NotTokenOwner();
    }
    
    // Check if already used
    if (ticketUsed[tokenId]) {
        revert TicketAlreadyUsed();
    }

    // Mark as used
    ticketUsed[tokenId] = true;

    // Mint Proof of Attendance NFT
    uint256 poaTokenId = proofOfAttendance.mintPOA(msg.sender, tokenId);
    
    emit TicketCheckedIn(tokenId, msg.sender);
    emit ProofOfAttendanceMinted(msg.sender, tokenId, poaTokenId);
}
const { ethereum } = window;
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();

const ticketContract = new ethers.Contract(
  eventContractAddress,
  EventTicketABI,
  signer
);

const tokenId = 123; // Your ticket token ID

// Self check-in
const tx = await ticketContract.checkIn(tokenId);
console.log('Check-in initiated:', tx.hash);

await tx.wait();
console.log('Checked in! POA NFT minted.');

Organizer-Assisted Check-In

Organizers can check in attendees on their behalf:
organizerCheckIn() Function
function organizerCheckIn(uint256 tokenId) external onlyOwner onlyEventStarted {
    if (ticketUsed[tokenId]) {
        revert TicketAlreadyUsed();
    }

    ticketUsed[tokenId] = true;
    address ticketHolder = ownerOf(tokenId);

    // Mint POA to ticket holder
    uint256 poaTokenId = proofOfAttendance.mintPOA(ticketHolder, tokenId);
    
    emit TicketCheckedIn(tokenId, ticketHolder);
    emit ProofOfAttendanceMinted(ticketHolder, tokenId, poaTokenId);
}
Only the event organizer (contract owner) can use organizerCheckIn().

Proof of Attendance (POA) Minting

After successful check-in, a POA NFT is automatically minted:

POA Contract

ProofOfAttendance.sol
contract ProofOfAttendance is ERC721, Ownable {
    mapping(uint256 => uint256) public originalTicketId;  // POA ID → Ticket ID
    mapping(uint256 => uint256) public ticketToPOA;       // Ticket ID → POA ID
    
    function mintPOA(
        address attendee,
        uint256 ticketId
    ) external onlyAuthorizedMinter returns (uint256) {
        require(ticketToPOA[ticketId] == 0, "POA already minted");
        
        uint256 poaTokenId = tokenCounter++;
        
        originalTicketId[poaTokenId] = ticketId;
        ticketToPOA[ticketId] = poaTokenId;
        
        _safeMint(attendee, poaTokenId);
        
        emit POAMinted(attendee, poaTokenId, ticketId);
        
        return poaTokenId;
    }
}

POA Properties

Soulbound
boolean
default:true
POAs are non-transferable (soulbound) - they stay with the attendee forever
Permanent
boolean
default:true
POAs cannot be burned or deleted once minted
Unique
boolean
default:true
One POA per ticket - duplicate minting is prevented
Metadata
string
Includes event name, date, venue, and attendee information

POA Metadata Example

POA Metadata
{
  "name": "Tech Conference 2024 - Proof of Attendance",
  "description": "Proof of attendance for Tech Conference 2024 held on June 15, 2026 at Landmark Center, Lagos",
  "image": "ipfs://QmPOA.../tech-conf-2024.png",
  "attributes": [
    {
      "trait_type": "Event",
      "value": "Tech Conference 2024"
    },
    {
      "trait_type": "Date",
      "value": "2026-06-15"
    },
    {
      "trait_type": "Venue",
      "value": "Landmark Center, Lagos"
    },
    {
      "trait_type": "Original Ticket ID",
      "value": "123"
    },
    {
      "trait_type": "Attendance Type",
      "value": "In-Person"
    }
  ],
  "external_url": "https://gatepass.com/events/tech-conf-2024"
}

Check-In Dashboard

Monitor real-time check-in statistics:

Live Metrics

Check-In Stats
interface CheckInStats {
  totalScanned: number;      // Total scans attempted
  validScans: number;        // Successfully checked in
  checkedIn: number;         // Unique attendees
  flagged: number;           // Invalid/duplicate tickets
  offlineQueue: number;      // Pending offline syncs
}

Recent Scans Display

Scan Result
interface ScanResult {
  id: string;
  ticketId: string;
  eventTitle: string;
  attendeeName: string;
  ticketType: string;
  status: 'valid' | 'invalid' | 'used' | 'expired';
  scanTime: string;
  walletAddress: string;
}

Offline Mode

The scanner supports offline check-ins with queue-based syncing:

Offline Capabilities

1

Pre-Download Database

Before the event, download the ticket database for offline validation:
Pre-Download
const tickets = await fetch(`/api/events/${eventId}/tickets`);
localStorage.setItem('offline_tickets', JSON.stringify(tickets));
2

Offline Validation

When offline, validate tickets using:
  • Cryptographic signature verification (no internet required)
  • Local ticket database
  • In-memory used ticket tracking
3

Queue Offline Scans

Store check-ins locally:
Offline Queue
const offlineQueue = [];

function queueScan(scanResult) {
  offlineQueue.push(scanResult);
  localStorage.setItem('offline_queue', JSON.stringify(offlineQueue));
}
4

Auto-Sync on Reconnection

When internet is restored, automatically sync queued check-ins:
Sync Queue
async function syncOfflineQueue() {
  const queue = JSON.parse(localStorage.getItem('offline_queue') || '[]');
  
  for (const scan of queue) {
    await fetch('/api/checkins', {
      method: 'POST',
      body: JSON.stringify(scan)
    });
  }
  
  localStorage.removeItem('offline_queue');
  console.log(`✅ Synced ${queue.length} offline scans`);
}
Signature verification works offline. Blockchain checks happen during sync.

Database Check-In Tracking

Check-ins are recorded in the database:
CheckIn Model
model CheckIn {
  id              String   @id @default(cuid())
  checkedInAt     DateTime @default(now())
  checkedInBy     String?  // Staff member ID
  location        String?  // Check-in gate/location
  
  // POA NFT info
  poaTokenId      Int?
  poaContractAddr String?
  poaTxHash       String?
  
  // Relations
  ticket   Ticket @relation(fields: [ticketId], references: [id])
  ticketId String @unique
  
  event    Event  @relation(fields: [eventId], references: [id])
  eventId  String
  
  user     User   @relation(fields: [userId], references: [id])
  userId   String
}

Create Check-In Record

API Call
const checkIn = await prisma.checkIn.create({
  data: {
    ticketId: 'ticket_123',
    eventId: 'event_456',
    userId: 'user_789',
    checkedInBy: 'organizer_001',
    location: 'Main Entrance',
    poaTokenId: 42,
    poaContractAddr: '0x...',
    poaTxHash: '0x...'
  }
});

Export Check-In Data

Export check-in records for reporting:
CSV Export
function exportCheckIns(checkIns) {
  const header = [
    'id', 'ticketId', 'eventTitle', 'attendeeName',
    'ticketType', 'seatNumber', 'status', 'scanTime', 'walletAddress'
  ];
  
  const rows = checkIns.map(c => [
    c.id, c.ticketId, c.eventTitle, c.attendeeName,
    c.ticketType, c.seatNumber, c.status, c.scanTime, c.walletAddress
  ]);
  
  const csv = [header, ...rows]
    .map(row => row.map(v => `"${v}"`).join(','))
    .join('\n');
  
  // Download CSV
  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `checkins-${Date.now()}.csv`;
  a.click();
}

Best Practices

Test Scanner Early

Test the scanner before your event starts. Verify camera works and QR codes scan correctly.

Multiple Check-In Points

For large events, set up multiple check-in stations to reduce wait times.

Offline Backup

Pre-download ticket data before the event in case of connectivity issues.

Staff Training

Train your staff on using the scanner and handling edge cases (lost tickets, transfer issues, etc.).

Troubleshooting

Common solutions:
  • Ensure good lighting (use flashlight if needed)
  • Increase brightness on attendee’s phone
  • Make sure QR code is not blurry or damaged
  • Try manual entry if QR fails repeatedly
This could indicate:
  • Ticket was previously checked in (check scan history)
  • Duplicate check-in attempt
  • Possible ticket duplication fraud
Verify blockchain state to confirm.
POA minting can fail if:
  • Insufficient gas (ensure organizer wallet has MATIC)
  • Contract not authorized as minter
  • Blockchain congestion (wait and retry)
Check-in succeeds even if POA minting fails (can be minted later).
Ensure:
  • Ticket database was pre-downloaded
  • Browser has local storage enabled
  • Service worker is registered (for PWA)

Next Steps

Analytics Dashboard

View detailed check-in analytics and attendance reports

Creating Events

Learn how to create your own ticketed events

Build docs developers (and LLMs) love