# How to Implement Token Metadata Fetching and Caching in Nautilus Wallet: A Complete Guide

> Learn how Nautilus Wallet implements token metadata fetching and caching. Discover its reactive in-memory Map, IndexedDB persistence, and GraphQL integration for fast UI and efficient data.

- Repository: [Nautilus Team/nautilus-wallet](https://github.com/nautls/nautilus-wallet)
- Tags: how-to-guide
- Published: 2026-03-07

---

**Nautilus Wallet implements token metadata fetching and caching using a layered architecture that combines a reactive in-memory Map with IndexedDB persistence and GraphQL network requests, ensuring fast UI rendering while minimizing redundant blockchain queries.**

Nautilus Wallet, an open-source Ergo blockchain wallet, handles token metadata (names, decimals, artwork) through a sophisticated caching system. This article explains how the wallet implements token metadata fetching and caching to balance performance, offline support, and network efficiency.

## Architecture Overview

The token metadata system in Nautilus Wallet follows a three-tier caching strategy:

1. **Reactive In-Memory Map** – Provides instant access to metadata for UI rendering
2. **IndexedDB Persistence** – Survives page reloads and enables offline functionality
3. **GraphQL Network Layer** – Fetches unknown tokens from the Ergo explorer in chunked batches

This design ensures that wallet components in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts) and [`src/stores/poolStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/poolStore.ts) can synchronously access metadata while the system asynchronously hydrates the cache from disk and network.

## The Reactive In-Memory Cache

At the core of the system is a reactive Map stored in [`src/stores/assetsStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/assetsStore.ts):

```typescript
// src/stores/assetsStore.ts – line 62
const metadata = shallowReactive(new Map([[ERG_TOKEN_ID, ERG_METADATA]]));

```

The `metadata` object is a **reactive `Map<TokenId, BasicAssetMetadata>`** exported from the store and consumed throughout the UI. Using `shallowReactive` ensures that modifications to the Map itself trigger re-renders, while nested object updates remain efficient.

## Loading Metadata Workflow

The primary entry point for token metadata fetching and caching is the `loadMetadata` function:

```typescript
// src/stores/assetsStore.ts – lines 32-55
async function loadMetadata(tokenIds: string[], options?: Partial<LoadMetadataOptions>) {
  const { fetchInBackground, persist } = ensureDefaults(options, {
    fetchInBackground: false,
    persist: true
  });

  const unloaded = uniq(tokenIds).filter((x) => !metadata.has(x));
  if (unloaded.length === 0) return;

```

This function implements **deduplication** using `uniq` and performs an **early exit** if all requested tokens already exist in the in-memory cache. The `fetchInBackground` option allows non-blocking network requests, while `persist` controls whether new data should be written to IndexedDB.

## IndexedDB Persistence Layer

When metadata must survive browser refreshes, the system utilizes `assetInfoDbService` in [`src/database/assetInfoDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/assetInfoDbService.ts):

```typescript
// src/database/assetInfoDbService.ts – lines 24-27
public async getAnyOf(ids: string[]): Promise<IAssetInfo[]> {
  if (isEmpty(ids)) return [];
  return await dbContext.assetInfo.where("id").anyOf(ids).toArray();
}

```

This service wraps a **Dexie-based IndexedDB** table (`dbContext.assetInfo`). During the loading workflow, the system calls `assetInfoDbService.getAnyOf(unloaded)` to retrieve locally persisted entries, then patches the in-memory cache via `patchMetadata(dbMeta)`.

## Network Fetching with GraphQL

For tokens not found in memory or IndexedDB, Nautilus Wallet queries the Ergo explorer using `graphQLService` in [`src/chains/ergo/services/graphQlService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/services/graphQlService.ts):

```typescript
// src/chains/ergo/services/graphQlService.ts – lines 54-73
async getAssetsMetadata(tokenIds: string[]): Promise<IAssetInfo[] | undefined> {
  const metadataChunks: Token[][] = [];
  const chunks = chunk(tokenIds, MAX_PARAMS_PER_REQUEST);

  try {
    for (const tokenIds of chunks) {
      const chunkMetadata = await this.#getTokenMetadata({ tokenIds });
      if (isEmpty(chunkMetadata.data.tokens)) return;

      metadataChunks.push(chunkMetadata.data.tokens);
    }
  } catch (e) {
    log.error("Failed to fetch metadata", tokenIds, e);
    return;
  }

  return metadataChunks
    .flat()
    .map(parseEIP4Asset)
    .filter((assetInfo) => assetInfo) as IAssetInfo[];
}

```

This method implements **request chunking** to respect the node's `MAX_PARAMS_PER_REQUEST` limit. The raw GraphQL responses are normalized through `parseEIP4Asset`, which converts Ergo's EIP-4 token standard data into the internal `IAssetInfo` format.

## Updating the Cache with Remote Data

Once network data returns, `loadRemoteMetadata` handles cache updates and persistence:

```typescript
// src/stores/assetsStore.ts – lines 56-71
async function loadRemoteMetadata(missing: string[], persist: boolean) {
  const newMeta = await graphQLService.getAssetsMetadata(missing);
  if (!newMeta) return;

  if (missing.length > newMeta.length) {
    // Fill in missing metadata with unknown placeholders
    newMeta.push(...missing.filter((id) => !newMeta.some((x) => x.id === id)).map(unknownMetadata));
  }

  if (newMeta.length > 0) {
    patchMetadata(newMeta);
    if (persist) await assetInfoDbService.bulkPut(newMeta);
  }
}

```

This function handles **unknown tokens** by generating placeholder metadata with `type: AssetType.Unknown`. It then patches the reactive map and optionally persists to IndexedDB via `assetInfoDbService.bulkPut`.

## Patching the In-Memory Map

The `patchMetadata` function performs the actual cache update:

```typescript
// src/stores/assetsStore.ts – lines 73-82
function patchMetadata(patch: IAssetInfo[]) {
  if (patch.length === 0) return;
  for (const info of patch) {
    metadata.set(info.id, {
      name: info.name,
      decimals: info.decimals,
      type: info.subtype,
      artworkUrl: info.artworkCover ?? info.artworkUrl
    });
  }
}

```

Every call to `patchMetadata` instantly updates the reactive `metadata` map, triggering re-renders in UI components that depend on token data.

## Integration with Wallet Stores

The metadata system is consumed by multiple stores throughout the application:

**Wallet Store** triggers metadata loading when asset balances change:

```typescript
// src/stores/walletStore.ts – line 295
await assetsStore.loadMetadata(tokenIds, { fetchInBackground: opt.syncInBackground });

```

**Pool Store** ensures metadata is available before building transactions:

```typescript
// src/stores/poolStore.ts – line 92
assets.loadMetadata(txns.flatMap((x) => x.delta.tokens.map((y) => y.tokenId)));

```

**UI Components** read from the cache for formatting and display:

```typescript
// src/composables/useFormat.ts – lines 67-68
val.metadata?.name || val.tokenId,
maxLen: val.metadata?.name ? maxLen : Math.floor(maxLen / 2)

```

## Code Example: Manually Fetching Token Metadata

Here is a complete example of using the metadata system in a Nautilus Wallet extension or plugin:

```typescript
import { useAssetsStore } from '@/stores/assetsStore';

// Token IDs you need metadata for
const tokenIds = [
  '0000000000000000000000000000000000000000000000000000000000000000', // ERG
  'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
];

// Load metadata (foreground, persisted)
await useAssetsStore().loadMetadata(tokenIds, { fetchInBackground: false, persist: true });

// After the call finishes, the map is populated:
const metaMap = useAssetsStore().metadata;
console.log(metaMap.get(tokenIds[1])?.name); // → "SigUSD" (if known)

```

This call automatically:
- Returns instantly for IDs already cached in memory
- Reads from IndexedDB for previously persisted data
- Queries the Ergo explorer for unknown IDs, updates the map, and stores results back to IndexedDB

## Summary

Nautilus Wallet implements token metadata fetching and caching through a sophisticated multi-layer system:

- **Reactive in-memory Map** (`assetsStore.metadata`) provides instantaneous UI access to token names, decimals, and artwork
- **IndexedDB persistence** (`assetInfoDbService`) ensures metadata survives browser refreshes and supports offline usage
- **Intelligent deduplication** prevents redundant network requests by checking memory and disk before querying the blockchain
- **Chunked GraphQL requests** (`graphQLService.getAssetsMetadata`) respect node limits while fetching unknown token data from the Ergo explorer
- **Automatic fallback handling** generates placeholder metadata for unknown tokens to maintain UI consistency

## Frequently Asked Questions

### How does Nautilus Wallet handle offline token metadata access?

Nautilus Wallet persists all fetched token metadata to IndexedDB via `assetInfoDbService`. When the application loads or requests token data, it first checks the in-memory reactive Map, then falls back to IndexedDB using `assetInfoDbService.getAnyOf()`. This ensures that previously viewed tokens remain accessible even without an internet connection.

### What happens when a token ID is not found in the cache or database?

When `loadMetadata()` encounters unknown token IDs, it filters out any entries already present in the in-memory cache or IndexedDB, then passes the remaining IDs to `loadRemoteMetadata()`. This function calls `graphQLService.getAssetsMetadata()` to query the Ergo explorer. If the network request returns fewer results than requested, the system generates placeholder metadata using `unknownMetadata()` to ensure every token ID has a corresponding entry in the UI.

### How does the wallet prevent excessive network requests when loading many tokens?

The metadata system implements several optimization strategies. First, it deduplicates token IDs using `uniq()` and filters out already-cached entries before making network requests. Second, the GraphQL service chunks requests according to `MAX_PARAMS_PER_REQUEST` to respect node limits. Finally, the `fetchInBackground` option allows non-critical metadata to load asynchronously without blocking the UI, while the `persist` flag controls whether results should be written to IndexedDB.

### Which stores trigger metadata loading in the application?

Two primary stores initiate metadata fetching. The `walletStore` (in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts)) calls `assetsStore.loadMetadata()` whenever the wallet's asset list changes, passing the `syncInBackground` option to control blocking behavior. The `poolStore` (in [`src/stores/poolStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/poolStore.ts)) ensures metadata is available before building transactions by calling `loadMetadata()` with token IDs from transaction deltas. Additionally, UI components like [`useFormat.ts`](https://github.com/nautls/nautilus-wallet/blob/main/useFormat.ts) read from the metadata map for display purposes but do not trigger fetches.