GatePass uses cryptographic QR codes and blockchain verification to prevent ticket fraud and streamline event check-in.
Overview
Ticket verification in GatePass combines QR code scanning with blockchain-based ownership verification to create a secure, fraud-resistant check-in system that works both online and offline.
Key Features
QR Code Scanning Fast, contactless check-in using mobile device cameras
Cryptographic Security Tickets are signed with cryptographic signatures to prevent forgery
Blockchain Verification On-chain ownership verification ensures ticket authenticity
Offline Mode Queue scans offline and sync when connection is restored
Check-In Model
Check-ins are recorded in the database with detailed metadata:
model CheckIn {
id String @id @default ( cuid ())
checkedInAt DateTime @default ( now ())
checkedInBy String ? // Staff member who checked in
location String ? // Check-in location/gate
// 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
@@map ( "check_ins" )
}
QR Code Structure
GatePass QR codes contain cryptographically signed ticket data:
// QR Code Format
// ticketId|eventId|timestamp|signature
// Example:
// TKT-123|EVT-456|1704067200|0x8a7b9c2d...
QR Code Generation
import crypto from 'crypto'
export const DEFAULT_QR_SECRET_SALT = 'gatepass-secure-qr-salt-2024'
export function generateQr (
ticketId : string ,
eventId : string ,
secretSalt : string = DEFAULT_QR_SECRET_SALT
) : string {
const timestamp = Date . now ()
const payload = ` ${ ticketId } | ${ eventId } | ${ timestamp } `
// Create HMAC signature
const signature = crypto
. createHmac ( 'sha256' , secretSalt )
. update ( payload )
. digest ( 'hex' )
return ` ${ payload } | ${ signature } `
}
QR Code Verification
export interface QrVerificationResult {
status : 'VALID' | 'ALREADY_USED' | 'FAKE' | 'TOO_EARLY' | 'EXPIRED'
ticketId : string
eventId : string
timestamp : number
}
export async function verifyQr (
qrString : string ,
secretSalt : string ,
expectedEventId : string ,
isUsed : ( ticketId : string ) => boolean ,
markUsed : ( ticketId : string ) => void ,
eventStartTime : number
) : Promise < QrVerificationResult > {
const parts = qrString . split ( '|' )
if ( parts . length !== 4 ) {
return {
status: 'FAKE' ,
ticketId: '' ,
eventId: '' ,
timestamp: 0
}
}
const [ ticketId , eventId , timestampStr , providedSignature ] = parts
const timestamp = parseInt ( timestampStr , 10 )
// Verify event ID
if ( eventId !== expectedEventId ) {
return { status: 'FAKE' , ticketId , eventId , timestamp }
}
// Recreate signature
const payload = ` ${ ticketId } | ${ eventId } | ${ timestamp } `
const expectedSignature = crypto
. createHmac ( 'sha256' , secretSalt )
. update ( payload )
. digest ( 'hex' )
// Compare signatures (constant-time comparison)
if ( ! crypto . timingSafeEqual (
Buffer . from ( providedSignature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
)) {
return { status: 'FAKE' , ticketId , eventId , timestamp }
}
// Check if ticket already used
if ( isUsed ( ticketId )) {
return { status: 'ALREADY_USED' , ticketId , eventId , timestamp }
}
// Check timing
const now = Date . now ()
if ( now < eventStartTime ) {
return { status: 'TOO_EARLY' , ticketId , eventId , timestamp }
}
// QR code expiry (24 hours after event start)
const qrExpiryTime = eventStartTime + ( 24 * 60 * 60 * 1000 )
if ( now > qrExpiryTime ) {
return { status: 'EXPIRED' , ticketId , eventId , timestamp }
}
// Mark as used
markUsed ( ticketId )
return { status: 'VALID' , ticketId , eventId , timestamp }
}
Smart Contract Check-In
Tickets are marked as used on-chain during check-in:
Self Check-In
Organizer-Assisted Check-In
Check Ticket Status
/**
* @notice Check-in a ticket and mint POA
* @param tokenId The ticket token ID to check in
*/
function checkIn ( uint256 tokenId ) external onlyEventStarted {
if ( ownerOf (tokenId) != msg.sender ) {
revert NotTokenOwner ();
}
if (ticketUsed[tokenId]) {
revert TicketAlreadyUsed ();
}
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);
}
Mobile Scanner Interface
GatePass provides a mobile-optimized scanner interface for event staff:
Select Event
Staff member selects the event they’re checking in attendees for
Start Scanning
Camera activates and begins scanning for QR codes
Automatic Verification
QR code is automatically verified when detected
Visual Feedback
Green checkmark for valid tickets, red X for invalid/used tickets
POA Minting
Proof of Attendance NFT is automatically minted on successful check-in
Scanner Component
export function MobileScanner ({ onBack } : MobileScannerProps ) {
const [ isScanning , setIsScanning ] = useState ( false )
const [ scanResults , setScanResults ] = useState < ScanResult []>([])
const [ currentEventId , setCurrentEventId ] = useState < string >( '1' )
const usedTicketsRef = useRef < Set < string >>( new Set ())
const scannerRef = useRef < Html5QrcodeScanner | null >( null )
useEffect (() => {
const containerId = 'qr-reader'
if ( isScanning && ! scannerRef . current ) {
const scanner = new Html5QrcodeScanner (
containerId ,
{ fps: 10 , qrbox: 250 },
false
)
scanner . render (
async ( decodedText ) => {
const parts = decodedText . split ( '|' )
const eventIdFromQr = parts [ 1 ] || currentEventId
const result = await verifyQr (
decodedText ,
DEFAULT_QR_SECRET_SALT ,
eventIdFromQr ,
( tid ) => usedTicketsRef . current . has ( tid ),
( tid ) => usedTicketsRef . current . add ( tid ),
Date . now () - 60 * 60 * 1000
)
handleVerificationOutcome ( decodedText , result )
},
( error ) => {
// Ignore scan errors (noisy)
}
)
scannerRef . current = scanner
}
}, [ isScanning , currentEventId ])
const handleVerificationOutcome = ( qrString : string , result : any ) => {
const statusMap : Record < string , ScanResult [ 'status' ]> = {
VALID: 'valid' ,
ALREADY_USED: 'used' ,
FAKE: 'invalid' ,
TOO_EARLY: 'expired' ,
EXPIRED: 'expired'
}
const status = statusMap [ result . status ] ?? 'invalid'
const newScan : ScanResult = {
id: Date . now (). toString (),
ticketId: result . ticketId ,
eventTitle: selectedEventTitle ,
attendeeName: 'Unknown' ,
ticketType: 'General' ,
seatNumber: 'N/A' ,
status ,
scanTime: new Date (). toLocaleTimeString (),
walletAddress: '0x0000...0000'
}
setScanResults ( prev => [ newScan , ... prev ])
if ( status === 'valid' ) {
toast . success ( 'VALID ticket' )
} else if ( status === 'used' ) {
toast . error ( 'ALREADY USED' )
} else {
toast . error ( 'FAKE/INVALID ticket' )
}
}
}
Offline Check-In
The scanner supports offline operation for venues with poor connectivity:
Before the event, staff can download a local database of all valid ticket IDs and signatures for offline verification. const preDownloadDb = async ( eventId : string ) => {
const tickets = await fetch ( `/api/events/ ${ eventId } /tickets` )
. then ( res => res . json ())
// Store in IndexedDB or localStorage
await localDB . store ( 'tickets' , tickets )
toast . success ( 'Ticket database downloaded for offline use' )
}
When offline, the scanner uses local cryptographic verification without network calls. const verifyOffline = async ( qrString : string ) => {
// Verify signature locally
const isValid = verifyQrSignature ( qrString )
if ( ! isValid ) {
return { status: 'FAKE' }
}
// Check against local database
const ticket = await localDB . getTicket ( ticketId )
if ( ticket ?. used ) {
return { status: 'ALREADY_USED' }
}
// Mark as used locally
await localDB . markUsed ( ticketId )
return { status: 'VALID' }
}
Offline scans are queued and synced when connection is restored. const syncOfflineScans = async () => {
const queuedScans = await localDB . getQueuedScans ()
for ( const scan of queuedScans ) {
try {
await fetch ( '/api/check-ins' , {
method: 'POST' ,
body: JSON . stringify ( scan )
})
await localDB . removeFromQueue ( scan . id )
} catch ( error ) {
// Will retry on next sync
console . error ( 'Sync failed:' , error )
}
}
toast . success ( `Synced ${ queuedScans . length } offline scans` )
}
Security Features
Cryptographic Signatures HMAC-SHA256 signatures prevent QR code forgery and tampering
Blockchain Verification On-chain ownership verification ensures tickets haven’t been transferred or revoked
Anti-Replay Protection Tickets are marked as used in the smart contract to prevent double-entry
Time-Based Validation QR codes are only valid during the event window to prevent early or late scanning
Event-Specific Codes QR codes are bound to specific events and cannot be reused for other events
Constant-Time Comparison Signature verification uses timing-safe comparison to prevent timing attacks
Check-In Methods
1. Self Check-In
Attendees scan their own QR code at entrance kiosks:
// Attendee calls checkIn() on the smart contract
const checkIn = async ( ticketId : number ) => {
const contract = new ethers . Contract (
eventContractAddress ,
EventTicketABI ,
signer
)
const tx = await contract . checkIn ( ticketId )
await tx . wait ()
toast . success ( 'Checked in successfully! POA NFT minted.' )
}
2. Staff-Assisted Check-In
Event staff scan attendee QR codes:
// Staff member scans QR code with mobile scanner
// Backend calls organizerCheckIn() on behalf of attendee
const staffCheckIn = async ( ticketId : number , attendeeAddress : string ) => {
const contract = new ethers . Contract (
eventContractAddress ,
EventTicketABI ,
organizerSigner
)
const tx = await contract . organizerCheckIn ( ticketId )
await tx . wait ()
// Record in database
await prisma . checkIn . create ({
data: {
ticketId: ticket . id ,
eventId: event . id ,
userId: attendee . id ,
checkedInBy: staff . id ,
location: 'Main Entrance' ,
checkedInAt: new Date ()
}
})
}
3. Manual Verification
For backup when QR scanning fails:
const manualCheckIn = async ( ticketId : string ) => {
// Look up ticket in database
const ticket = await prisma . ticket . findFirst ({
where: { id: ticketId },
include: { order: true , event: true }
})
if ( ! ticket ) {
throw new Error ( 'Ticket not found' )
}
if ( ticket . isUsed ) {
throw new Error ( 'Ticket already used' )
}
// Verify payment completed
if ( ticket . order . paymentStatus !== 'COMPLETED' ) {
throw new Error ( 'Payment not completed' )
}
// Mark as used
await prisma . ticket . update ({
where: { id: ticketId },
data: { isUsed: true , usedAt: new Date () }
})
// Create check-in record
await prisma . checkIn . create ({
data: {
ticketId: ticket . id ,
eventId: ticket . eventId ,
userId: ticket . order . userId ,
checkedInBy: staff . id ,
location: 'Manual Verification'
}
})
}
Scanner Analytics
Track check-in performance and identify bottlenecks:
interface ScannerStats {
totalScanned : number
validTickets : number
invalidTickets : number
alreadyUsed : number
checkInRate : number // Percentage of sold tickets checked in
avgScanTime : number // Average time per scan in milliseconds
peakHours : { hour : number ; count : number }[]
}
const getCheckInStats = async ( eventId : string ) => {
const checkIns = await prisma . checkIn . findMany ({
where: { eventId },
include: { ticket: true }
})
const totalTicketsSold = await prisma . ticket . count ({
where: { eventId }
})
return {
totalScanned: checkIns . length ,
validTickets: checkIns . filter ( c => c . ticket . isUsed ). length ,
checkInRate: ( checkIns . length / totalTicketsSold ) * 100 ,
peakHours: calculatePeakHours ( checkIns )
}
}
Best Practices
Always test your check-in system before the event with a small group to identify any issues.
Pre-Event Testing
Test the scanner with sample tickets 24-48 hours before the event
Download Offline Database
Pre-download the ticket database to all scanner devices before the event
Multiple Check-In Points
Set up multiple check-in stations to reduce queues and wait times
Backup Power
Ensure all scanner devices have backup power sources
Staff Training
Train staff on how to use the scanner and handle edge cases
Manual Verification Process
Have a documented manual verification process for when technology fails
Troubleshooting
Ensure adequate lighting
Clean camera lens
Adjust distance between camera and QR code
Use manual entry as backup
Check if attendee already checked in
Verify ticket hasn’t been transferred
Look up check-in record in database
Contact event organizer if fraudulent activity suspected
Network Connectivity Issues
Switch to offline mode
Use local verification
Queue scans for later sync
Ensure offline database is up to date
Signature Verification Failed
Verify QR code hasn’t been tampered with
Check event ID matches current event
Ensure secret salt is correctly configured
Use blockchain verification as fallback
Next Steps
Proof of Attendance Learn about POA NFTs that are automatically minted during check-in
Event Management Manage your events and monitor check-in rates