Skip to main content

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

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,
    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 } }
  ],
});
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

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