Documentation Index Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
Use this file to discover all available pages before exploring further.
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