Tradable Protocol
The Tradable Protocol enables orderbook-style trading of badges through standardized bid and listing approvals. This protocol supports both fungible and non-fungible badge types with coin-based transactions.
Protocol Overview
The Tradable Protocol creates a decentralized marketplace where users can:
List badges for sale at specific prices
Place bids to buy badges from other users
Support both NFT and fungible badges with flexible trading rules
Execute trades through standardized approval mechanisms
Protocol Requirements
Collection Standards
Must include "Tradable" in the
standardsTimeline
for the current time period. Note this often also goes well with "NFT" standard if your collection is NFTs.Compatible with all badge types (fungible and non-fungible)
No restrictions on badge ID ranges or quantities
Approval Types
1. Listing Approvals (Outgoing)
Listings allow badge owners to sell their badges for coins.
Requirements:
Single transfer time range
Exactly one coin transfer with one coin denomination
Coin recipient must be the badge owner (
to
equalsfromListId
)No address overrides (
overrideFromWithApproverAddress
andoverrideToWithInitiator
must be false)Specific badge IDs (no
allowOverrideWithAnyValidBadge
)Full ownership times
No Merkle challenges or prerequisite badges
overallMaxNumTransfers
> 0Typically, you want the denomination to match the collection's preferred denomination.
2. Bid Approvals (Incoming)
Bids allow users to offer coins to purchase badges from others.
Requirements:
Single transfer time range
Exactly one coin transfer with one coin denomination
Coins come from bidder (
overrideFromWithApproverAddress
must be true)Coins go to badge owner (
overrideToWithInitiator
must be true)Specific badge IDs (no
allowOverrideWithAnyValidBadge
unless collection bid)Full ownership times
No Merkle challenges or prerequisite badges
overallMaxNumTransfers
> 0
3. Collection Bids (Special Case)
Collection bids allow users to bid on any badge within a collection.
Additional Requirements:
Must have
allowOverrideWithAnyValidBadge
set to trueAll other bid requirements apply
Validation Functions
General Orderbook Validation
API Documentation: isOrderbookBidOrListingApproval
export const isOrderbookBidOrListingApproval = (
approval: iCollectionApproval<bigint>,
approvalLevel: 'incoming' | 'outgoing'
) => {
return isBidOrListingApproval(approval, approvalLevel, {
isFungibleCheck: true,
fungibleOrNonFungibleAllowed: true,
});
};
Core Bid/Listing Validation
API Documentation: isBidOrListingApproval
export const isBidOrListingApproval = (
approval: iCollectionApproval<bigint>,
approvalLevel: 'incoming' | 'outgoing',
options?: {
isFungibleCheck?: boolean;
fungibleOrNonFungibleAllowed?: boolean;
isCollectionBid?: boolean;
}
) => {
const approvalCriteria = approval.approvalCriteria;
if (approvalCriteria?.coinTransfers?.length !== 1) {
return false;
}
if (approval.transferTimes.length !== 1) {
return false;
}
const coinTransfer = approvalCriteria.coinTransfers[0];
if (coinTransfer.coins.length !== 1) {
return false;
}
// Validate address overrides for incoming approvals (bids)
if (
approvalLevel === 'incoming' &&
!coinTransfer.overrideFromWithApproverAddress
) {
return false;
}
if (approvalLevel === 'incoming' && !coinTransfer.overrideToWithInitiator) {
return false;
}
// Validate address overrides for outgoing approvals (listings)
if (
approvalLevel === 'outgoing' &&
coinTransfer.overrideFromWithApproverAddress
) {
return false;
}
if (approvalLevel === 'outgoing' && coinTransfer.overrideToWithInitiator) {
return false;
}
// For listings, recipient must be the approving user
const to = coinTransfer.to;
if (approvalLevel === 'outgoing' && to !== approval.fromListId) {
return false;
}
const incrementedBalances =
approvalCriteria.predeterminedBalances?.incrementedBalances;
if (!incrementedBalances) {
return false;
}
if (incrementedBalances.startBalances.length !== 1) {
return false;
}
// Collection bids can accept any valid badge
if (options?.isCollectionBid) {
if (!incrementedBalances.allowOverrideWithAnyValidBadge) {
return false;
}
} else {
const allBadgeIds = UintRangeArray.From(
incrementedBalances.startBalances[0].badgeIds
)
.sortAndMerge()
.convert(BigInt);
if (allBadgeIds.length !== 1 || allBadgeIds.size() !== 1n) {
return false;
}
if (incrementedBalances.allowOverrideWithAnyValidBadge) {
return false;
}
}
const amount = incrementedBalances.startBalances[0].amount;
const toCheckAmountOne =
!options ||
(!options.isFungibleCheck && !options.fungibleOrNonFungibleAllowed);
if (toCheckAmountOne) {
if (amount !== 1n) {
return false;
}
}
if (
!UintRangeArray.From(
incrementedBalances.startBalances[0].ownershipTimes
).isFull()
) {
return false;
}
if (incrementedBalances.incrementBadgeIdsBy !== 0n) {
return false;
}
if (incrementedBalances.incrementOwnershipTimesBy !== 0n) {
return false;
}
if (incrementedBalances.durationFromTimestamp !== 0n) {
return false;
}
if (incrementedBalances.allowOverrideTimestamp) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.startTime !== 0n) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.intervalLength !== 0n) {
return false;
}
if (incrementedBalances.recurringOwnershipTimes.chargePeriodLength !== 0n) {
return false;
}
if (approvalCriteria.requireFromEqualsInitiatedBy) {
return false;
}
if (approvalCriteria.requireToEqualsInitiatedBy) {
return false;
}
if (approvalCriteria.overridesToIncomingApprovals) {
return false;
}
if (approvalCriteria.merkleChallenges?.length) {
return false;
}
if (approvalCriteria.mustOwnBadges?.length) {
return false;
}
if (
(approvalCriteria.maxNumTransfers?.overallMaxNumTransfers ?? 0n) === 0n
) {
return false;
}
return true;
};
Collection Bid Validation
API Documentation: isCollectionBid
export const isCollectionBid = (approval: iCollectionApproval<bigint>) => {
return isBidOrListingApproval(approval, 'incoming', {
isCollectionBid: true,
});
};
Implementation Example
For a complete implementation example, see the Tradable NFT Collection Example.
Last updated