The user's full NFT collection, with artwork and details, displayed in the 'Collectibles' tab.

Your wallet now displays token balances, calculates total portfolio value, and tracks detailed account activity. To give users a holistic view of their onchain assets, the final piece is to showcase their NFT collections. In this third and final guide of our wallet series, we will focus on implementing the Collectibles tab.

This guide assumes you have completed the previous guides:

  1. Build a Realtime Wallet
  2. Add Account Activity

Explore the NFT Collection

See the collectibles feature in action with the live demo below. Click on the “Collectibles” tab to browse the sample wallet’s NFT collection:

Fetch NFT Collectibles

Let’s add a new asynchronous getWalletCollectibles function to server.js to fetch a user’s NFT collection using the Collectibles API.

server.js (getWalletCollectibles - Sim API portion)
async function getWalletCollectibles(walletAddress, limit = 50) {
    if (!walletAddress) return [];

    const url = `https://api.sim.dune.com/v1/evm/collectibles/${walletAddress}?limit=${limit}`;

    try {
        const response = await fetch(url, {
            headers: {
                'X-Sim-Api-Key': SIM_API_KEY,
                'Content-Type': 'application/json'
            }
        });

        if (!response.ok) {
            const errorBody = await response.text();
            console.error(`Collectibles API request failed with status ${response.status}: ${response.statusText}`, errorBody);
            throw new Error(`Collectibles API request failed: ${response.statusText}`);
        }

        const data = await response.json();
        const collectibles = data.entries || [];
        
        // ... (OpenSea enrichment will be added in the next section)
        
    } catch (error) {
        console.error("Error fetching wallet collectibles:", error.message);
        return [];
    }
}

The NFT data is extracted from the entries array within this response, providing information like contract addresses, token IDs, and chain data.

The Collectibles API supports pagination using limit and offset query parameters. For wallets with many NFTs, you can implement logic to fetch subsequent pages using the next_offset value returned by the API to provide a complete view of the user’s collection.

Fetch NFT Images

Sim APIs provide comprehensive blockchain metadata for NFTs, but we images to create a rich visual experience. We’ll integrate with OpenSea’s API to enrich our NFT data with image URLs.

NFT image data and enhanced metadata might be coming soon to the Sim APIs, but for now you can use OpenSea APIs to grab image URLs and provide a visual NFT display for users.

Get an OpenSea API Key

Before we can fetch NFT images from OpenSea, you’ll need to obtain an OpenSea API key. Once you receive your API key, add it to your .env file:

.env (Add OpenSea API Key)
SIM_API_KEY=your_sim_api_key_here
OPENSEA_API_KEY=your_opensea_api_key_here

Update getWalletCollectibles

Let’s complete the getWalletCollectibles function by adding OpenSea API integration to fetch images:

server.js (getWalletCollectibles - Complete function)
async function getWalletCollectibles(walletAddress, limit = 50) {
    if (!walletAddress) return [];

    const url = `https://api.sim.dune.com/v1/evm/collectibles/${walletAddress}?limit=${limit}`;

    try {
        const response = await fetch(url, {
            headers: {
                'X-Sim-Api-Key': SIM_API_KEY,
                'Content-Type': 'application/json'
            }
        });

        if (!response.ok) {
            const errorBody = await response.text();
            console.error(`Collectibles API request failed with status ${response.status}: ${response.statusText}`, errorBody);
            throw new Error(`Collectibles API request failed: ${response.statusText}`);
        }

        const data = await response.json();
        const collectibles = data.entries || [];

        // Enrich collectibles with OpenSea image data
        const enrichedCollectibles = await Promise.all(
            collectibles.map(async (collectible) => {
                try {
                    // Use the chain value directly from Sim APIs
                    if (collectible.chain) {
                        const openSeaUrl = `https://api.opensea.io/api/v2/chain/${collectible.chain}/contract/${collectible.contract_address}/nfts/${collectible.token_id}`;
                        
                        const openSeaResponse = await fetch(openSeaUrl, {
                            headers: {
                                'Accept': 'application/json',
                                'x-api-key': process.env.OPENSEA_API_KEY
                            }
                        });

                        if (openSeaResponse.ok) {
                            const openSeaData = await openSeaResponse.json();
                            return {
                                ...collectible,
                                image_url: openSeaData.nft?.image_url || null,
                                opensea_url: openSeaData.nft?.opensea_url || null,
                                description: openSeaData.nft?.description || null,
                                collection_name: openSeaData.nft?.collection || collectible.name
                            };
                        }
                    }
                    
                    // Return original collectible if OpenSea fetch fails or no chain info
                    return {
                        ...collectible,
                        image_url: null,
                        opensea_url: null,
                        description: null,
                        collection_name: collectible.name
                    };
                } catch (error) {
                    console.error(`Error fetching OpenSea data for ${collectible.chain}:${collectible.contract_address}:${collectible.token_id}:`, error.message);
                    return {
                        ...collectible,
                        image_url: null,
                        opensea_url: null,
                        description: null,
                        collection_name: collectible.name
                    };
                }
            })
        );

        // Filter out collectibles without images
        return enrichedCollectibles.filter(collectible => collectible.image_url !== null);

    } catch (error) {
        console.error("Error fetching wallet collectibles:", error.message);
        return [];
    }
}

This enhanced function combines blockchain data from Sim APIs with rich metadata from OpenSea. For each NFT, we make an additional API call to OpenSea using the chain and contract information provided by Sim APIs. The function enriches each collectible with image_url, opensea_url, description, and collection_name fields, then filters to only return NFTs that have available images for display.

Add Collectibles into the Server Route

Next, we update our main app.get('/') route handler in server.js to call this new function:

server.js (app.get('/') updated for collectibles)
app.get('/', async (req, res) => {
    const { 
        walletAddress = '',
        tab = 'tokens'
    } = req.query;

    let tokens = [];
    let activities = [];
    let collectibles = []; // Initialize collectibles array
    let totalWalletUSDValue = 0;
    let errorMessage = null;

    if (walletAddress) {
        try {
            // Fetch balances, activities, and collectibles concurrently for better performance
            [tokens, activities, collectibles] = await Promise.all([
                getWalletBalances(walletAddress),
                getWalletActivity(walletAddress, 25), // Fetching 25 recent activities
                getWalletCollectibles(walletAddress, 50) // Fetching up to 50 collectibles
            ]);

            // Calculate total portfolio value from token balances (Guide 1)
            if (tokens && tokens.length > 0) {
                totalWalletUSDValue = tokens.reduce((sum, token) => {
                    const value = parseFloat(token.value_usd);
                    return sum + (isNaN(value) ? 0 : value);
                }, 0);
            }
        } catch (error) {
            console.error("Error in route handler fetching all data:", error);
            errorMessage = "Failed to fetch wallet data. Please try again.";
        }
    }
    
    res.render('wallet', {
        walletAddress: walletAddress,
        currentTab: tab,
        totalWalletUSDValue: `$${totalWalletUSDValue.toFixed(2)}`,
        tokens: tokens,
        activities: activities,
        collectibles: collectibles, // Pass collectibles to the template
        errorMessage: errorMessage
    });
});

The route handler now fetches balances, activities, and the enriched NFT collectibles data concurrently for optimal performance. The collectibles array, now containing both blockchain data and image URLs, is passed to the wallet.ejs template.

Display Collectibles in the Frontend

The final step is to modify views/wallet.ejs to render the fetched collectibles within the “Collectibles” tab. We will use a grid layout to display NFT images with their collection names and token IDs.

In views/wallet.ejs, find the section for the “Collectibles” tab (you can search for id="collectibles"). It currently contains a placeholder paragraph. Replace that entire div with the following EJS:

views/wallet.ejs (Collectibles tab content)
<!-- Collectibles Tab Pane -->
<div id="collectibles" class="tab-pane <%= currentTab === 'collectibles' ? 'active' : '' %>">
    <% if (collectibles && collectibles.length > 0) { %>
        <div class="collectibles-grid">
            <% collectibles.forEach(collectible => { %>
                <% if (collectible.opensea_url) { %>
                    <a href="<%= collectible.opensea_url %>" target="_blank" class="collectible-item-link">
                <% } else { %>
                    <div class="collectible-item-link">
                <% } %>
                    <div class="collectible-item">
                        <div class="collectible-image-container">
                            <% if (collectible.image_url) { %>
                                <img src="<%= collectible.image_url %>" alt="<%= collectible.collection_name || collectible.name || 'NFT' %>" class="collectible-image">
                            <% } else { %>
                                <div class="collectible-image-placeholder">
                                    NFT
                                </div>
                            <% } %>
                        </div>
                        <div class="collectible-info-static">
                            <div class="collectible-name">
                                <%= collectible.collection_name || collectible.name || `Token #${collectible.token_id}` %>
                            </div>
                            <div class="collectible-collection">
                                #<%= collectible.token_id.length > 10 ? collectible.token_id.substring(0, 8) + '...' : collectible.token_id %>
                            </div>
                        </div>
                    </div>
                <% if (collectible.opensea_url) { %>
                    </a>
                <% } else { %>
                    </div>
                <% } %>
            <% }); %>
        </div>
    <% } else if (walletAddress) { %>
        <p style="text-align: center; padding-top: 30px; color: var(--color-text-muted);">No collectibles found for this wallet.</p>
    <% } else { %>
        <p style="text-align: center; padding-top: 30px; color: var(--color-text-muted);">Enter a wallet address to see collectibles.</p>
    <% } %>
</div>

The EJS template iterates through the collectibles array and displays each NFT with its enriched metadata. Each collectible shows the image_url from OpenSea, the collection_name or fallback name, and a truncated token_id for identification. If an opensea_url is available, the entire NFT card becomes a clickable link that opens the NFT’s OpenSea page in a new tab.


Restart your server using node server.js and navigate to your wallet app in the browser. When you click on the “Collectibles” tab, and if the wallet has NFTs, you should see the NFT collection displayed with rich visual metadata.

Conclusion

That concludes this three-part series! With just three API requests - Balances, Activity, and Collectibles - enhanced with OpenSea metadata, you’ve built a fully functional, multichain wallet that displays token balances, calculates portfolio value, tracks detailed transaction activity, and showcases NFT collections with rich visual displays.

This project serves as a solid foundation for a wallet. You can now expand upon it by exploring other Sim API features. Whether you want to add more sophisticated analytics, deeper NFT insights, or advanced transaction tracking, Sim APIs provides the blockchain data you need to build the next generation of onchain apps.