In this guide we will go through the process of creating a custom marketplace from a few simple tools from the Sequence stack.

The tools will enable you to perform:

  1. Minting: Minting of tokens to your wallet from the Sequence Builder
  2. Wallet Authentication: Use of Web SDK to authenticate a user
  3. Marketplace SDK set up: Basic Guide to Configure the Marketplace SDK
  4. Displaying collections: Retrieve and display the marketplace’s available collections.
  5. Listings and Offers: Enable users to list tokens for sale or make offers

1. Minting

The first step is to create a collectible from the Sequence Builder and mint a few tokens, which can be accomplished with this guide and to use the tokenId you minted in the following steps to query and fulfill orders.

2. Wallet Authentication

For your project, you’ll need a way to authenticate your user with a wallet.

For this guide we’ll use an Embedded Wallet with Web SDK connector which can authenticate users using Google or Apple auth, in addition to user brought wallets like Coinbase or Metamask.

Install Packages

Either you can start from a great foundation with our Marketplace Boilerplate, which already includes all of this integrated with a great UI, or we will walk you through how to use React from scratch here.

Start by creating a project in a folder of your name choosing:

npm
npm create vite --template react-ts

# or

pnpm create vite --template react-ts

# or

yarn create vite --template react-ts

Then, begin by installing the required packages in the <project_name> folder

pnpm
npm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

# or

pnpm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

# or

yarn add @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

Create a config

config.ts
import { createConfig } from "@0xsequence/connect";



const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;

const waasConfigKey = import.meta.env.VITE_WAAS_CONFIG_KEY;

const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;

const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;



export const config: any = createConfig("waas", {

  projectAccessKey,

  chainIds: [1, 137],

  defaultChainId: 1,

  appName: "Demo Dapp",

  waasConfigKey, // for waas

  google: {

    clientId: googleClientId,

  },

  walletConnect: {

    projectId: walletConnectId,

  },

});

Make sure to add all the chainIds you’re going to use

Create a layout in the main.tsx

main.tsx
import { StrictMode } from 'react'

import { createRoot } from 'react-dom/client'

import App from './App.tsx'

import "./index.css";

import { config } from "./config";

import { SequenceConnect } from "@0xsequence/connect";



function Dapp() {

  return (

    <SequenceConnect config={config}>

      <App />

    </SequenceConnect>

  );

}



createRoot(document.getElementById('root')!).render(

  <StrictMode>

    <Dapp />

  </StrictMode>,

)

Authentication Component

To complete the Authentication, you will need the authentication component

App.tsx
import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();



  function handleDisconnect() {

    disconnect();

  }



  if (isConnected && address)

    return (

      <>

        <p>User Address: {address}</p>

        <button onClick={handleDisconnect}>Disconnect</button>

      </>

    );



  return (

    <>

      <button onClick={() => setOpenConnectModal(true)}>Connect</button>

    </>

  );

}



export default App;

3. Marketplace SDK set up

Marketplace SDK is a comprehensive toolkit that seamlessly integrates our Marketplaces into applications. More about Marketplace SDK

npm install @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

# or

pnpm add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

# or

yarn add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

Add the required CSS imports to your main styles file

index.css
@import 'tailwindcss';

@import '@0xsequence/marketplace-sdk/styles/preset';

@import '@0xsequence/design-system/preset';

Configure Tailwind CSS plugin in vite.config.ts

vite.config.ts
import { defineConfig } from "vite";

import react from "@vitejs/plugin-react";

import tailwindcss from "@tailwindcss/vite";



// https://vite.dev/config/

export default defineConfig({

  plugins: [react(), tailwindcss()],

});

Create a config

config.ts
import type { SdkConfig } from '@0xsequence/marketplace-sdk';



const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;

const projectId = import.meta.env.VITE_PROJECT_ID!;

const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;



export function getConfig() {

  const config = {

    projectId,

    projectAccessKey,

    walletConnectProjectId: walletConnectId

  } satisfies SdkConfig;



  return config;

}

Add the Marketplace SDK Providers Alongside Your Web SDK Providers

App.tsx
import { StrictMode } from 'react'

import { createRoot } from 'react-dom/client'

import App from './App.tsx'

import "./index.css";

import { config } from "./config";

import { SequenceConnect } from "@0xsequence/connect";

import {

  MarketplaceProvider,

  ModalProvider,

} from "@0xsequence/marketplace-sdk/react";

import { getConfig } from "./config";

import { ThemeProvider, ToastProvider } from '@0xsequence/design-system';



const sdkConfig = getConfig();



function Dapp() {

  return (

    <ThemeProvider>

      <ToastProvider>

        <SequenceConnect config={config}>

          <MarketplaceProvider config={sdkConfig}>

            <App />

            <ModalProvider />

          </MarketplaceProvider>

        </SequenceConnect>

      </ToastProvider>

    </ThemeProvider>

  );

}



createRoot(document.getElementById('root')!).render(

  <StrictMode>

    <Dapp />

  </StrictMode>,

)

4. Displaying collections

To display the marketplace collections we need to create a folder named Collections inside the src/app/components directory. Within this folder, we will create two files: index.tsx and Collection.tsx.

Collection.tsx
import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useCollection } from "@0xsequence/marketplace-sdk/react";



export const Collection = ({

  collection,

  onSelectCollection,

}: {

  collection: MarketplaceCollection;

  onSelectCollection: (value: MarketplaceCollection) => void;

}) => {

  const { data } = useCollection({

    chainId: collection.chainId,

    collectionAddress: collection.address,

  });



  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing

  const logoURI = data?.logoURI;

  // You can add a placeholder image to improve the UX

  const image = data?.extensions.ogImage || collection.bannerUrl || logoURI;

  const description = data?.extensions.description;

  const name = data?.name;

  const symbol = data?.symbol;

  const contractType = data?.type;



  function handleOnSelectCollection() {

    onSelectCollection(collection);

  }



  return (

    <div

      onClick={handleOnSelectCollection}

      className="flex flex-col gap-2 border rounded-lg cursor-pointer w-[400px]"

    >

      <h2>

        {name} {symbol && <span>({symbol})</span>}

      </h2>

      <p>{collection.address}</p>

      <p>Chain ID: {collection.chainId}</p>

      <img

        src={image}

        alt={collection.address}

        className="w-[200px] h-[200px] object-cover object-center"

      />

      <p>Contract type: {contractType}</p>

      <p>{description}</p>

    </div>

  );

};
index.tsx
import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { Collection } from "./Collection";

import { useMarketplaceConfig } from "@0xsequence/marketplace-sdk/react";



export const Collections = ({ onSelectCollection } : { onSelectCollection: (value: MarketplaceCollection) => void }) => {

	const { data } = useMarketplaceConfig();



	const collections: MarketplaceCollection[] = data?.collections || [];

  return (

    <div>

      {collections?.map((collection) => (

        <Collection key={collection.address} collection={collection} onSelectCollection={onSelectCollection}/>

      ))}

    </div>

  );

};

To finish we will add the Collections component in the App.tsx

import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";

import { Collections } from "./components/Collections";

import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useState } from "react";

import { Collectibles } from "./components/Collectibles";

import type { Address } from "viem";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();

  const [collectionSelected, setCollectionSelected] =

    useState<MarketplaceCollection | null>(null);



  function handleDisconnect() {

    disconnect();

  }

  

  function onSelectCollection(value: MarketplaceCollection) {

    setCollectionSelected(value);

  }



  function onRestartSelectedCollectionValue() {

    setCollectionSelected(null);

  }



  if (!isConnected && !address) {

    return (

      <>

        <button onClick={() => setOpenConnectModal(true)}>Connect</button>

      </>

    );

  }



  return (

    <>

      <p>User Address: {address}</p>

      <button onClick={handleDisconnect}>Disconnect</button>

      {!collectionSelected ? (

        <Collections onSelectCollection={onSelectCollection} />

      ) : (

        <>

          <button onClick={onRestartSelectedCollectionValue}>Show Collections</button>

          <div>{JSON.stringify(collectionSelected)}</div>

        </>

      )}

    </>

  );

}



export default App;

5. Listings and Offers

This section is for integrating the listings and offers from our marketplace into our UI.

Create a Types File

Before starting this, we need a file with some types for our components inside src/utils. We’ll use this to type our functions and keep our codebase consistent and maintainable.

types.ts
import type {

  MarketplaceKind,

  Order,

  OrderbookKind,

  Step,

} from "@0xsequence/marketplace-sdk";

import type { Address, Hash, Hex } from "viem";



type ModalCallbacks = {

  onSuccess?: ({ hash, orderId }: { hash?: Hash; orderId?: string }) => void;

  onError?: (error: Error) => void;

  onBuyAtFloorPrice?: () => void;

};



export type ShowBuyModalArgs = {

  orderId: string;

  chainId: number;

  collectionAddress: Address;

  collectibleId: string;

  marketplace: MarketplaceKind;

  customCreditCardProviderCallback?: (buyStep: Step) => void;

  skipNativeBalanceCheck?: boolean;

  nativeTokenAddress?: Address;

};



export type ShowCreateListingModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  collectibleId: string;

  orderbookKind?: OrderbookKind;

  callbacks?: ModalCallbacks;

};

export type ShowMakeOfferModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  collectibleId: string;

  orderbookKind?: OrderbookKind;

  callbacks?: ModalCallbacks;

};



export type ShowSellModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  tokenId: string;

  order: Order;

  callbacks?: ModalCallbacks;

};

Set Up the Collectibles Components

To create the Items page and its components, we need to add a new folder named Collectibles inside the src/app/components directory. Within this folder, we will create two files: index.tsx and Collectible.tsx.

Collectible.tsx
import type {

  ShowBuyModalArgs,

  ShowCreateListingModalArgs,

  ShowMakeOfferModalArgs,

  ShowSellModalArgs,

} from "../../utils/types";

import type {

  CollectibleOrder,

  MarketplaceKind,

  OrderbookKind,

} from "@0xsequence/marketplace-sdk";

import { useBalanceOfCollectible } from "@0xsequence/marketplace-sdk/react";

import type { Address } from "viem";



export const Collectible = ({

  collectible,

  chainId,

  collectionAddress,

  showBuyModal,

  showListModal,

  showOfferModal,

  showSellModal,

  address,

  isConnected,

  orderbookKind,

}: {

  collectible: CollectibleOrder;

  chainId: string;

  collectionAddress: Address;

  showBuyModal: (args: ShowBuyModalArgs) => void;

  showListModal: (args: ShowCreateListingModalArgs) => void;

  showOfferModal: (args: ShowMakeOfferModalArgs) => void;

  showSellModal: (args: ShowSellModalArgs) => void;

  address?: Address;

  isConnected: boolean;

  orderbookKind: OrderbookKind;

}) => {

  const { name, image, tokenId } = collectible.metadata;



  // @ts-ignore: unused variable 'isBalanceLoading'

  const { data: userBalanceResp, isLoading: isBalanceLoading } =

    useBalanceOfCollectible({

      chainId: Number(chainId),

      collectionAddress,

      collectableId: tokenId,

      userAddress: address,

      query: {

        enabled: !!isConnected && !!address,

      },

    });



  const tokenBalance = userBalanceResp?.balance;



  const onClickBuy = () =>

    showBuyModal({

      chainId: Number(chainId),

      collectionAddress,

      collectibleId: tokenId,

      orderId: collectible!.order!.orderId,

      marketplace: orderbookKind as unknown as MarketplaceKind,

    });



  const onClickList = () => {

    showListModal({

      collectionAddress,

      chainId: Number(chainId),

      collectibleId: tokenId,

      orderbookKind,

    });

  };



  const onClickOffer = () => {

    showOfferModal({

      collectionAddress,

      chainId: Number(chainId),

      collectibleId: tokenId,

      orderbookKind,

    });

  };



  const onAcceptOffer = () => {

    showSellModal({

      collectionAddress,

      chainId: Number(chainId),

      tokenId,

      order: collectible!.offer!,

    });

  };



  const hasOffer = Boolean(collectible?.offer);

  const sellDisabled = !isConnected || !hasOffer || !tokenBalance;

  const showActionButtons = address && isConnected;



  return (

    <div className="flex flex-col justify-center items-center border border-white">

      <h2>{name}</h2>

      <img

        src={image}

        alt={name}

        className="w-[200px] h-[200px] object-cover object-center"

      />

      {showActionButtons && (

        <>

          {collectible.order && <button onClick={onClickBuy}>Buy</button>}

          {tokenBalance && <button onClick={onClickList}>List</button>}

          <button onClick={onClickOffer}>Make offer</button>

          {!sellDisabled && (

            <button onClick={onAcceptOffer}>Accept Offer</button>

          )}

        </>

      )}

    </div>

  );

};
index.tsx
import { OrderbookKind, OrderSide } from "@0xsequence/marketplace-sdk";

import {

  useBuyModal,

  useCreateListingModal,

  useListCollectibles,

  useMakeOfferModal,

  useMarketplaceConfig,

  useSellModal,

} from "@0xsequence/marketplace-sdk/react";

import { Collectible } from "./Collectible";

import type { Address } from "viem";

import { useAccount } from "wagmi";



export const Collectibles = ({

  collectionId,

  chainId,

}: {

  collectionId: Address;

  chainId: number;

}) => {

  const { address, isConnected } = useAccount();

  const {

    data: collectibles,

    // @ts-ignore: unused variable 'collectiblesLoading'

    isLoading: collectiblesLoading,

    // @ts-ignore: unused variable 'fetchNextCollectibles'

    fetchNextPage: fetchNextCollectibles,

  } = useListCollectibles({

    chainId: Number(chainId),

    collectionAddress: collectionId,

    filter: {

      // # Optional filters

      includeEmpty: true,

      // searchText: text,

      // properties,

    },

    side: OrderSide.listing,

  });



  const { data } = useMarketplaceConfig();



  const onError = (error: Error) => {

    console.error(error.message);

  };



  const { show: showBuyModal } = useBuyModal({

    onSuccess(hash) {

      console.log("Buy transaction sent with hash: ", hash);

    },

    onError,

  });



  const { show: showListModal } = useCreateListingModal({ onError });

  const { show: showOfferModal } = useMakeOfferModal({

    onError,

  });

  const { show: showSellModal } = useSellModal({ onError });



  const collectiblesFlat =

    collectibles?.pages.flatMap((p) => p.collectibles) ?? [];

  const collectionData =

    data?.collections?.find(

      (collection) => collection.address === collectionId

    ) || null;

  const orderbookKind: OrderbookKind =

    (collectionData?.destinationMarketplace || "") as unknown as OrderbookKind;



  return (

    <div>

      <h1 className="text-[32px] font-semibold">Collectibles</h1>

      {collectiblesFlat?.map((collectible) => (

        <Collectible

          key={collectible.metadata.tokenId}

          collectible={collectible}

          chainId={String(chainId)}

          collectionAddress={collectionId}

          showBuyModal={showBuyModal}

          showListModal={showListModal}

          showOfferModal={showOfferModal}

          showSellModal={showSellModal}

          address={address}

          isConnected={isConnected}

          orderbookKind={orderbookKind}

        />

      ))}

    </div>

  );

};

Finally, add the component for Listings and Offers into our App.tsx file

App.tsx
import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";

import { Collections } from "./components/Collections";

import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useState } from "react";

import { Collectibles } from "./components/Collectibles";

import type { Address } from "viem";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();

  const [collectionSelected, setCollectionSelected] =

    useState<MarketplaceCollection | null>(null);



  function handleDisconnect() {

    disconnect();

  }



  function onSelectCollection(value: MarketplaceCollection) {

    setCollectionSelected(value);

  }



  function onRestartSelectedCollectionValue() {

    setCollectionSelected(null);

  }



  if (!isConnected && !address) {

    return (

      <>

        <button onClick={() => setOpenConnectModal(true)}>Connect</button>

      </>

    );

  }



  return (

    <>

      <p>User Address: {address}</p>

      <button onClick={handleDisconnect}>Disconnect</button>

      {!collectionSelected ? (

        <Collections onSelectCollection={onSelectCollection} />

      ) : (

        <>

          <button onClick={onRestartSelectedCollectionValue}>

            Show Collections

          </button>

          <Collectibles

            chainId={collectionSelected.chainId}

            collectionId={collectionSelected.address as Address}

          />

        </>

      )}

    </>

  );

}



export default App;

Run and test

You’ve successfully added the items to your new page, and they should now be visible in the UI. To see them in action, run pnpm dev. From the landing page, select any of your collections to view the NFTs it contains. Make sure everything works as expected by testing key flows like listing, purchasing, making offers, and accepting offers on an NFT.