> ## 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.

# Metadata Search

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:

```json theme={null}
{
  "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

```typescript theme={null}
import { BlockchainData } from '@imtbl/blockchain-data';
import { Environment } from '@imtbl/config';

const indexer = new BlockchainData({
  baseConfig: {
    environment: Environment.SANDBOX,
    publishableKey: 'YOUR_PUBLISHABLE_KEY',
  },
});
```

```typescript theme={null}
// 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

```typescript theme={null}
// Find Legendary Swords
const { result } = await indexer.listNFTs({
  chainName: 'imtbl-zkevm-testnet',
  contractAddress: COLLECTION_ADDRESS,
  metadataFilters: [
    { key: 'Rarity', value: 'Legendary' },
    { key: 'Type', value: 'Sword' },
  ],
});
```

<Info>
  Multiple filters are combined with AND logic. An NFT must match all filters to be returned.
</Info>

### Filter by Numeric Range

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// 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:

```typescript theme={null}
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', ...] },
// ]
```

<Tip>
  Cache collection attributes—they change rarely. Refresh when new NFTs are minted.
</Tip>

## Metadata Best Practices

### Consistent Trait Names

```json theme={null}
// ✅ 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

```json theme={null}
// ✅ 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

<CardGroup cols={2}>
  <Card title="Indexer Overview" icon="database" href="/docs/products/indexer/overview">
    Integration patterns and caching
  </Card>

  <Card title="ERC-721 Contracts" icon="file-contract" href="/docs/products/asset-contracts/erc721">
    Set up your collection metadata
  </Card>

  <Card title="Orderbook" icon="book" href="/docs/products/orderbook/overview">
    Add marketplace search to trading
  </Card>

  <Card title="Build a Marketplace" icon="store" href="/docs/guides/build-a-marketplace">
    Implement marketplace features
  </Card>
</CardGroup>
