Section 01 · Introduction
Why gyms are moving membership cards on chain
A traditional gym membership lives in three places that rarely agree — the front desk software, the billing system, and the member's wallet app. An NFT membership replaces those three with one chain entry.
Quick answer
How does a gym membership NFT work? A gym membership NFT is an ERC721 token where one tokenId represents one member's pass. The token carries the tier (basic, premium, family), an expiry timestamp, and an optional check-in counter. When the member arrives, the front desk scans their wallet, the contract verifies the pass is active, and the member is admitted. Renewal extends the same NFT instead of issuing a new card.
The current state of gym tech is the plastic card with a barcode plus a member record in a SaaS dashboard. Half the failures in that stack come from the two never staying in sync. A canceled member shows active. A renewed member shows expired. The front desk overrides manually. Nobody trusts the data.
An NFT membership solves the sync problem by making the chain the single source of truth. The expiry timestamp is on chain. The tier is on chain. The check-in counter is on chain. The front desk reads from the same place the member reads from. Drift becomes impossible because there is only one source.
This guide walks through the actual Solidity contract a small chain of gyms can ship in a weekend. The companion piece on building an ERC721 NFT in Remix covers the base contract this extends.
The NFT is the membership, the metadata is the tier
One contract handles every member at every tier. The tier lives in the Membership struct attached to each tokenId. Adding a new tier later is a metadata change, not a new contract deploy.
Section 02 · The Contract
One ERC721 contract, one struct per member
The whole membership system is a thin extension of OpenZeppelin's ERC721 — fewer than 60 lines of Solidity for the core.
// Gym membership NFT — ERC721 with expiry and tier
contract GymMembership is ERC721, Ownable {
uint256 public nextId;
struct Membership {
uint8 tier; // 1=basic, 2=premium, 3=family
uint64 expiresAt; // unix timestamp
uint32 checkInsLeft; // for class packs, 0 = unlimited
}
mapping(uint256 => Membership) public membership;
event MembershipMinted(uint256 indexed id, address indexed to, uint8 tier, uint64 expiresAt);
event MembershipRenewed(uint256 indexed id, uint64 newExpiry);
event CheckedIn(uint256 indexed id, address indexed by);
constructor() ERC721("GymPass", "GYM") Ownable(msg.sender) {}
function mint(address to, uint8 tier, uint64 expiresAt, uint32 checkInsLeft)
external
onlyOwner
returns (uint256 id)
{
id = ++nextId;
membership[id] = Membership(tier, expiresAt, checkInsLeft);
_safeMint(to, id);
emit MembershipMinted(id, to, tier, expiresAt);
}
function isActive(uint256 id) public view returns (bool) {
Membership memory m = membership[id];
return m.expiresAt > block.timestamp && (m.checkInsLeft > 0 || m.checkInsLeft == type(uint32).max);
}
}The Membership struct packs into one storage slot: a tier byte, an expiry timestamp, and a check-in counter. Cheap to write, cheap to read. Reading isActive(id) costs one storage load — well under one cent of gas on any modern L2.
The mint function is owner only because in this model the gym is the issuer. A member signs up at the front desk or through the gym's app, pays in fiat or stablecoin, and the gym mints the pass to the member's wallet. Self mint is also possible if the gym wants to wire a public payable function — the structure does not change.
Section 03 · Check-In and Renewal
What happens when a member walks in
The check-in flow is two reads and one write. It runs in well under a second on any L2 and costs the member nothing if the gym sponsors gas.
// Front desk scans the wallet, calls checkIn
function checkIn(uint256 id) external {
require(ownerOf(id) == msg.sender, "Gym: not your pass");
Membership storage m = membership[id];
require(m.expiresAt > block.timestamp, "Gym: expired");
if (m.checkInsLeft != type(uint32).max) {
require(m.checkInsLeft > 0, "Gym: no check-ins left");
m.checkInsLeft -= 1;
}
emit CheckedIn(id, msg.sender);
}
// Renewal — owner takes payment off chain, extends expiry
function renew(uint256 id, uint64 newExpiry, uint32 addCheckIns) external onlyOwner {
Membership storage m = membership[id];
require(newExpiry > m.expiresAt, "Gym: cannot shorten");
m.expiresAt = newExpiry;
if (addCheckIns > 0 && m.checkInsLeft != type(uint32).max) {
m.checkInsLeft += addCheckIns;
}
emit MembershipRenewed(id, newExpiry);
}The front desk app reads the member's wallet, looks up their tokenId, and asks them to sign a checkIn call. If the gym wants zero friction, a paymaster sponsors the gas through ERC-4337 account abstraction. The member taps their phone, the door opens, they never see a transaction prompt.
Renewal is even simpler. When the member pays for another month, the gym calls renew with a later expiry timestamp. The tokenId stays the same. The member never gets a new pass. The history of every renewal is visible on chain through the MembershipRenewed events.
Class packs map to checkInsLeft
A 10 class pack mints with checkInsLeft = 10. Each check-in decrements. When the counter hits zero, isActive returns false. The gym can renew by adding more check-ins or extending the expiry — the same NFT serves both monthly and pack memberships.
Unlimited membership uses a sentinel value
checkInsLeft = type(uint32).max signals unlimited. The decrement step is skipped. This avoids a separate isUnlimited flag and saves a storage slot.
Frozen membership uses expiry = 0
Health issue or paused payment — the gym sets expiry to 0. isActive returns false. When the member returns, renew with a fresh expiry. The NFT keeps its history.
Section 04 · Transfer Rules
When members can gift, sell, or trade their membership
The default ERC721 lets any holder transfer their NFT to anyone. A gym usually wants that for general passes and wants it disabled for personal training or family plans.
// Optional — block transfer for personal training plans, allow for general pass
function _beforeTokenTransfer(address from, address to, uint256 id, uint256)
internal
override
{
if (from == address(0) || to == address(0)) return; // mint/burn ok
require(membership[id].tier != 3, "Gym: family pass non transferable");
}The transfer hook lets the gym express the policy directly in the contract. A general pass is freely transferable — a member who moves cities can sell their remaining months to a friend. A family plan is soulbound because it is tied to the household. A personal training package locks transfer because the trainer scheduled around a specific person.
| Tier | Transferable | Check-ins | Best for |
|---|---|---|---|
| Basic monthly | Yes — can gift or resell | Unlimited | Single member, flexible |
| Class pack (10) | Yes — can split with friends | 10 then expires | Drop in users |
| Premium with PT | Owner unbind only | Unlimited + 4 PT sessions | Long term members |
| Family plan | Soulbound | Shared across 4 members | Households |
Section 05 · Why Bother
What an NFT membership actually buys you over a plastic card
The technical work is small. The operational wins are bigger than they look from the outside.
One source of truth across multiple gym locations
A chain of 12 gyms with a single contract means every location reads the same membership state. No nightly sync. No location specific cards. A member who signs up in Lahore checks in to the Karachi branch on the first try.
Transferable membership is a feature, not a leak
Members who move or stop using the gym recover some value by selling their remaining time to someone else. That increases willingness to commit to a longer plan up front, because the lock-in feels reversible.
Cross gym partnerships work without integrations
A boxing gym and a yoga studio both issue ERC721 memberships from their own contracts. Either gym can accept the other's pass at the door by reading isActive on the partner contract. The integration is a single read call.
The data trail is honest by default
Every check-in is a CheckedIn event. The gym owner sees real usage per member per location without trusting a SaaS provider's dashboard. Audit and tax reporting become a query against on chain logs.
The member does not need to understand crypto
A custodial wallet inside the gym app holds the NFT for members who do not want to manage keys. Power users can self custody. Both groups get the same pass. The contract does not care.
Section 06 · Integration
What to build alongside the contract
The Solidity is one weekend of work. The pieces around it are where the real product lives.
A small front desk app reads the tokenId for a scanned wallet, displays the tier and remaining time, and lets staff trigger renewals after the member pays. The app talks to the contract through a public RPC for reads and through a relayer for writes so the front desk staff never need ETH.
A member facing app shows the NFT pass with a QR code that maps to the member's wallet address. Tap the phone, the door scanner reads the QR, the contract confirms isActive, the door opens. For gyms that already have a turnstile system, the integration is a single API endpoint that calls isActive(id).
Picking a chain matters. A small gym should use Polygon, Base, or Arbitrum — check-ins cost fractions of a cent. Mainnet Ethereum would price out the whole model. The contract code is identical across EVM chains.
Section 07 · FAQ
Common questions about gym membership NFTs
The questions gym owners ask before they switch from plastic cards to on chain memberships.
How does a gym membership NFT work?
It is an ERC721 token where each tokenId represents one member's pass with a tier, an expiry date, and an optional check-in counter. The member's wallet holds the pass. The gym front desk scans the wallet at check-in, the contract verifies the pass is active, and the member is admitted. Renewals extend the same NFT instead of issuing a fresh card.
Can members sell or transfer their gym membership?
That is a policy choice the gym sets in the contract. A general pass can be freely transferable, which lets members recover value by selling unused months. A family plan or personal training package can be soulbound — the transfer hook reverts on any non mint, non burn move. Both policies are one if statement in the contract.
What happens when a member's membership expires?
The expiry timestamp on chain is in the past, so isActive returns false. The front desk reads false and denies entry. The NFT itself is never burned — it stays in the member's wallet as a historical record. When the member renews, the same tokenId gets a fresh expiry timestamp and goes active again.
Do gym members need to know crypto to use this?
No. The gym's app can hold a custodial wallet on behalf of the member, exactly like a username and password account. The member sees a membership card and a check-in button. Power users who want self custody can claim their NFT to their own wallet at any time. The contract serves both groups identically.
How much does it cost a gym to run this?
On a layer 2 like Polygon or Base, a check-in costs well under one cent. A mint costs around two cents. A renewal costs about one cent. A gym with 1,000 active members and four check-ins per member per week pays roughly fifteen to twenty dollars per month in gas — and zero in card printing, card replacement, or sync incidents.
Section 08 · Next Steps
Ship the pass before you ship the marketing
A membership NFT is a small contract with a big operational footprint. Get the contract right and most of the integration work follows naturally.
We help fitness chains, coworking spaces, and subscription venues ship production grade on chain membership systems, including the contract, the front desk app, the wallet integration, and the L2 deployment. The same pattern applies far beyond gyms — any venue that issues a renewable pass benefits from the model.