Build an NFT Gallery Chrome Extension with OpenSea API
The NFT ecosystem has exploded in recent years, with millions of digital collectibles traded across Ethereum, Polygon, and other blockchain networks. For crypto enthusiasts and collectors, keeping track of their NFT portfolio often means juggling multiple marketplaces, wallets, and tracking tools. What if you could have your entire NFT gallery accessible directly from your browser toolbar?
In this comprehensive guide, we will build a fully functional NFT Gallery Chrome Extension using the OpenSea API. By the end of this tutorial, you will have an extension that allows users to connect their wallet, view their NFT collection, check floor prices, and explore NFT details — all without leaving the browser.
This project is perfect for developers who want to combine their Chrome extension skills with Web3 development. Whether you are building for personal use or planning to publish to the Chrome Web Store, this guide covers everything from API setup to advanced features.
Why Build an NFT Gallery Extension?
Before diving into code, let us explore why NFT gallery extensions are valuable and what makes them different from traditional web applications.
The Current NFT Landscape
NFTs (Non-Fungible Tokens) have evolved beyond simple JPEG images. Today, they represent digital ownership of art, music, game items, domain names, and even real-world assets. Major marketplaces like OpenSea, Blur, and LooksRare process billions of dollars in trading volume monthly.
For NFT collectors, this means managing assets across multiple collections and blockchains. A dedicated Chrome extension can simplify this by providing instant access to portfolio data without navigating through multiple websites.
Advantages of a Browser Extension
Browser extensions offer unique advantages for NFT portfolio management:
- Instant Access: Users can check their collection with a single click from any webpage
- Background Updates: The extension can periodically refresh data without user intervention
- Persistent Presence: Unlike web apps that require logging in each time, extensions maintain state
- Cross-Site Functionality: Extensions can interact with NFT data regardless of which marketplace the user is viewing
Use Cases for Your Extension
Your NFT Gallery extension can serve multiple purposes:
- Portfolio Viewer: Display all NFTs owned by a wallet address
- Price Tracker: Show floor prices and recent sales for collections
- Watchlist: Monitor specific collections without owning them
- Quick Viewer: Preview NFT details when browsing marketplace pages
For this tutorial, we will focus on building a comprehensive portfolio viewer that combines all these features.
Understanding the OpenSea API
The OpenSea API is the most comprehensive NFT data API available, providing access to asset information, collection data, ownership details, and trading history. Understanding how to use this API effectively is crucial for building your extension.
API Overview
OpenSea offers several API endpoints that are essential for our extension:
- Assets API: Retrieve NFT details including image, name, description, and attributes
- Collections API: Get collection statistics, floor prices, and metadata
- Owners API: Find out which addresses own specific NFTs
- Events API: Track transfers, sales, and other on-chain events
Authentication Requirements
The OpenSea API requires an API key for most endpoints. Here is how to obtain one:
- Visit the OpenSea API page
- Sign in to your OpenSea account
- Navigate to the API Keys section
- Create a new API key with appropriate permissions
- Copy and securely store your key
For development, the API key is free. However, be aware of rate limits — the API allows a certain number of requests per second depending on your plan.
API Response Format
OpenSea returns data in a standardized JSON format. Here is an example response for a single NFT asset:
{
"id": 12345678,
"name": "Bored Ape #8429",
"description": "The Bored Ape Yacht Club is a collection of 10,000 unique Bored Apes.",
"image_url": "https://ipfs.io/ipfs/QmX.../image.png",
"external_link": "https://opensea.io/assets/eth/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/8429",
"collection": {
"name": "Bored Ape Yacht Club",
"floor_price": 15.5
},
"owner": {
"address": "0x..."
},
"traits": [
{
"trait_type": "Background",
"value": "Blue"
}
]
}
Understanding this structure helps us design our extension’s data handling layer effectively.
Project Setup and Structure
Now let us set up our Chrome extension project with the proper structure for an NFT gallery application.
Directory Structure
Create the following project structure:
nft-gallery-extension/
├── manifest.json
├── background.js
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── content/
│ └── content.js
├── utils/
│ ├── api.js
│ └── storage.js
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── _locales/
└── en/
└── messages.json
This structure follows Chrome extension best practices, separating concerns between different components.
Manifest Configuration
The manifest.json file defines our extension capabilities:
{
"manifest_version": 3,
"name": "NFT Gallery",
"version": "1.0.0",
"description": "View and track your NFT portfolio directly from your browser",
"permissions": [
"storage",
"activeTab"
],
"host_permissions": [
"https://api.opensea.io/*"
],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Key points to notice:
- We request
https://api.opensea.io/*in host_permissions to allow API calls - The popup directory contains our main UI
- Storage permission lets us cache wallet addresses and preferences
Building the API Layer
The API layer handles all communication with OpenSea. Creating a robust, error-resistant API client is essential for a good user experience.
API Utility Module
Create utils/api.js:
// utils/api.js
const OPENSEA_API_BASE = 'https://api.opensea.io/api/v2';
const API_KEY = 'YOUR_OPENSEA_API_KEY'; // Replace with your key
class OpenSeaAPI {
constructor(apiKey) {
this.apiKey = apiKey;
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
async request(endpoint, options = {}) {
const url = `${OPENSEA_API_BASE}${endpoint}`;
const headers = {
'Accept': 'application/json',
'X-API-KEY': this.apiKey,
...options.headers
};
try {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('OpenSea API request failed:', error);
throw error;
}
}
async getAssets(ownerAddress, options = {}) {
const { limit = 50, cursor = null } = options;
const params = new URLSearchParams({
owner: ownerAddress,
limit: limit.toString()
});
if (cursor) {
params.append('cursor', cursor);
}
const cacheKey = `assets_${ownerAddress}_${cursor || 'first'}`;
const cached = this.getCached(cacheKey);
if (cached) return cached;
const data = await this.request(`/assets?${params.toString()}`);
this.setCache(cacheKey, data);
return data;
}
async getCollection(collectionSlug) {
const cacheKey = `collection_${collectionSlug}`;
const cached = this.getCached(cacheKey);
if (cached) return cached;
const data = await this.request(`/collections/${collectionSlug}`);
this.setCache(cacheKey, data);
return data;
}
async getAccountBalance(address) {
return this.getAssets(address, { limit: 100 });
}
getCached(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
return null;
}
setCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
// Export singleton instance
const openSeaAPI = new OpenSeaAPI(API_KEY);
export default openSeaAPI;
This API layer includes several important features:
- Caching: Reduces API calls and improves performance
- Error Handling: Graceful error reporting for failed requests
- Pagination Support: Handles cursor-based pagination for large collections
- Configurable Limits: Users can fetch different numbers of assets
Storage Management
Persistent storage is crucial for remembering user preferences and wallet addresses.
Storage Utility Module
Create utils/storage.js:
// utils/storage.js
const STORAGE_KEYS = {
WALLET_ADDRESS: 'wallet_address',
WATCHED_COLLECTIONS: 'watched_collections',
THEME: 'theme',
REFRESH_INTERVAL: 'refresh_interval',
LAST_SYNC: 'last_sync'
};
class StorageManager {
constructor() {
this.storage = chrome.storage.local;
}
async setWalletAddress(address) {
await this.storage.set({ [STORAGE_KEYS.WALLET_ADDRESS]: address });
}
async getWalletAddress() {
const result = await this.storage.get(STORAGE_KEYS.WALLET_ADDRESS);
return result[STORAGE_KEYS.WALLET_ADDRESS] || null;
}
async setWatchedCollections(collections) {
await this.storage.set({ [STORAGE_KEYS.WATCHED_COLLECTIONS]: collections });
}
async getWatchedCollections() {
const result = await this.storage.get(STORAGE_KEYS.WATCHED_COLLECTIONS);
return result[STORAGE_KEYS.WATCHED_COLLECTIONS] || [];
}
async setTheme(theme) {
await this.storage.set({ [STORAGE_KEYS.THEME]: theme });
}
async getTheme() {
const result = await this.storage.get(STORAGE_KEYS.THEME);
return result[STORAGE_KEYS.THEME] || 'dark';
}
async setLastSync(timestamp) {
await this.storage.set({ [STORAGE_KEYS.LAST_SYNC]: timestamp });
}
async getLastSync() {
const result = await this.storage.get(STORAGE_KEYS.LAST_SYNC);
return result[STORAGE_KEYS.LAST_SYNC] || null;
}
async clearAll() {
await this.storage.clear();
}
}
export default new StorageManager();
export { STORAGE_KEYS };
Building the Popup UI
The popup is the main interface users interact with. It needs to be clean, responsive, and provide quick access to portfolio information.
Popup HTML
Create popup/popup.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFT Gallery</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header class="header">
<h1>NFT Gallery</h1>
<button id="settingsBtn" class="icon-btn" aria-label="Settings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
</header>
<div id="connectView" class="view">
<div class="welcome">
<h2>Welcome to NFT Gallery</h2>
<p>Connect your wallet to view your NFT collection</p>
</div>
<form id="connectForm" class="connect-form">
<input
type="text"
id="walletAddress"
placeholder="Enter wallet address (0x...)"
class="wallet-input"
>
<button type="submit" class="btn btn-primary">Connect Wallet</button>
</form>
<p class="help-text">
Don't have a wallet? <a href="https://metamask.io/" target="_blank">Get MetaMask</a>
</p>
</div>
<div id="portfolioView" class="view hidden">
<div class="portfolio-summary">
<div class="stat-card">
<span class="stat-label">Total NFTs</span>
<span id="totalCount" class="stat-value">0</span>
</div>
<div class="stat-card">
<span class="stat-label">Collections</span>
<span id="collectionCount" class="stat-value">0</span>
</div>
<div class="stat-card">
<span class="stat-label">Est. Floor Value</span>
<span id="floorValue" class="stat-value">0 ETH</span>
</div>
</div>
<div class="refresh-bar">
<span id="lastSync" class="last-sync">Last updated: Never</span>
<button id="refreshBtn" class="btn btn-small">Refresh</button>
</div>
<div id="nftGrid" class="nft-grid">
<!-- NFT items will be inserted here -->
</div>
<button id="disconnectBtn" class="btn btn-secondary btn-full">
Disconnect Wallet
</button>
</div>
<div id="settingsView" class="view hidden">
<h2>Settings</h2>
<div class="setting-item">
<label for="refreshInterval">Auto-refresh interval</label>
<select id="refreshInterval" class="select-input">
<option value="5">5 minutes</option>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
</select>
</div>
<div class="setting-item">
<label for="themeSelect">Theme</label>
<select id="themeSelect" class="select-input">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<button id="saveSettings" class="btn btn-primary btn-full">Save Settings</button>
<button id="backBtn" class="btn btn-secondary btn-full">Back</button>
</div>
</div>
<div id="loadingOverlay" class="loading-overlay hidden">
<div class="spinner"></div>
</div>
<script type="module" src="popup.js"></script>
</body>
</html>
Popup CSS
Create popup/popup.css:
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-card: #0f3460;
--text-primary: #eaeaea;
--text-secondary: #a0a0a0;
--accent: #e94560;
--accent-hover: #ff6b6b;
--border: #2a2a4a;
--success: #4ade80;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 380px;
min-height: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
}
.container {
padding: 16px;
}
.hidden {
display: none !important;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.header h1 {
font-size: 18px;
font-weight: 600;
background: linear-gradient(135deg, var(--accent), #ff9a8b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.icon-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.icon-btn:hover {
background: var(--bg-card);
color: var(--text-primary);
}
/* Views */
.view {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Welcome & Connect */
.welcome {
text-align: center;
margin-bottom: 24px;
}
.welcome h2 {
font-size: 20px;
margin-bottom: 8px;
}
.welcome p {
color: var(--text-secondary);
font-size: 14px;
}
.connect-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.wallet-input {
width: 100%;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.wallet-input:focus {
border-color: var(--accent);
}
.wallet-input::placeholder {
color: var(--text-secondary);
}
/* Buttons */
.btn {
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--bg-secondary);
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.btn-full {
width: 100%;
}
.help-text {
text-align: center;
margin-top: 16px;
font-size: 12px;
color: var(--text-secondary);
}
.help-text a {
color: var(--accent);
text-decoration: none;
}
/* Portfolio Summary */
.portfolio-summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.stat-card {
background: var(--bg-card);
padding: 12px 8px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 10px;
color: var(--text-secondary);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
/* Refresh Bar */
.refresh-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 8px 12px;
background: var(--bg-secondary);
border-radius: 8px;
}
.last-sync {
font-size: 11px;
color: var(--text-secondary);
}
/* NFT Grid */
.nft-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
max-height: 320px;
overflow-y: auto;
margin-bottom: 16px;
padding-right: 4px;
}
.nft-grid::-webkit-scrollbar {
width: 4px;
}
.nft-grid::-webkit-scrollbar-thumb {
background: var(--bg-card);
border-radius: 2px;
}
.nft-item {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
position: relative;
background: var(--bg-card);
cursor: pointer;
}
.nft-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s;
}
.nft-item:hover img {
transform: scale(1.05);
}
.nft-item-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
font-size: 10px;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Settings */
.setting-item {
margin-bottom: 16px;
}
.setting-item label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.select-input {
width: 100%;
padding: 10px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
}
.select-input:focus {
outline: none;
border-color: var(--accent);
}
/* Loading */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 26, 46, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--bg-card);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Popup JavaScript
Create popup/popup.js:
import openSeaAPI from '../utils/api.js';
import storageManager, { STORAGE_KEYS } from '../utils/storage.js';
class NFTGallery {
constructor() {
this.currentView = 'connect';
this.nfts = [];
this.collections = new Map();
this.init();
}
async init() {
this.bindEvents();
await this.checkExistingWallet();
}
bindEvents() {
// Connect form
document.getElementById('connectForm').addEventListener('submit', (e) => {
e.preventDefault();
this.connectWallet();
});
// Refresh button
document.getElementById('refreshBtn').addEventListener('click', () => {
this.refreshPortfolio();
});
// Disconnect button
document.getElementById('disconnectBtn').addEventListener('click', () => {
this.disconnectWallet();
});
// Settings
document.getElementById('settingsBtn').addEventListener('click', () => {
this.showView('settings');
});
document.getElementById('backBtn').addEventListener('click', () => {
this.showView('portfolio');
});
document.getElementById('saveSettings').addEventListener('click', () => {
this.saveSettings();
});
}
showView(viewName) {
const views = ['connectView', 'portfolioView', 'settingsView'];
views.forEach(view => {
document.getElementById(view).classList.add('hidden');
});
document.getElementById(`${viewName}View`).classList.remove('hidden');
this.currentView = viewName;
}
async checkExistingWallet() {
const walletAddress = await storageManager.getWalletAddress();
if (walletAddress) {
this.showView('portfolio');
await this.loadPortfolio(walletAddress);
} else {
this.showView('connect');
}
}
async connectWallet() {
const addressInput = document.getElementById('walletAddress');
const address = addressInput.value.trim();
if (!this.isValidAddress(address)) {
alert('Please enter a valid Ethereum wallet address');
return;
}
this.showLoading(true);
try {
await storageManager.setWalletAddress(address);
await this.loadPortfolio(address);
this.showView('portfolio');
} catch (error) {
alert('Failed to load portfolio. Please check the wallet address.');
console.error(error);
} finally {
this.showLoading(false);
}
}
async loadPortfolio(address) {
try {
const response = await openSeaAPI.getAssets(address, { limit: 50 });
this.nfts = response.assets || [];
this.processCollections();
this.renderNFTs();
this.updateStats();
await storageManager.setLastSync(Date.now());
this.updateLastSyncTime();
} catch (error) {
console.error('Failed to load portfolio:', error);
throw error;
}
}
processCollections() {
this.collections.clear();
this.nfts.forEach(nft => {
const collectionName = nft.collection?.name || 'Unknown';
if (!this.collections.has(collectionName)) {
this.collections.set(collectionName, {
name: collectionName,
slug: nft.collection?.slug,
floorPrice: nft.collection?.floor_price || 0,
count: 0
});
}
const collection = this.collections.get(collectionName);
collection.count++;
});
}
renderNFTs() {
const grid = document.getElementById('nftGrid');
grid.innerHTML = '';
this.nfts.forEach(nft => {
const item = document.createElement('div');
item.className = 'nft-item';
item.innerHTML = `
<img src="${nft.image_url || nft.image_preview_url || ''}" alt="${nft.name || 'NFT'}" loading="lazy">
<div class="nft-item-overlay">${nft.name || 'Unnamed NFT'}</div>
`;
item.addEventListener('click', () => {
if (nft.external_link) {
chrome.tabs.create({ url: nft.external_link });
}
});
grid.appendChild(item);
});
}
updateStats() {
document.getElementById('totalCount').textContent = this.nfts.length;
document.getElementById('collectionCount').textContent = this.collections.size;
let totalFloor = 0;
this.collections.forEach(collection => {
totalFloor += collection.floorPrice || 0;
});
document.getElementById('floorValue').textContent = `${totalFloor.toFixed(2)} ETH`;
}
async refreshPortfolio() {
const walletAddress = await storageManager.getWalletAddress();
if (walletAddress) {
await this.loadPortfolio(walletAddress);
}
}
async disconnectWallet() {
await storageManager.clearAll();
this.nfts = [];
this.collections.clear();
document.getElementById('walletAddress').value = '';
this.showView('connect');
}
updateLastSyncTime() {
const lastSync = storageManager.getLastSync();
const lastSyncEl = document.getElementById('lastSync');
if (lastSync) {
const date = new Date(lastSync);
lastSyncEl.textContent = `Last updated: ${date.toLocaleTimeString()}`;
}
}
async saveSettings() {
const refreshInterval = document.getElementById('refreshInterval').value;
const theme = document.getElementById('themeSelect').value;
await storageManager.storage.set({
[STORAGE_KEYS.REFRESH_INTERVAL]: refreshInterval,
[STORAGE_KEYS.THEME]: theme
});
// Apply theme
document.documentElement.setAttribute('data-theme', theme);
this.showView('portfolio');
}
isValidAddress(address) {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
showLoading(show) {
document.getElementById('loadingOverlay').classList.toggle('hidden', !show);
}
}
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
new NFTGallery();
});
Background Service Worker
The background service worker handles periodic tasks and manages extension lifecycle events.
Create background.js:
// background.js
chrome.runtime.onInstalled.addListener((details) => {
console.log('NFT Gallery extension installed:', details.reason);
if (details.reason === 'install') {
// Set default settings
chrome.storage.local.set({
refreshInterval: 15,
theme: 'dark'
});
}
});
// Handle messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'refreshPortfolio') {
// Trigger a background refresh
console.log('Background refresh requested');
sendResponse({ status: 'success' });
}
return true;
});
// Schedule periodic refreshes (if user has set up auto-refresh)
chrome.alarms.create('portfolioRefresh', { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'portfolioRefresh') {
console.log('Portfolio auto-refresh triggered');
// In a production app, you might update cached data here
}
});
Advanced Features
Now that we have a working NFT gallery extension, let us explore several advanced features that can make your extension truly stand out.
Adding Wallet Connection with MetaMask
For a more seamless experience, you can integrate MetaMask wallet connection:
// Add to popup.js
async function connectMetaMask() {
if (typeof window.ethereum !== 'undefined') {
try {
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
if (accounts.length > 0) {
return accounts[0];
}
} catch (error) {
console.error('MetaMask connection failed:', error);
}
} else {
alert('MetaMask is not installed. Please install MetaMask to use this feature.');
}
return null;
}
// Update connect handler
document.getElementById('connectMetaMask').addEventListener('click', async () => {
const address = await connectMetaMask();
if (address) {
document.getElementById('walletAddress').value = address;
await connectWallet();
}
});
Collection Watchlist
Add the ability to watch specific collections:
// Add to storage.js and popup.js
async function addToWatchlist(collectionSlug) {
const watched = await storageManager.getWatchedCollections();
if (!watched.includes(collectionSlug)) {
watched.push(collectionSlug);
await storageManager.setWatchedCollections(watched);
}
}
async function removeFromWatchlist(collectionSlug) {
const watched = await storageManager.getWatchedCollections();
const filtered = watched.filter(s => s !== collectionSlug);
await storageManager.setWatchedCollections(filtered);
}
Floor Price Alerts
Implement price tracking with notifications:
// background.js - Add price monitoring
async function checkFloorPrices() {
const watchedCollections = await storageManager.getWatchedCollections();
const prices = await chrome.storage.local.get('collectionPrices') || {};
for (const slug of watchedCollections) {
try {
const data = await openSeaAPI.getCollection(slug);
const newFloor = data.collection?.floor_price;
const oldFloor = prices[slug];
if (oldFloor && newFloor) {
const change = ((newFloor - oldFloor) / oldFloor) * 100;
// Notify if price changed significantly
if (Math.abs(change) > 10) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'NFT Price Alert',
message: `${data.collection.name} floor price changed by ${change.toFixed(1)}%`
});
}
}
prices[slug] = newFloor;
} catch (error) {
console.error(`Failed to check collection: ${slug}`);
}
}
await chrome.storage.local.set({ collectionPrices: prices });
}
Testing Your Extension
Testing is crucial for a production-ready extension. Here is how to test your NFT Gallery:
Manual Testing
- Open Chrome and navigate to
chrome://extensions/ - Enable Developer mode
- Click “Load unpacked” and select your extension directory
- Click the extension icon to open the popup
- Enter a known wallet address (you can find sample addresses on Etherscan)
- Verify that NFTs load correctly
Test Wallet Addresses
Here are some popular wallet addresses you can use for testing:
- Bored Ape Yacht Club Creator:
0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D - CryptoPunks Creator:
0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB - Pudgy Penguins:
0xBd3531dA5CF5857e7CfAA92426880bBd73534B98
Debugging Tips
- Use
chrome://extensions/and click “service worker” to see console logs - Inspect popup with right-click → Inspect popup
- Check Network tab in DevTools for API calls
Publishing to the Chrome Web Store
Once your extension is tested and polished, follow these steps to publish:
Prepare for Submission
- Create icons: Generate 16x16, 48x48, and 128x128 PNG icons
- Write description: Clearly explain features and permissions
- Create screenshots: Capture 1280x800 screenshots of your extension
- Privacy policy: Required since the extension makes network requests
Submission Process
- Go to the Chrome Web Store Developer Dashboard
- Create a new item and upload your extension as a ZIP file
- Fill in the store listing details
- Submit for review
Store Listing Best Practices
- Use a clear, descriptive title
- Highlight key features in bullet points
- Include high-quality screenshots showing the popup UI
- Set appropriate categories and tags
Conclusion
Congratulations! You have built a fully functional NFT Gallery Chrome Extension using the OpenSea API. This extension demonstrates several key concepts:
- API Integration: Connecting to external APIs (OpenSea) from a Chrome extension
- Storage Management: Persisting user preferences and wallet addresses
- Popup UI: Creating a responsive, attractive user interface
- Background Processing: Handling periodic updates and notifications
- Web3 Concepts: Working with blockchain addresses and NFT data
Future Enhancements
To take this extension further, consider adding:
- Multi-chain support: Add support for Polygon, Solana, and other chains
- Portfolio analytics: Charts showing portfolio value over time
- Trait filtering: Filter NFTs by attributes and rarity
- Collection comparison: Compare multiple collections side by side
- Sell/Transfer features: Integrate with marketplace listing APIs
Continue Learning
For more Chrome extension development resources, explore our guides on:
The Web3 ecosystem is evolving rapidly, and Chrome extensions provide a unique way to bring blockchain functionality directly to users’ browsers. Start building today and help shape the future of Web3 UX!
This guide is part of the Chrome Extension Guide by theluckystrike — your comprehensive resource for Chrome extension development.