Permits are the on-chain representation of tickets in TIX Protocol. Each permit is a unique, verifiable record of ticket ownership.
Permit Structure
structPermit{owner:Pubkey,// Current ticket holder's walletstatus:u8,// 0=Active, 1=Used, 2=Voidversion:u64,// Increments on every changeticket_id:u64,// Unique identifier within eventpadding:[u8;16],// Reserved for listing lock data}
Status Values
Status
Value
Description
Allowed Actions
Active
0
Ticket is valid
Transfer, list, resell
Used
1
Ticket has been redeemed
None (terminal state)
Void
2
Ticket has been cancelled
None (terminal state)
Paged Storage
Permits are stored in contiguous vectors within PermitPage accounts, minimizing account creation cost.
Storage Layout
Page Calculation
Page Initialization
On first write to a page, the program allocates a vector of 128 default permits:
Version Tracking
Every permit has a version number that increments on any mutation:
Operation
Version Increment
issue_permit
0 → 1
transfer_permit
+1
mark_used
+1
mark_void
+1
accept_offer
+1
Purpose
Version tracking prevents stale operations:
Listing Version Mismatch — When a listing is created, it stores the current permit version. If the permit changes before purchase, the sale is rejected.
Race Condition Prevention — Concurrent transfer attempts are serialized by version checks.
Listing Locks
When a ticket is listed for sale, the permit is "locked" to prevent simultaneous transfers.
Lock Mechanism
The permit's padding area stores lock information:
Byte Range
Purpose
padding[0]
Lock flag (0=unlocked, 1=locked)
padding[1..9]
Expiry timestamp (i64 little-endian)
Lock Flow
Expiry Handling
If a listing expires:
transfer_permit can proceed (checks expiry timestamp)
accept_offer will fail with ListingExpired
cancel_listing can clean up the stale listing
Guard Rails
TicketAlreadyMinted
Prevents re-issuing the same ticket slot:
SupplyExceeded
Prevents minting beyond event capacity:
PermitNotActive
Blocks operations on used/void tickets:
ListingActive
Prevents transfers while listing is active:
Best Practices
For Integrators
Batch Issuance — Issue multiple tickets in a single transaction when possible
Page Pre-allocation — Consider ticket ID assignment in your own database to minimize page creation
Version Caching — Store permit versions when creating listings
For Clients
Always Fetch Fresh — Get latest permit state before operations
Handle Version Errors — Retry with updated version on mismatch
Check Lock Status — Verify permit isn't locked before transfer attempts
const PERMITS_PER_PAGE = 128;
function getPageForTicket(ticketId: bigint): bigint {
return ticketId / BigInt(PERMITS_PER_PAGE);
}
function getLocalIndex(ticketId: bigint): number {
return Number(ticketId % BigInt(PERMITS_PER_PAGE));
}
// Example: Ticket ID 300
// Page: 300 / 128 = 2
// Local index: 300 % 128 = 44
// When page doesn't exist
permits: vec![Permit::default(); PERMITS_PER_PAGE]
// Listing stores expected version
listing.permit_version = permit.version;
// On accept_offer, version must match
if (permit.version !== listing.permit_version) {
throw Error("ListingVersionMismatch");
}
1. list_ticket
└── Sets padding[0] = 1
└── Stores expiry in padding[1..9]
2. While locked:
└── transfer_permit blocked (unless listing expired)
└── Only accept_offer or cancel_listing can proceed
3. cancel_listing / accept_offer
└── Clears padding[0] = 0
└── Permit becomes transferable again
if permit.version > 0 {
return Err(ErrorCode::TicketAlreadyMinted);
}
if ticket_id >= event.total_supply {
return Err(ErrorCode::SupplyExceeded);
}
if permit.status != PermitStatus::Active {
return Err(ErrorCode::PermitNotActive);
}
if permit.padding[0] == 1 && !listing_expired {
return Err(ErrorCode::ListingActive);
}