Skip to main content
Proof of Attendance (POA) NFTs are automatically minted when attendees check in to events, providing permanent, verifiable proof of attendance on the blockchain.

Overview

GatePass mints Proof of Attendance NFTs as soulbound tokens (non-transferable) that commemorate event participation. These NFTs serve as digital memorabilia and verifiable credentials for attendance.

Key Features

Automatic Minting

POAs are automatically minted during ticket check-in

Soulbound Tokens

Non-transferable NFTs that stay with the original attendee

Verifiable Credentials

Blockchain-based proof of attendance for events

Digital Collectibles

Commemorative NFTs that build an attendance history

Smart Contract Architecture

The ProofOfAttendance.sol contract implements ERC721 with soulbound mechanics:
contract ProofOfAttendance is ERC721, Ownable, ReentrancyGuard {
    /// @notice Base URI for token metadata
    string private _baseTokenURI;
    
    /// @notice Counter for minted POA tokens
    uint256 public tokenCounter = 1;
    
    /// @notice Mapping from POA token ID to original ticket ID
    mapping(uint256 => uint256) public originalTicketId;
    
    /// @notice Mapping from original ticket ID to POA token ID
    mapping(uint256 => uint256) public ticketToPOA;
    
    /// @notice Mapping of addresses authorized to mint POAs (event contracts)
    mapping(address => bool) public authorizedMinters;

    event POAMinted(address indexed attendee, uint256 indexed poaTokenId, uint256 indexed ticketId);
    event MinterAuthorized(address indexed minter);
    event MinterRevoked(address indexed minter);
}

POA Minting Process

POAs are minted automatically during the check-in process:
1

Attendee Checks In

Attendee scans their ticket QR code at the event entrance
2

Ticket Verification

Ticket is verified cryptographically and marked as used on-chain
3

POA Minting

The EventTicket contract calls mintPOA() on the ProofOfAttendance contract
4

Token Transfer

POA NFT is minted directly to the attendee’s wallet
5

Database Update

Check-in record is updated with POA token ID and transaction hash

Mint Function

ProofOfAttendance.sol
/**
 * @notice Mint a Proof of Attendance NFT
 * @param attendee Address to mint the POA to
 * @param ticketId Original ticket ID that was checked in
 * @return poaTokenId The minted POA token ID
 */
function mintPOA(
    address attendee,
    uint256 ticketId
) external onlyAuthorizedMinter nonReentrant returns (uint256) {
    require(attendee != address(0), "Invalid attendee address");
    require(ticketToPOA[ticketId] == 0, "POA already minted for this ticket");

    uint256 poaTokenId = tokenCounter++;
    
    // Store mappings
    originalTicketId[poaTokenId] = ticketId;
    ticketToPOA[ticketId] = poaTokenId;
    
    // Mint the POA NFT
    _safeMint(attendee, poaTokenId);
    
    emit POAMinted(attendee, poaTokenId, ticketId);
    
    return poaTokenId;
}

Soulbound Implementation

POAs are non-transferable to preserve the integrity of attendance records:
ProofOfAttendance.sol
/**
 * @notice Override transfer functions to make POAs soulbound (non-transferable)
 * POAs should stay with the original attendee
 */
function _update(
    address to,
    uint256 tokenId,
    address auth
) internal override returns (address) {
    address from = _ownerOf(tokenId);
    
    // Allow minting (from == address(0)) but prevent transfers
    if (from != address(0) && to != address(0)) {
        revert("POAs are soulbound and non-transferable");
    }
    
    return super._update(to, tokenId, auth);
}
Soulbound tokens cannot be transferred, sold, or given away, ensuring they permanently represent the original attendee’s participation.

POA Metadata

Each POA includes rich metadata commemorating the event:
{
  "name": "Tech Conference 2024 - Proof of Attendance",
  "description": "This NFT certifies that the holder attended Tech Conference 2024 on June 15-17, 2024 at San Francisco Convention Center.",
  "image": "ipfs://QmXxxx.../poa-42.png",
  "animation_url": "ipfs://QmXxxx.../poa-42.mp4",
  "attributes": [
    {
      "trait_type": "Event",
      "value": "Tech Conference 2024"
    },
    {
      "trait_type": "Date",
      "value": "2024-06-15"
    },
    {
      "trait_type": "Venue",
      "value": "San Francisco Convention Center"
    },
    {
      "trait_type": "City",
      "value": "San Francisco"
    },
    {
      "trait_type": "Country",
      "value": "United States"
    },
    {
      "trait_type": "Original Ticket ID",
      "value": "42"
    },
    {
      "trait_type": "Check-In Time",
      "value": "2024-06-15T09:30:00Z"
    },
    {
      "trait_type": "Soulbound",
      "value": "Yes"
    }
  ]
}

Integration with Check-In

POA minting is integrated directly into the check-in flow:
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);
}

Querying POAs

Retrieve POA information for tickets and attendees:
/**
 * @notice Check if a POA exists for a given ticket ID
 * @param ticketId Original ticket ID
 * @return exists True if POA exists
 * @return poaTokenId The POA token ID (0 if doesn't exist)
 */
function getPOAForTicket(uint256 ticketId) 
    external 
    view 
    returns (bool exists, uint256 poaTokenId) 
{
    poaTokenId = ticketToPOA[ticketId];
    exists = poaTokenId != 0;
}

Database Integration

POA data is tracked in the CheckIn model:
schema.prisma
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")
}

Authorization Management

Control which contracts can mint POAs:
/**
 * @notice Authorize an address to mint POAs (typically event contracts)
 * @param minter Address to authorize
 */
function authorizeMinter(address minter) external onlyOwner {
    require(minter != address(0), "Invalid minter address");
    authorizedMinters[minter] = true;
    emit MinterAuthorized(minter);
}

Use Cases

Event Attendance Records

Build a verifiable history of events attended across multiple platforms

Professional Credentials

Prove attendance at conferences, workshops, and training sessions

Community Participation

Demonstrate active participation in community events and meetups

Access Gating

Use POAs to gate access to exclusive content or future events

Reputation Building

Accumulate POAs to build reputation within communities

Digital Memorabilia

Collect commemorative NFTs from memorable events

POA Portfolio

Attendees can view their complete POA collection:
const getUserPOAs = async (walletAddress: string) => {
  // Query all POAs owned by the user
  const checkIns = await prisma.checkIn.findMany({
    where: {
      user: {
        walletAddress
      },
      poaTokenId: {
        not: null
      }
    },
    include: {
      event: true,
      ticket: true
    },
    orderBy: {
      checkedInAt: 'desc'
    }
  })
  
  return checkIns.map(checkIn => ({
    poaTokenId: checkIn.poaTokenId,
    poaTxHash: checkIn.poaTxHash,
    eventTitle: checkIn.event.title,
    eventDate: checkIn.event.eventDate,
    venue: checkIn.event.venue,
    checkInTime: checkIn.checkedInAt,
    imageUrl: checkIn.event.imageUrl
  }))
}

Advanced Features

Dynamic Metadata

Generate metadata dynamically based on event details:
const generatePOAMetadata = (checkIn: CheckIn, event: Event) => {
  return {
    name: `${event.title} - Proof of Attendance`,
    description: `This NFT certifies that the holder attended ${event.title} on ${new Date(event.eventDate).toLocaleDateString()} at ${event.venue}.`,
    image: generatePOAImage(event, checkIn),
    attributes: [
      { trait_type: "Event", value: event.title },
      { trait_type: "Date", value: event.eventDate.toISOString() },
      { trait_type: "Venue", value: event.venue },
      { trait_type: "City", value: event.city },
      { trait_type: "Category", value: event.category },
      { trait_type: "Check-In Time", value: checkIn.checkedInAt.toISOString() },
      { trait_type: "Soulbound", value: "Yes" }
    ]
  }
}

Special Edition POAs

Create unique POAs for early attendees or special milestones:
// Mint special POA for first 100 attendees
function mintSpecialPOA(
    address attendee,
    uint256 ticketId,
    string memory specialEdition
) external onlyAuthorizedMinter returns (uint256) {
    uint256 poaTokenId = mintPOA(attendee, ticketId);
    
    // Store special edition metadata
    specialEditions[poaTokenId] = specialEdition;
    
    emit SpecialPOAMinted(attendee, poaTokenId, specialEdition);
    
    return poaTokenId;
}

POA Leaderboards

Rank attendees by number of events attended:
const getPOALeaderboard = async (limit: number = 100) => {
  const leaderboard = await prisma.user.findMany({
    where: {
      checkIns: {
        some: {
          poaTokenId: {
            not: null
          }
        }
      }
    },
    include: {
      checkIns: {
        where: {
          poaTokenId: {
            not: null
          }
        },
        include: {
          event: true
        }
      }
    },
    take: limit
  })
  
  return leaderboard
    .map(user => ({
      walletAddress: user.walletAddress,
      name: user.name,
      poaCount: user.checkIns.length,
      events: user.checkIns.map(c => c.event.title)
    }))
    .sort((a, b) => b.poaCount - a.poaCount)
}

Best Practices

Create distinct POA designs for each event to make them collectible and memorable.
Use professional artwork and design to make POAs valuable digital collectibles.
Include comprehensive event details in metadata for future reference and verification.
Host POA images and metadata on IPFS for decentralized, permanent storage.
Always verify ticket ownership and event timing before minting POAs.
Batch mint POAs when possible to reduce gas costs for large events.

Security Considerations

POA contracts should have strict authorization controls to prevent unauthorized minting.

Authorized Minters Only

Only authorized event contracts should be able to mint POAs

One POA Per Ticket

Enforce that each ticket can only mint one POA to prevent abuse

Immutable Ownership

Soulbound implementation prevents POA trading or transfers

Event Timing Checks

Verify event has started before allowing POA minting

Future Enhancements

1

Dynamic POA Artwork

Generate unique artwork for each POA based on check-in time, location, or other variables
2

POA Staking

Allow users to stake POAs to unlock benefits or prove long-term community participation
3

Cross-Event POA Rewards

Reward attendees who collect POAs from multiple related events
4

POA-Gated Access

Use POA ownership to gate access to exclusive content, communities, or future events
5

POA Reputation Scores

Calculate reputation scores based on POA collection and event participation

Next Steps

Ticket Verification

Learn how check-in triggers automatic POA minting

NFT Tickets

Understand the relationship between tickets and POAs

Build docs developers (and LLMs) love