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 },
});
// 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 |
|---|
gte | Greater than or equal |
gt | Greater than |
lte | Less than or equal |
lt | Less 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