BigBlocks Docs
Components/Marketplace

CreateListingButton

Create marketplace listings on the Bitcoin blockchain with customizable forms, image uploads, and rich metadata

A comprehensive component for creating marketplace listings on the Bitcoin blockchain, featuring customizable forms, multi-image uploads, category selection, and on-chain metadata storage for permanent, decentralized marketplace functionality.

View Component Preview →

Installation

npx bigblocks add create-listing-button

Import

import { CreateListingButton } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
onCreateListing(listing: Listing) => voidNo-Callback when listing is created successfully
onError(error: MarketError) => voidNo-Error callback
categoriesstring[]NoDefault categoriesAvailable categories for selection
maxImagesnumberNo5Maximum number of images allowed
requiresApprovalbooleanNofalseWhether listings require admin approval
feeAmountnumberNo0.001Listing fee in BSV
variant'button' | 'card' | 'inline'No'button'Display variant
size'sm' | 'md' | 'lg'No'md'Component size
classNamestringNo-Additional CSS classes

Listing Interface

interface Listing {
  id: string;
  txid: string;                    // Blockchain transaction ID
  title: string;
  description: string;
  price: number;                   // Price in satoshis
  currency: 'BSV' | 'USD';
  category: string;
  images: string[];                // Array of image URLs
  seller: {
    address: string;
    name?: string;
    idKey: string;               // BAP identity key
  };
  assetType: 'BSV20' | 'BSV21' | 'BSV721' | 'DIGITAL' | 'PHYSICAL';
  metadata?: {
    condition?: 'new' | 'used' | 'refurbished';
    location?: string;
    shipping?: boolean;
    tags?: string[];
  };
  paymentAddress: string;         // Address to receive payment
  ordAddress: string;             // Ordinals address for NFTs
  timestamp: number;
  status: 'active' | 'sold' | 'cancelled';
}

interface MarketError {
  code: 
    | 'INSUFFICIENT_FUNDS'
    | 'INVALID_LISTING_DATA'
    | 'IMAGE_TOO_LARGE'
    | 'UNSUPPORTED_ASSET_TYPE'
    | 'CATEGORY_NOT_FOUND'
    | 'RATE_LIMITED';
  message: string;
  details?: {
    required?: number;
    available?: number;
    maxSize?: number;
    supportedTypes?: string[];
  };
}

Basic Usage

import { CreateListingButton } from 'bigblocks';

export default function Marketplace() {
  const handleListingCreated = (listing: Listing) => {
    console.log('Created listing:', listing);
    console.log('Transaction ID:', listing.txid);
    
    // Navigate to listing detail page
    window.location.href = `/marketplace/listing/${listing.id}`;
  };

  const handleError = (error: MarketError) => {
    console.error('Listing creation failed:', error);
    
    if (error.code === 'INSUFFICIENT_FUNDS') {
      alert(`Need ${error.details?.required} satoshis, but only have ${error.details?.available}`);
    } else {
      alert(error.message);
    }
  };

  return (
    <CreateListingButton
      onCreateListing={handleListingCreated}
      onError={handleError}
    />
  );
}

Advanced Usage

Complete Marketplace Integration

import { CreateListingButton } from 'bigblocks';
import { useState, useEffect } from 'react';

export default function AdvancedMarketplace() {
  const [categories, setCategories] = useState<string[]>([]);
  const [userListings, setUserListings] = useState<Listing[]>([]);
  const [listingFee, setListingFee] = useState(0.001);

  useEffect(() => {
    // Fetch available categories
    fetchCategories();
    // Get current listing fee
    fetchListingFee();
  }, []);

  const fetchCategories = async () => {
    try {
      const response = await fetch('/api/market/categories');
      const data = await response.json();
      setCategories(data.categories.map((cat: any) => cat.name));
    } catch (error) {
      console.error('Failed to fetch categories:', error);
    }
  };

  const fetchListingFee = async () => {
    try {
      const response = await fetch('/api/market/listings/estimate-fee', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          assetType: 'DIGITAL',
          imageCount: 1,
          hasMetadata: true
        })
      });
      const data = await response.json();
      setListingFee(data.fee / 100000000); // Convert to BSV
    } catch (error) {
      console.error('Failed to fetch fee:', error);
    }
  };

  const handleListingCreated = async (listing: Listing) => {
    console.log('New listing created:', listing);
    
    // Add to user's listings
    setUserListings(prev => [listing, ...prev]);
    
    // Track analytics
    await fetch('/api/analytics/listing-created', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        listingId: listing.id,
        category: listing.category,
        price: listing.price,
        timestamp: Date.now()
      })
    });

    // Show success notification
    const notification = new Notification('Listing Created!', {
      body: `Your listing "${listing.title}" is now live on the marketplace.`,
      icon: '/icon-192x192.png'
    });

    // Navigate to listing
    setTimeout(() => {
      window.location.href = `/marketplace/listing/${listing.id}`;
    }, 2000);
  };

  return (
    <div className="advanced-marketplace">
      <div className="marketplace-header">
        <h1>Create New Listing</h1>
        <p className="fee-info">
          Listing fee: {listingFee} BSV
        </p>
      </div>

      <CreateListingButton
        categories={categories}
        maxImages={10}
        feeAmount={listingFee}
        variant="card"
        size="lg"
        onCreateListing={handleListingCreated}
        onError={(error) => {
          if (error.code === 'RATE_LIMITED') {
            alert('You can only create 10 listings per hour. Please try again later.');
          } else if (error.code === 'IMAGE_TOO_LARGE') {
            alert(`Image too large. Maximum size: ${error.details?.maxSize} MB`);
          } else {
            alert(`Error: ${error.message}`);
          }
        }}
        className="marketplace-create-button"
      />

      {userListings.length > 0 && (
        <div className="user-listings mt-8">
          <h2>Your Recent Listings</h2>
          <div className="listings-grid">
            {userListings.slice(0, 6).map((listing) => (
              <div key={listing.id} className="listing-card">
                <img src={listing.images[0]} alt={listing.title} />
                <h3>{listing.title}</h3>
                <p className="price">{listing.price / 100000000} BSV</p>
                <span className="status">{listing.status}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

NFT Marketplace Creation

import { CreateListingButton } from 'bigblocks';

interface NFTMetadata {
  name: string;
  description: string;
  image: string;
  attributes: Array<{
    trait_type: string;
    value: string | number;
  }>;
  rarity?: string;
  collection?: string;
}

export default function NFTMarketplace() {
  const handleNFTListing = async (listing: Listing) => {
    console.log('NFT listing created:', listing);
    
    // For NFT listings, the ordAddress is particularly important
    console.log('Ordinals address:', listing.ordAddress);
    console.log('Asset type:', listing.assetType); // BSV721
    
    // Store NFT metadata on-chain
    await storeNFTMetadata(listing.id, listing.metadata as NFTMetadata);
  };

  return (
    <div className="nft-marketplace">
      <h1>List Your NFT</h1>
      
      <CreateListingButton
        variant="card"
        categories={['Art', 'Collectibles', 'Gaming', 'Music', 'Photography']}
        maxImages={10}
        feeAmount={0.01} // Higher fee for NFT listings
        onCreateListing={handleNFTListing}
        className="nft-create-listing"
      />
      
      <div className="listing-tips">
        <h3>Tips for NFT Listings</h3>
        <ul>
          <li>Use high-quality images (at least 1000x1000px)</li>
          <li>Include detailed metadata and attributes</li>
          <li>Specify rarity and collection information</li>
          <li>Set competitive prices based on market trends</li>
        </ul>
      </div>
    </div>
  );
}

Common Patterns

Multi-Step Listing Creation

import { CreateListingButton } from 'bigblocks';
import { useState } from 'react';

export default function MultiStepListing() {
  const [step, setStep] = useState<'prepare' | 'create' | 'complete'>('prepare');
  const [preparedData, setPreparedData] = useState<any>(null);
  const [createdListing, setCreatedListing] = useState<Listing | null>(null);

  const handlePrepare = () => {
    // Prepare listing data
    const data = {
      title: document.getElementById('title')?.value,
      description: document.getElementById('description')?.value,
      price: parseFloat(document.getElementById('price')?.value) * 100000000,
      category: document.getElementById('category')?.value,
    };
    
    setPreparedData(data);
    setStep('create');
  };

  const handleListingCreated = (listing: Listing) => {
    setCreatedListing(listing);
    setStep('complete');
  };

  return (
    <div className="multi-step-listing">
      {step === 'prepare' && (
        <div className="step-prepare">
          <h2>Prepare Your Listing</h2>
          <form>
            <input id="title" placeholder="Title" required />
            <textarea id="description" placeholder="Description" required />
            <input id="price" type="number" placeholder="Price in BSV" required />
            <select id="category" required>
              <option value="">Select Category</option>
              <option value="Electronics">Electronics</option>
              <option value="Art">Art</option>
              <option value="Collectibles">Collectibles</option>
            </select>
          </form>
          <button onClick={handlePrepare}>Next: Add Images</button>
        </div>
      )}

      {step === 'create' && (
        <div className="step-create">
          <h2>Complete Your Listing</h2>
          <div className="prepared-info">
            <h3>{preparedData.title}</h3>
            <p>{preparedData.description}</p>
            <p className="price">{preparedData.price / 100000000} BSV</p>
          </div>
          
          <CreateListingButton
            variant="inline"
            onCreateListing={handleListingCreated}
            className="complete-listing-button"
          />
        </div>
      )}

      {step === 'complete' && createdListing && (
        <div className="step-complete">
          <h2>✅ Listing Created Successfully!</h2>
          <div className="listing-summary">
            <h3>{createdListing.title}</h3>
            <p>Transaction ID: {createdListing.txid}</p>
            <p>Listing ID: {createdListing.id}</p>
            <p>Status: {createdListing.status}</p>
          </div>
          
          <div className="next-actions">
            <button onClick={() => window.location.href = `/marketplace/listing/${createdListing.id}`}>
              View Listing
            </button>
            <button onClick={() => window.location.href = '/marketplace/dashboard'}>
              Go to Dashboard
            </button>
            <button onClick={() => setStep('prepare')}>
              Create Another
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

Bulk Listing Upload

import { CreateListingButton } from 'bigblocks';
import { useState } from 'react';

interface BulkListingData {
  title: string;
  description: string;
  price: number;
  category: string;
  images: File[];
}

export default function BulkListingUpload() {
  const [bulkData, setBulkData] = useState<BulkListingData[]>([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [created, setCreated] = useState<Listing[]>([]);

  const handleCSVUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    // Parse CSV and populate bulkData
    const reader = new FileReader();
    reader.onload = (e) => {
      const csv = e.target?.result as string;
      const parsed = parseCSV(csv);
      setBulkData(parsed);
    };
    reader.readAsText(file);
  };

  const handleListingCreated = (listing: Listing) => {
    setCreated(prev => [...prev, listing]);
    
    // Move to next listing
    if (currentIndex < bulkData.length - 1) {
      setCurrentIndex(currentIndex + 1);
    } else {
      console.log('All listings created!', created);
    }
  };

  const currentListing = bulkData[currentIndex];

  return (
    <div className="bulk-listing-upload">
      <h2>Bulk Listing Upload</h2>
      
      {bulkData.length === 0 ? (
        <div className="upload-section">
          <input 
            type="file" 
            accept=".csv"
            onChange={handleCSVUpload}
          />
          <p>Upload a CSV file with your listings</p>
        </div>
      ) : (
        <div className="bulk-creation">
          <div className="progress">
            Creating listing {currentIndex + 1} of {bulkData.length}
          </div>
          
          <div className="current-listing">
            <h3>{currentListing.title}</h3>
            <p>{currentListing.description}</p>
            <p>{currentListing.price} BSV</p>
          </div>
          
          <CreateListingButton
            variant="inline"
            onCreateListing={handleListingCreated}
            onError={(error) => {
              console.error(`Failed to create listing ${currentIndex + 1}:`, error);
              // Skip to next
              if (currentIndex < bulkData.length - 1) {
                setCurrentIndex(currentIndex + 1);
              }
            }}
          />
          
          <div className="created-count">
            ✅ {created.length} listings created successfully
          </div>
        </div>
      )}
    </div>
  );
}

Service Marketplace

import { CreateListingButton } from 'bigblocks';

interface ServiceListing extends Listing {
  metadata: {
    serviceType: 'consultation' | 'development' | 'design' | 'writing' | 'other';
    deliveryTime: string;
    revisions: number;
    requirements?: string[];
  };
}

export default function ServiceMarketplace() {
  const handleServiceListing = (listing: Listing) => {
    const serviceListing = listing as ServiceListing;
    
    console.log('Service listing created:', {
      title: serviceListing.title,
      type: serviceListing.metadata.serviceType,
      delivery: serviceListing.metadata.deliveryTime,
      price: `${serviceListing.price / 100000000} BSV`
    });
  };

  return (
    <div className="service-marketplace">
      <h1>Offer Your Services</h1>
      
      <CreateListingButton
        categories={[
          'Consultation',
          'Development', 
          'Design',
          'Writing',
          'Marketing',
          'Other Services'
        ]}
        variant="card"
        size="lg"
        onCreateListing={handleServiceListing}
        className="service-listing-button"
      />
      
      <div className="service-examples">
        <h3>Popular Services</h3>
        <ul>
          <li>🖥️ Smart Contract Development</li>
          <li>🎨 NFT Art Creation</li>
          <li>✍️ Technical Writing</li>
          <li>📊 Blockchain Consulting</li>
          <li>🔍 Security Audits</li>
        </ul>
      </div>
    </div>
  );
}

Physical Goods with Shipping

import { CreateListingButton } from 'bigblocks';

export default function PhysicalGoodsMarketplace() {
  const shippingRegions = ['Local', 'National', 'International'];
  
  const handlePhysicalListing = async (listing: Listing) => {
    if (listing.assetType !== 'PHYSICAL') return;
    
    // Calculate shipping costs
    const shippingCosts = await calculateShipping({
      weight: listing.metadata?.weight,
      dimensions: listing.metadata?.dimensions,
      destination: listing.metadata?.location
    });
    
    console.log('Physical item listed:', {
      title: listing.title,
      shipping: listing.metadata?.shipping,
      location: listing.metadata?.location,
      estimatedShipping: shippingCosts
    });
  };

  return (
    <div className="physical-goods-marketplace">
      <h1>Sell Physical Items</h1>
      
      <CreateListingButton
        categories={[
          'Electronics',
          'Books',
          'Collectibles',
          'Art',
          'Jewelry',
          'Home & Garden',
          'Sports & Outdoors'
        ]}
        variant="card"
        maxImages={10}
        onCreateListing={handlePhysicalListing}
      />
      
      <div className="shipping-info">
        <h3>Shipping Information</h3>
        <p>When listing physical items, please include:</p>
        <ul>
          <li>Item weight and dimensions</li>
          <li>Shipping regions you support</li>
          <li>Estimated delivery times</li>
          <li>Return policy</li>
        </ul>
      </div>
    </div>
  );
}

Authentication Requirements

The CreateListingButton requires Bitcoin authentication for creating listings:

import { 
  BitcoinAuthProvider, 
  BitcoinQueryProvider, 
  CreateListingButton 
} from 'bigblocks';

function App() {
  return (
    <BitcoinQueryProvider>
      <BitcoinAuthProvider config={{ 
        apiUrl: '/api',
        walletMode: 'integrated' // Required for payment
      }}>
        <CreateListingButton 
          onCreateListing={(listing) => {
            console.log('Created listing:', listing);
          }}
        />
      </BitcoinAuthProvider>
    </BitcoinQueryProvider>
  );
}

API Integration

The CreateListingButton integrates with several backend endpoints:

Core Listing Endpoints

// Create new listing
POST /api/market/listings
{
  title: string;
  description: string;
  price: number; // satoshis
  currency: 'BSV' | 'USD';
  category: string;
  images: string[];
  metadata?: object;
  assetType: string;
}

// Upload images
POST /api/market/images/upload
FormData with multiple image files

// Get categories
GET /api/market/categories

// Estimate listing fee
POST /api/market/listings/estimate-fee
{
  assetType: string;
  imageCount: number;
  hasMetadata: boolean;
}

Real-time Updates

// Server-Sent Events for marketplace
GET /api/market/stream

Events:
- listing_created: { listing: Listing }
- listing_sold: { listingId: string, buyer: string }
- listing_cancelled: { listingId: string }
- price_updated: { listingId: string, newPrice: number }

Features

  • On-chain Storage: Permanent blockchain marketplace listings
  • Multi-Image Upload: Support for multiple images with automatic thumbnails
  • Category System: Organized hierarchical category structure
  • Flexible Pricing: Support for BSV or USD pricing
  • Rich Metadata: Detailed item information and attributes
  • Preview Mode: Review listing before blockchain submission
  • Fee Estimation: Calculate listing fees before creation
  • Draft Saving: Save listings as drafts before publishing
  • Asset Types: Support for digital goods, NFTs, and physical items

Error Handling

Comprehensive Error Management

import { CreateListingButton } from 'bigblocks';

export default function RobustListingCreation() {
  const handleError = (error: MarketError) => {
    switch (error.code) {
      case 'INSUFFICIENT_FUNDS':
        console.log(`Need ${error.details?.required} satoshis for listing fee`);
        // Prompt user to add funds
        showFundingModal(error.details?.required);
        break;
        
      case 'IMAGE_TOO_LARGE':
        alert(`Image exceeds maximum size of ${error.details?.maxSize} MB`);
        break;
        
      case 'RATE_LIMITED':
        alert('You have reached the hourly listing limit. Please try again later.');
        break;
        
      case 'UNSUPPORTED_ASSET_TYPE':
        console.error('Asset type not supported:', error.details?.supportedTypes);
        break;
        
      default:
        alert(`Error creating listing: ${error.message}`);
    }
  };

  return (
    <CreateListingButton 
      onError={handleError}
      onCreateListing={(listing) => {
        console.log('Success:', listing);
      }}
    />
  );
}

Styling

The component includes multiple display variants:

/* Button variant */
.create-listing-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.5rem;
  background: var(--primary-color);
  color: white;
  border-radius: 0.5rem;
  font-weight: 600;
  transition: all 0.2s;
}

/* Card variant */
.create-listing-card {
  border: 1px solid var(--border-color);
  border-radius: 0.75rem;
  padding: 2rem;
  background: var(--card-background);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Inline variant */
.create-listing-inline {
  width: 100%;
  padding: 1rem;
  border: 2px dashed var(--border-color);
  border-radius: 0.5rem;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.2s;
}

.create-listing-inline:hover {
  border-color: var(--primary-color);
}

/* Modal form */
.listing-form-modal {
  max-width: 600px;
  max-height: 90vh;
  overflow-y: auto;
}

/* Image upload area */
.image-upload-zone {
  border: 2px dashed var(--border-color);
  border-radius: 0.5rem;
  padding: 2rem;
  text-align: center;
  cursor: pointer;
}

.image-preview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

Performance Optimization

Image Optimization

import { CreateListingButton } from 'bigblocks';

export default function OptimizedImageUpload() {
  const processImages = async (images: File[]) => {
    // Resize and compress images before upload
    const optimized = await Promise.all(
      images.map(async (file) => {
        if (file.size > 1024 * 1024) { // > 1MB
          return compressImage(file, {
            maxWidth: 1920,
            maxHeight: 1080,
            quality: 0.8
          });
        }
        return file;
      })
    );
    
    return optimized;
  };

  return (
    <CreateListingButton 
      maxImages={20} // Allow more images since they're optimized
      onCreateListing={(listing) => {
        console.log('Optimized listing created:', listing);
      }}
    />
  );
}

Troubleshooting

Common Issues

Form validation errors

  • Ensure all required fields are filled
  • Check price is a valid number > 0
  • Verify at least one image is uploaded
  • Confirm category is selected

Image upload failures

  • Check file size (max 10MB per image)
  • Verify supported formats (JPG, PNG, GIF, WebP)
  • Ensure stable internet connection
  • Try reducing image dimensions

Insufficient funds error

  • Check wallet balance for listing fee
  • Fee includes base fee + image storage
  • Typical fee: 0.001-0.01 BSV

Rate limiting

  • Maximum 10 listings per hour
  • Wait for cooldown period
  • Consider bulk upload for multiple items

API Reference

CreateListingButton Component

interface CreateListingButtonProps {
  onCreateListing?: (listing: Listing) => void;
  onError?: (error: MarketError) => void;
  categories?: string[];
  maxImages?: number;
  requiresApproval?: boolean;
  feeAmount?: number;
  variant?: 'button' | 'card' | 'inline';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

Usage with React Query

import { useMutation } from '@tanstack/react-query';
import { CreateListingButton } from 'bigblocks';

function CreateListingWithQuery() {
  const createMutation = useMutation({
    mutationFn: (listingData: any) =>
      fetch('/api/market/listings', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(listingData)
      }).then(res => res.json()),
    onSuccess: (data) => {
      console.log('Listing created:', data.listing);
      queryClient.invalidateQueries(['market-listings']);
    }
  });

  return (
    <CreateListingButton 
      onCreateListing={(listing) => {
        console.log('Component listing:', listing);
        // Additional processing if needed
      }}
    />
  );
}