BlockchainSolidity11 min readUpdated

NFT Ticketing on ERC721: Event Access and Anti Scalp Patterns

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: NFT Ticketing on ERC721: Event Access and Anti Scalp Patterns

Section 01 · Introduction

Why ERC721 is the right shape for event tickets

A paper ticket has a holder, a unique serial number, and a redeem state. An ERC721 NFT has exactly the same properties on chain, plus programmable transfer rules.

Quick answer

How do NFT tickets work? An NFT ticket is an ERC721 token whose tokenId corresponds to a specific seat or pass at a specific event. The wallet that owns the token at door scan time is the wallet that gets in. The contract enforces transfer rules — price caps, blackout windows, or full transfer locks — directly on chain, which is how production NFT ticketing platforms prevent scalping.

The ticketing industry runs on two failure modes. The first is counterfeiting. The second is uncapped resale: a wholesaler buys 500 tickets at $200, then sells them for $1,500 on a secondary marketplace and pockets the spread. Promoters and artists hate that. Fans hate it more.

ERC721 attacks both problems at the same time. Counterfeiting becomes impossible because the canonical owner record lives on chain. Uncapped resale becomes programmable because the contract decides which transfers are allowed. Neither property requires the user to understand a blockchain — the wallet sits inside a regular ticketing app.

The companion guide on creating an ERC721 NFT in Remix walks through the base contract. This post covers the additional surface a production ticketing contract ships on top.

The ticket is the NFT, the seat is the metadata

Most NFT ticket contracts use one tokenId per ticket. The seat number, the event id, and the tier live in metadata returned by tokenURI. A single contract can issue tickets for a season's worth of events because the constraints are per token, not per contract.

Section 02 · Contract Shape

The minimum surface for a ticket NFT

A production ticket contract extends ERC721 with three pieces of state: a per token event record, a used flag, and a verifier role.

solidity
// Minimal NFT ticket contract — ERC721 plus event metadata and a used flag
contract EventTicket is ERC721, AccessControl {
    bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE");

    struct Event {
        uint64 eventStart;
        uint64 transferFreezeAt;   // block.timestamp after which transfers revert
        uint128 maxResalePriceWei; // 0 means no resale cap
        bool soulbound;            // true means tokens cannot transfer at all
    }

    mapping(uint256 => Event) public ticketEvent;     // tokenId => event
    mapping(uint256 => bool) public used;             // tokenId => redeemed flag
    mapping(uint256 => uint256) public lastResalePrice; // tokenId => last sale price

    constructor() ERC721("Event Ticket", "TKT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(
        address to,
        uint256 tokenId,
        Event calldata ev
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        ticketEvent[tokenId] = ev;
        _safeMint(to, tokenId);
    }
}

The Event struct compresses the policy for each tokenId: when the event starts, when transfers freeze, whether resale is capped, whether the ticket is soulbound. Packing these into a single storage slot keeps mint cheap and the policy attached to the token itself rather than a separate config registry.

AccessControl gives us a clean way to express the verifier role: the wallets allowed to sign redemption messages at the door. In a production deployment those wallets live inside a hardware POS device at the venue entrance, separate from the deployer wallet that mints tickets.

Mint is the wrong place to put scalp prevention

A common mistake is enforcing one ticket per wallet at mint time. Wholesalers route around this trivially with 500 fresh addresses. The scalp prevention has to live on the transfer hook, where the secondary market actually happens.

Section 03 · Transfer Hook

The anti scalp policy lives in _beforeTokenTransfer

OpenZeppelin's _beforeTokenTransfer runs on every mint, transfer, and burn. It is the one place where the contract can refuse to move the token.

solidity
// Anti scalp transfer hook — runs on every mint, transfer, and burn
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId,
    uint256 batchSize
) internal override {
    super._beforeTokenTransfer(from, to, tokenId, batchSize);

    // Mint and burn are always allowed
    if (from == address(0) || to == address(0)) return;

    Event memory ev = ticketEvent[tokenId];

    // Soulbound tickets cannot transfer at all
    require(!ev.soulbound, "Ticket: soulbound");

    // Freeze transfers in the resale blackout window before the event
    require(block.timestamp < ev.transferFreezeAt, "Ticket: resale frozen");

    // Used tickets cannot be resold
    require(!used[tokenId], "Ticket: already redeemed");

    // Resale price cap — caller must declare price via setResalePrice first
    if (ev.maxResalePriceWei > 0) {
        require(
            lastResalePrice[tokenId] <= ev.maxResalePriceWei,
            "Ticket: above resale cap"
        );
    }
}
Decision tree diagram showing a ticket holder attempting a transfer. The flow branches through four gates: is the ticket soulbound, are we inside the freeze window, is the ticket already redeemed, and does the declared resale price exceed the cap. Any gate failing causes the transfer to revert; passing all four allows the transfer to proceed.
The four gates a ticket transfer passes through. Each gate corresponds to one production scalp prevention pattern.

The freeze window is the most common pattern. The contract allows resale up to 48 hours before the event, then locks all transfers. The intent is to make wholesale resale impossible right when scalp demand peaks. Real holders who cannot attend at the last minute lose some flexibility; the contract owner sets the window to balance that against scalp prevention.

The resale price cap is the next most common pattern. The contract reads a declared price from the latest setResalePrice call and reverts if it exceeds the cap. This requires marketplace cooperation — the marketplace must call setResalePrice before the transfer. The contract cannot read the actual buyer payment otherwise.

Soulbound is the nuclear option. The transfer hook reverts on every non mint, non burn move. Each holder must show up themselves. Festivals and small venue tours use this. Stadium concerts almost never do because the resale flexibility is a feature for legitimate holders.

Three anti scalp patterns, ranked by how much they constrain legitimate holders.
PatternHolder frictionScalp resistanceProduction use
Freeze windowLow — resale allowed until 48h before eventMedium — wholesalers can still flip earlyMost stadium concerts
Resale price capMedium — cannot sell above capHigh — removes the spreadArtist controlled tours
SoulboundHigh — cannot transfer at allMaximum — every holder must attend personallyFestivals, intimate venues

Section 04 · Redemption

Door scan with an EIP-712 signature

When the holder arrives at the venue, the door device signs a message authorizing redemption. The contract flips the used flag and rejects any future redeem call for the same tokenId.

solidity
// Door scan flow — verifier signs an EIP-712 message, ticket is marked used
struct RedeemPayload {
    uint256 tokenId;
    address holder;
    uint256 nonce;
    uint64 deadline;
}

function redeem(RedeemPayload calldata p, bytes calldata sig) external {
    require(block.timestamp <= p.deadline, "Ticket: signature expired");
    require(ownerOf(p.tokenId) == p.holder, "Ticket: holder mismatch");
    require(!used[p.tokenId], "Ticket: already used");

    bytes32 digest = _hashTypedDataV4(
        keccak256(abi.encode(
            keccak256("RedeemPayload(uint256 tokenId,address holder,uint256 nonce,uint64 deadline)"),
            p.tokenId,
            p.holder,
            p.nonce,
            p.deadline
        ))
    );
    address signer = ECDSA.recover(digest, sig);
    require(hasRole(VERIFIER_ROLE, signer), "Ticket: bad verifier signature");

    used[p.tokenId] = true;
    emit Redeemed(p.tokenId, p.holder, signer);
}

The flow starts with the door device generating a one time EIP-712 payload tied to the tokenId, the current holder, a nonce, and a short deadline. The device signs the payload with its verifier key. The holder's ticketing app reads the signature, calls redeem on chain, and the contract verifies that the signature came from an authorized verifier wallet.

Six step flow diagram: buyer purchases ticket and receives the ERC721 NFT, optional secondary resale within the allowed window, holder arrives at venue, door device scans the wallet and produces an EIP-712 verifier signature, holder calls redeem with the signature, the contract marks the tokenId used and emits an event consumed by the venue access system.
Mint, optional resale, door scan, and on chain redemption. The used flag is the single source of truth for whether a ticket has been claimed.

Two patterns reduce friction at the door. The first is sponsoring gas on the redeem transaction so the holder does not need ETH in their wallet to enter the venue. The second is using account abstraction (ERC-4337) so the holder signs a userOp inside the ticketing app and a paymaster covers the on chain cost.

Verifier keys live inside the door hardware

The verifier signing key never leaves the POS device at the entrance. If the device is stolen, the issuer rotates the verifier role on chain. The signature scheme limits the blast radius — a stolen key can only mark tickets used at the venue it was deployed to.

The used flag is one way

Once flipped, the used flag cannot be cleared. This prevents a venue operator from reusing the same NFT for two holders. If the holder enters and leaves and tries to re enter, the second redeem reverts. The exit and re entry policy lives off chain at the venue.

Post event the NFT keeps utility

After redemption the token still exists in the holder's wallet. The contract can flip tokenURI to a post event collectible image, a video drop, or a future tour pre sale discount. The same NFT covers ticket, memorabilia, and customer relationship.

Section 05 · Royalties

Secondary sales become a revenue line

An ERC721 ticket contract that implements ERC-2981 declares a royalty percentage. Marketplaces that honor it route a slice of every secondary sale back to the issuer.

Royalty enforcement on chain is contested. Some marketplaces respect ERC-2981. Others ignore it. The current direction of travel is to use OpenSea's operator filter or a custom transfer hook that only allows transfers through marketplaces that pay royalties. The pattern is becoming standard for production ticketing contracts that care about the resale revenue tail.

The combined effect: a single contract can capture primary sale revenue (mint), enforce resale rules on the secondary market (transfer hook), redeem at the venue (used flag), and earn royalties on every collectible trade after the event. That stack does not exist on a paper ticket.

The economic alignment shifts to the artist

Under the traditional ticketing model, the artist sees a fixed fee from primary sale and zero from secondary. Under an NFT ticketing model with capped resale plus royalties, the artist captures a slice of every transaction in the token's life. The default direction of revenue moves toward the creator.

Section 06 · Integration

How to integrate an NFT ticket into a venue stack

The contract is half the system. The other half is the door hardware, the holder app, and the venue access database — and the integration points need to be designed for the day the network is slow.

Treat the chain as eventually consistent at the door. A redeem transaction takes a few seconds to confirm even on a fast L2. The door system should verify the verifier signature locally, admit the holder, and only commit the on chain redeem afterward. If the chain is congested, the holder is already inside and the contract catches up.

Use a layer 2 for the actual deployment. Base, Optimism, and Polygon all run with cents per redemption. Mainnet Ethereum at typical gas would price out every concert under $200 per ticket.

Keep an off chain mirror of redemption state for analytics and customer support. The chain is the source of truth, but you do not want every refund request to require an RPC roundtrip.

Section 07 · FAQ

Common questions about ERC721 NFT tickets

The questions venue operators, promoters, and engineers ask before shipping the first event.

How do NFT tickets work?

An NFT ticket is an ERC721 token where the tokenId corresponds to a specific seat or pass at a specific event. The wallet that owns the token at scan time is the wallet that gets in. The contract enforces transfer rules and redemption on chain so the ticket cannot be counterfeited or reused.

Can NFT tickets be transferred?

Yes, unless the contract sets the soulbound flag. Most production deployments allow transfers up to a freeze window before the event, optionally cap the resale price, and revert any transfer that violates either rule. Soulbound tickets disable transfer entirely.

What stops NFT ticket scalping?

Three patterns: a freeze window that blocks transfers in the hours before the event, a resale price cap enforced in the transfer hook, and soulbound tokens that cannot be moved at all. Most stadium tours combine the freeze window with a moderate resale cap to keep flexibility for legitimate holders while removing the wholesale spread.

How is an NFT ticket redeemed at the venue?

A door device signs an EIP-712 payload tied to the tokenId, the holder, and a short deadline. The holder calls redeem on the contract with the signature. The contract verifies the signer is an authorized verifier, marks the used flag for that tokenId, and emits a Redeemed event. The flag cannot be cleared, so the same ticket cannot be redeemed twice.

What happens to an NFT ticket after the event?

The NFT remains in the holder's wallet with the used flag set. The contract can swap tokenURI to a post event collectible — a video drop, a memorabilia image, or a discount code for the next tour. Many issuers earn additional revenue through ERC-2981 royalties on secondary collectible sales after the event.

Section 08 · Next Steps

Ship the ticket as a product, not a contract

The Solidity is the easy part. The harder work is the door hardware integration, the holder app, the customer support tooling, and the L2 choice.

We help teams ship production grade NFT ticketing and access systems, including the contract, the door integration, the verifier key management, and the L2 deployment. A second deep dive on a related ERC721 use case lives at the real estate tokenization post — the contract patterns share more than they differ.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View blockchain development serviceSee ChainTrust case study

Related service

Blockchain Development

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →