Filter NFTs by their attributes—essential for building inventory filters and marketplace search.
Use Cases
Scenario Query Inventory filter Show only “Legendary” rarity items Marketplace search Find swords with attack > 50 Crafting UI Display items of type “Material” Leaderboards Rank by numeric attribute
How It Works
NFT metadata follows a standard structure:
{
"name" : "Dragon Slayer" ,
"description" : "A legendary sword" ,
"image" : "https://..." ,
"attributes" : [
{ "trait_type" : "Rarity" , "value" : "Legendary" },
{ "trait_type" : "Type" , "value" : "Sword" },
{ "trait_type" : "Attack" , "value" : 85 , "display_type" : "number" },
{ "trait_type" : "Element" , "value" : "Fire" }
]
}
The Indexer indexes these attributes, allowing you to query by them.
Query Syntax
Filter by String Attribute
import { BlockchainData } from '@imtbl/blockchain-data' ;
import { Environment } from '@imtbl/config' ;
const indexer = new BlockchainData ({
baseConfig: {
environment: Environment . SANDBOX ,
publishableKey: 'YOUR_PUBLISHABLE_KEY' ,
},
});
// Find all Legendary items
const { result } = await indexer . listNFTs ({
chainName: 'imtbl-zkevm-testnet' ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters: [
{ key: 'Rarity' , value: 'Legendary' }
],
});
Filter by Multiple Attributes
// Find Legendary Swords
const { result } = await indexer . listNFTs ({
chainName: 'imtbl-zkevm-testnet' ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters: [
{ key: 'Rarity' , value: 'Legendary' },
{ key: 'Type' , value: 'Sword' },
],
});
Multiple filters are combined with AND logic. An NFT must match all filters to be returned.
Filter by Numeric Range
// Find items with Attack between 50 and 100
const { result } = await indexer . listNFTs ({
chainName: 'imtbl-zkevm-testnet' ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters: [
{ key: 'Attack' , value: { gte: 50 , lte: 100 } }
],
});
Operator Meaning gteGreater than or equal gtGreater than lteLess than or equal ltLess than
Combine with Owner Filter
// Find the player's Legendary items
const { result } = await indexer . listNFTsByAccountAddress ({
chainName: 'imtbl-zkevm-testnet' ,
accountAddress: playerAddress ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters: [
{ key: 'Rarity' , value: 'Legendary' }
],
});
Building Inventory Filters
Define Your Filters
interface InventoryFilters {
rarity ?: string [];
type ?: string [];
minLevel ?: number ;
maxLevel ?: number ;
}
async function getFilteredInventory (
address : string ,
filters : InventoryFilters
) {
const metadataFilters : MetadataFilter [] = [];
// String filters - user can select multiple values
if ( filters . rarity ?. length ) {
// Note: Multiple values for same trait = OR logic
// Query separately and merge results
const results = await Promise . all (
filters . rarity . map ( rarity =>
indexer . listNFTsByAccountAddress ({
chainName: 'imtbl-zkevm-testnet' ,
accountAddress: address ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters: [{ key: 'Rarity' , value: rarity }],
})
)
);
// Deduplicate by token_id
const seen = new Set ();
return results . flatMap ( r => r . result ). filter ( nft => {
if ( seen . has ( nft . token_id )) return false ;
seen . add ( nft . token_id );
return true ;
});
}
// Numeric range filter
if ( filters . minLevel !== undefined || filters . maxLevel !== undefined ) {
const range : any = {};
if ( filters . minLevel !== undefined ) range . gte = filters . minLevel ;
if ( filters . maxLevel !== undefined ) range . lte = filters . maxLevel ;
metadataFilters . push ({ key: 'Level' , value: range });
}
const { result } = await indexer . listNFTsByAccountAddress ({
chainName: 'imtbl-zkevm-testnet' ,
accountAddress: address ,
contractAddress: COLLECTION_ADDRESS ,
metadataFilters ,
});
return result ;
}
React Filter Component
function InventoryFilters ({ onFilterChange } : { onFilterChange : ( filters : InventoryFilters ) => void }) {
const [ rarity , setRarity ] = useState < string []>([]);
const [ type , setType ] = useState < string []>([]);
const [ levelRange , setLevelRange ] = useState ({ min: 0 , max: 100 });
useEffect (() => {
onFilterChange ({
rarity: rarity . length ? rarity : undefined ,
type: type . length ? type : undefined ,
minLevel: levelRange . min || undefined ,
maxLevel: levelRange . max < 100 ? levelRange . max : undefined ,
});
}, [ rarity , type , levelRange ]);
return (
< div className = "filters" >
< MultiSelect
label = "Rarity"
options = { [ 'Common' , 'Uncommon' , 'Rare' , 'Epic' , 'Legendary' ]}
value={rarity}
onChange={setRarity}
/>
<MultiSelect
label="Type"
options={[ 'Sword' , 'Shield' , 'Armor' , 'Potion' , 'Material' ]}
value={type}
onChange={setType}
/>
<RangeSlider
label="Level"
min={0}
max={100}
value={levelRange}
onChange={setLevelRange}
/>
</div>
);
}
Marketplace Search
Building a Search API
// API endpoint for marketplace search
app . get ( '/api/marketplace/search' , async ( req , res ) => {
const {
collection ,
rarity ,
type ,
minPrice ,
maxPrice ,
sortBy = 'price_asc' ,
page = 1 ,
} = req . query ;
// Build metadata filters
const metadataFilters : MetadataFilter [] = [];
if ( rarity ) metadataFilters . push ({ key: 'Rarity' , value: rarity });
if ( type ) metadataFilters . push ({ key: 'Type' , value: type });
// Get NFTs matching metadata criteria
const { result : nfts } = await indexer . listNFTs ({
chainName: 'imtbl-zkevm-mainnet' ,
contractAddress: collection ,
metadataFilters ,
pageSize: 100 ,
});
// Get active listings for these NFTs
const tokenIds = nfts . map ( n => n . token_id );
const { result : listings } = await orderbook . listListings ({
sellItemContractAddress: collection ,
sellItemTokenId: tokenIds , // Filter to specific tokens
status: 'ACTIVE' ,
});
// Match NFTs with their listings
const listingsMap = new Map ( listings . map ( l => [ l . sell [ 0 ]. tokenId , l ]));
const results = nfts
. map ( nft => ({
... nft ,
listing: listingsMap . get ( nft . token_id ),
}))
. filter ( item => item . listing ) // Only show listed items
. filter ( item => {
const price = BigInt ( item . listing . buy [ 0 ]. amount );
if ( minPrice && price < BigInt ( minPrice )) return false ;
if ( maxPrice && price > BigInt ( maxPrice )) return false ;
return true ;
});
// Sort
results . sort (( a , b ) => {
const priceA = BigInt ( a . listing . buy [ 0 ]. amount );
const priceB = BigInt ( b . listing . buy [ 0 ]. amount );
return sortBy === 'price_asc'
? Number ( priceA - priceB )
: Number ( priceB - priceA );
});
res . json ({
items: results . slice (( page - 1 ) * 20 , page * 20 ),
total: results . length ,
});
});
Getting Available Filters
Dynamically discover which attributes exist in a collection:
async function getCollectionAttributes ( contractAddress : string ) {
const { result : nfts } = await indexer . listNFTs ({
chainName: 'imtbl-zkevm-testnet' ,
contractAddress ,
pageSize: 200 ,
});
// Extract unique attribute keys and values
const attributes = new Map < string , Set < string >>();
for ( const nft of nfts ) {
for ( const attr of nft . attributes || []) {
if ( ! attributes . has ( attr . trait_type )) {
attributes . set ( attr . trait_type , new Set ());
}
attributes . get ( attr . trait_type ) ! . add ( String ( attr . value ));
}
}
// Convert to filter options
return Array . from ( attributes . entries ()). map (([ trait , values ]) => ({
trait ,
values: Array . from ( values ). sort (),
}));
}
// Result:
// [
// { trait: 'Rarity', values: ['Common', 'Epic', 'Legendary', 'Rare', 'Uncommon'] },
// { trait: 'Type', values: ['Armor', 'Material', 'Potion', 'Shield', 'Sword'] },
// { trait: 'Level', values: ['1', '10', '15', '20', ...] },
// ]
Cache collection attributes—they change rarely. Refresh when new NFTs are minted.
Consistent Trait Names
// ✅ Good - consistent naming
{ "trait_type" : "Rarity" , "value" : "Legendary" }
{ "trait_type" : "Rarity" , "value" : "Epic" }
// ❌ Bad - inconsistent
{ "trait_type" : "Rarity" , "value" : "legendary" } // lowercase
{ "trait_type" : "rarity" , "value" : "Epic" } // different case
Numeric Attributes
// ✅ Good - use display_type for numbers
{
"trait_type" : "Attack" ,
"value" : 85 ,
"display_type" : "number"
}
// ❌ Bad - number as string
{
"trait_type" : "Attack" ,
"value" : "85"
}
Searchable Categories
Design attributes with filtering in mind:
Attribute Type Example Values Rarity String (enum) Common, Uncommon, Rare, Epic, Legendary Type String (enum) Sword, Shield, Armor, Potion Level Number 1-100 Attack Number 0-999 Element String (enum) Fire, Water, Earth, Air
Next Steps
Indexer Overview Integration patterns and caching
ERC-721 Contracts Set up your collection metadata
Orderbook Add marketplace search to trading
Build a Marketplace Implement marketplace features