Skip to main content
Filter NFTs by their attributes—essential for building inventory filters and marketplace search.

Use Cases

ScenarioQuery
Inventory filterShow only “Legendary” rarity items
Marketplace searchFind swords with attack > 50
Crafting UIDisplay items of type “Material”
LeaderboardsRank 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 } }
  ],
});
OperatorMeaning
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>
  );
}

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.

Metadata Best Practices

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:
AttributeTypeExample Values
RarityString (enum)Common, Uncommon, Rare, Epic, Legendary
TypeString (enum)Sword, Shield, Armor, Potion
LevelNumber1-100
AttackNumber0-999
ElementString (enum)Fire, Water, Earth, Air

Next Steps