NFT Temporary Access System Development
Most projects that want to implement temporary access via NFT run into the same problem: ERC-721 originally has no concept of "expiration." A token either exists in your wallet or it doesn't. Expiration date is additional logic that must be embedded correctly, otherwise you get either a gas swamp or a race condition between checking and execution.
Temporary Access Architecture
ERC-5643: Subscription Standard
In 2022, ERC-5643 emerged — an extension of ERC-721 specifically for subscription NFTs. The standard adds two key methods:
interface IERC5643 {
event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration);
function renewSubscription(uint256 tokenId, uint64 duration) external payable;
function cancelSubscription(uint256 tokenId) external payable;
function expiresAt(uint256 tokenId) external view returns (uint64);
function isRenewable(uint256 tokenId) external view returns (bool);
}
expiresAt returns the unix timestamp when a subscription for a specific token expires. Storage: mapping(uint256 => uint64). uint64 is sufficient for timestamps thousands of years into the future and fits in one storage slot with other packed variables.
Critical detail: expiresAt is a view function, it doesn't block transfers. If you need to prevent transfer of expired tokens at the contract level, override _beforeTokenTransfer in OpenZeppelin ERC-721:
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if (from != address(0) && to != address(0)) {
// Block transfer of expired tokens
require(
block.timestamp < _expirations[tokenId],
"Subscription expired"
);
}
}
Alternative: allow transfer of expired tokens but deny access. This depends on business model — sometimes useful to transfer the token and renew subscription under new owner.
Off-chain Access Verification
On-chain state is source of truth. But calling expiresAt on every HTTP request is slow. Standard architecture:
Backend middleware reads contract state via multicall on first access, caches result in Redis with TTL equal to subscription expiry. On attempt to access protected resource:
- User signs message (EIP-4361 Sign-In With Ethereum)
- Backend verifies signature, extracts wallet address
- Checks Redis cache → if miss, queries contract
- If
expiresAt(tokenId) > block.timestamp— issue JWT with expiry = min(subscription_expiry, JWT_max_age)
JWT invalidates itself when it expires. No need for blacklist if JWT TTL is aligned with subscription expiry.
Renewal and Payment
renewSubscription accepts duration in seconds and ETH/token for payment. Important nuance: renewal should add to current expiry, not block.timestamp:
function renewSubscription(uint256 tokenId, uint64 duration) external payable {
require(ownerOf(tokenId) == msg.sender, "Not owner");
require(msg.value >= _price * duration / 30 days, "Insufficient payment");
uint64 current = _expirations[tokenId];
// If subscription expired — renew from current moment
// If still active — add to existing expiry
uint64 newExpiry = (current < uint64(block.timestamp))
? uint64(block.timestamp) + duration
: current + duration;
_expirations[tokenId] = newExpiry;
emit SubscriptionUpdate(tokenId, newExpiry);
}
This is critical for user: if they renew active subscription for a month, they don't lose remaining days.
Soulbound vs. Transferable
Choice between non-transferable (ERC-5192, Soulbound) and transferable access is architectural, not technical. Soulbound convenient for personalized subscriptions (courses, licenses for specific person). Transferable — for corporate licenses or when resale of access is part of model.
ERC-5192 implemented simply: locked() returns true, all transfer functions revert. OpenZeppelin 5.x added ERC721Votes and ERC721Enumerable as extensions — Soulbound implemented similarly via _update hook.
Stack and Integration
Solidity 0.8.20+ with Foundry. ERC-5643 + ERC-5192 (optionally). Off-chain: Node.js/TypeScript, viem for contract reading, Redis for access cache, JWT (jose) for sessions. Frontend: wagmi + ConnectKit for wallet connection, react-query for subscription state.
For ERC-20 payment (USDC/DAI) add Permit2 — user signs approval and renewSubscription call in one operation, no preliminary approve.
Timeline Estimates
Basic contract with ERC-5643 + backend middleware for access verification + frontend subscription management component — 3-4 days. With Permit2 payment, multi-tier access (several pricing tiers), and renewal analytics — 5-7 days.







