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.
// 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.
// 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"
);
}
}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.
| Pattern | Holder friction | Scalp resistance | Production use |
|---|---|---|---|
| Freeze window | Low — resale allowed until 48h before event | Medium — wholesalers can still flip early | Most stadium concerts |
| Resale price cap | Medium — cannot sell above cap | High — removes the spread | Artist controlled tours |
| Soulbound | High — cannot transfer at all | Maximum — every holder must attend personally | Festivals, 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.
// 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.
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.