Time: ~60 minutes
Prerequisites: Completed the Build a Game tutorial
Prerequisites: Completed the Build a Game tutorial
Sample Projects Available — See working marketplace implementations:
- Unity Sample Game (
mainbranch) — Orderbook and zkEVM API integration - Unreal Sample Game — Full marketplace with listings, buying, and cancellation
What We’re Building
- Browse NFT listings
- Display player inventory
- Create sell listings
- Purchase NFTs from other players
- Cancel listings
Installation
- TypeScript
- Unity
- Unreal
Copy
Ask AI
npm install @imtbl/auth @imtbl/wallet @imtbl/orderbook @imtbl/blockchain-data @imtbl/config viem
.env.local):Copy
Ask AI
NEXT_PUBLIC_CLIENT_ID=your_client_id
NEXT_PUBLIC_PUBLISHABLE_KEY=your_publishable_key
NEXT_PUBLIC_CONTRACT_ADDRESS=your_nft_contract
Get your Client ID and Publishable Key from Hub. See API Keys for details.
- Open Window → Package Manager
- Click + → Add package from git URL
- Enter:
https://github.com/immutable/unity-immutable-sdk.git - Click Add
Unity Orderbook is in Alpha. API may change.
- Download the plugin from GitHub releases
- Copy
ImmutableSDKfolder to your project’sPluginsdirectory - Restart Unreal Editor
- Enable the plugin in Edit → Plugins → Immutable SDK
Unreal Orderbook is in Alpha. API may change.
Display Player Inventory
Before listing NFTs for sale, players need to see what they own.- TypeScript
- Unity
- Unreal
Copy
Ask AI
import { BlockchainData } from '@imtbl/blockchain-data';
import { Environment } from '@imtbl/config';
const indexer = new BlockchainData({
baseConfig: { environment: Environment.SANDBOX },
});
async function loadInventory(address: string) {
const { result } = await indexer.listNFTsByAccountAddress({
chainName: 'imtbl-zkevm-testnet',
accountAddress: address,
contractAddress: CONTRACT_ADDRESS,
});
return result.map(nft => ({
tokenId: nft.token_id,
name: nft.name || `Item #${nft.token_id}`,
image: nft.image || '/placeholder.png',
attributes: nft.attributes,
}));
}
Copy
Ask AI
using System.Collections.Generic;
using Immutable.Api.ZkEvm.Api;
using Immutable.Api.ZkEvm.Model;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
private NftsApi nftsApi;
void Start()
{
nftsApi = new NftsApi();
}
public async void LoadInventory(string walletAddress)
{
try
{
var response = await nftsApi.ListNFTsByAccountAddressAsync(
chainName: "imtbl-zkevm-testnet",
accountAddress: walletAddress,
contractAddress: CONTRACT_ADDRESS
);
foreach (var nft in response.Result)
{
Debug.Log($"NFT: {nft.Name} (Token ID: {nft.TokenId})");
// Add to UI inventory grid
AddToInventoryUI(nft);
}
}
catch (ApiException e)
{
Debug.LogError($"Failed to load inventory: {e.Message}");
}
}
private void AddToInventoryUI(NFTWithBalance nft)
{
// Instantiate inventory slot prefab and populate with NFT data
var slot = Instantiate(inventorySlotPrefab, inventoryGrid);
slot.GetComponent<InventorySlot>().SetData(nft);
}
}
Copy
Ask AI
#include "Immutable/ImmutableSubsystem.h"
#include "Immutable/ImmutableDataTypes.h"
void AMarketplaceController::LoadInventory(const FString& WalletAddress)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
Immutable->ListNFTsByOwner(
WalletAddress,
CONTRACT_ADDRESS,
FImmutableNFTsComplete::CreateUObject(this, &AMarketplaceController::OnInventoryLoaded),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed)
);
}
void AMarketplaceController::OnInventoryLoaded(const TArray<FImmutableNFT>& NFTs)
{
for (const FImmutableNFT& NFT : NFTs)
{
UE_LOG(LogTemp, Log, TEXT("NFT: %s (Token ID: %s)"), *NFT.Name, *NFT.TokenId);
// Add to UI
AddNFTToInventoryWidget(NFT);
}
}
Display Marketplace Listings
Show active listings that players can purchase.- TypeScript
- Unity
- Unreal
Copy
Ask AI
import { Orderbook, OrderStatusName } from '@imtbl/orderbook';
import { Environment } from '@imtbl/config';
import { formatEther } from 'viem';
const orderbook = new Orderbook({
baseConfig: {
environment: Environment.SANDBOX,
publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY!,
},
});
async function loadListings() {
const { result } = await orderbook.listListings({
sellItemContractAddress: CONTRACT_ADDRESS,
status: OrderStatusName.ACTIVE,
pageSize: 50,
});
return result.map(listing => ({
id: listing.id,
tokenId: listing.sell[0].tokenId,
name: listing.sell[0].name || `Item #${listing.sell[0].tokenId}`,
image: listing.sell[0].image,
price: formatEther(BigInt(listing.buy[0].amount)),
seller: listing.accountAddress,
}));
}
Copy
Ask AI
using System.Collections.Generic;
using Immutable.Orderbook;
using Immutable.Orderbook.Model;
using UnityEngine;
public class MarketplaceManager : MonoBehaviour
{
private Orderbook orderbook;
async void Start()
{
orderbook = new Orderbook(Environment.Sandbox);
}
public async void LoadListings()
{
try
{
var response = await orderbook.ListListings(new ListListingsRequest
{
SellItemContractAddress = CONTRACT_ADDRESS,
Status = OrderStatus.Active,
PageSize = 50
});
foreach (var listing in response.Result)
{
Debug.Log($"Listing: {listing.Id} - {listing.Sell[0].TokenId} for {listing.Buy[0].Amount}");
AddListingToUI(listing);
}
}
catch (Exception e)
{
Debug.LogError($"Failed to load listings: {e.Message}");
}
}
private void AddListingToUI(Listing listing)
{
var card = Instantiate(listingCardPrefab, listingsGrid);
card.GetComponent<ListingCard>().SetData(listing);
}
}
Copy
Ask AI
void AMarketplaceController::LoadListings()
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
FListListingsRequest Request;
Request.SellItemContractAddress = CONTRACT_ADDRESS;
Request.Status = EOrderStatus::Active;
Request.PageSize = 50;
Immutable->ListListings(
Request,
FImmutableListingsComplete::CreateUObject(this, &AMarketplaceController::OnListingsLoaded),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed)
);
}
void AMarketplaceController::OnListingsLoaded(const TArray<FImmutableListing>& Listings)
{
for (const FImmutableListing& Listing : Listings)
{
UE_LOG(LogTemp, Log, TEXT("Listing: %s for %s IMX"), *Listing.TokenId, *Listing.Price);
AddListingToWidget(Listing);
}
}
Create Listings
Let players list their NFTs for sale.- TypeScript
- Unity
- Unreal
Copy
Ask AI
import { Orderbook } from '@imtbl/orderbook';
import { createWalletClient, custom, parseEther } from 'viem';
import { immutableZkEvmTestnet } from 'viem/chains';
async function createListing(tokenId: string, priceInIMX: string) {
const provider = await getProvider();
const [address] = await provider.request({ method: 'eth_accounts' });
const walletClient = createWalletClient({
chain: immutableZkEvmTestnet,
transport: custom(provider),
});
// 1. Prepare the listing
const { orderComponents, orderHash, typedData } = await orderbook.prepareListing({
makerAddress: address,
sell: {
type: 'ERC721',
contractAddress: CONTRACT_ADDRESS,
tokenId: tokenId,
},
buy: {
type: 'NATIVE',
amount: parseEther(priceInIMX).toString(),
},
});
// 2. Sign the listing (gasless)
const signature = await walletClient.signTypedData({
account: address,
...typedData,
});
// 3. Submit the listing
const { result } = await orderbook.createListing({
orderComponents,
orderHash,
orderSignature: signature,
makerFees: [],
});
console.log('Listing created:', result.id);
return result;
}
Copy
Ask AI
using Immutable.Orderbook;
using Immutable.Passport;
using UnityEngine;
public class CreateListingManager : MonoBehaviour
{
private Orderbook orderbook;
private Passport passport;
public async void CreateListing(string tokenId, string priceInWei)
{
try
{
var address = await passport.GetAddress();
// 1. Prepare the listing
var prepareRequest = new PrepareListingRequest(
makerAddress: address,
sell: new ERC721Item(
contractAddress: CONTRACT_ADDRESS,
tokenId: tokenId
),
buy: new NativeItem(amount: priceInWei)
);
var prepareResponse = await orderbook.PrepareListing(prepareRequest);
// 2. Sign the listing (Passport handles signing)
var signature = await passport.SignTypedData(prepareResponse.TypedData);
// 3. Submit the listing
var createRequest = new CreateListingRequest(
orderComponents: prepareResponse.OrderComponents,
orderHash: prepareResponse.OrderHash,
orderSignature: signature
);
var listing = await orderbook.CreateListing(createRequest);
Debug.Log($"Listing created: {listing.Result.Id}");
OnListingCreated?.Invoke(listing.Result);
}
catch (Exception e)
{
Debug.LogError($"Failed to create listing: {e.Message}");
}
}
public event Action<Listing> OnListingCreated;
}
Copy
Ask AI
void AMarketplaceController::CreateListing(const FString& TokenId, const FString& PriceInWei)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
// Get wallet address first
Passport->GetAddress(FImmutableAddressComplete::CreateLambda([this, TokenId, PriceInWei](const FString& Address)
{
// 1. Prepare the listing
FPrepareListingRequest PrepareRequest;
PrepareRequest.MakerAddress = Address;
PrepareRequest.Sell = FERC721Item(CONTRACT_ADDRESS, TokenId);
PrepareRequest.Buy = FNativeItem(PriceInWei);
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
Immutable->PrepareListing(PrepareRequest,
FImmutablePrepareListingComplete::CreateUObject(this, &AMarketplaceController::OnListingPrepared),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}));
}
void AMarketplaceController::OnListingPrepared(const FPrepareListingResponse& Response)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
// 2. Sign the listing
Passport->SignTypedData(Response.TypedData,
FImmutableSignComplete::CreateLambda([this, Response](const FString& Signature)
{
// 3. Submit the listing
FCreateListingRequest CreateRequest;
CreateRequest.OrderComponents = Response.OrderComponents;
CreateRequest.OrderHash = Response.OrderHash;
CreateRequest.OrderSignature = Signature;
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
Immutable->CreateListing(CreateRequest,
FImmutableCreateListingComplete::CreateLambda([](const FListing& Listing)
{
UE_LOG(LogTemp, Log, TEXT("Listing created: %s"), *Listing.Id);
}),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}));
}
Fill Orders (Buy NFTs)
Let players purchase listed NFTs.- TypeScript
- Unity
- Unreal
Copy
Ask AI
async function buyNFT(listingId: string) {
const provider = await getProvider();
const [address] = await provider.request({ method: 'eth_accounts' });
const walletClient = createWalletClient({
chain: immutableZkEvmTestnet,
transport: custom(provider),
});
const publicClient = createPublicClient({
chain: immutableZkEvmTestnet,
transport: http(),
});
// 1. Prepare the fulfillment
const { actions } = await orderbook.fulfillOrder(listingId, address, []);
// 2. Execute all required transactions
for (const action of actions) {
if (action.type === 'TRANSACTION') {
const hash = await walletClient.sendTransaction({
to: action.to,
data: action.data,
value: BigInt(action.value || '0'),
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
console.log('Purchase complete!');
}
Copy
Ask AI
public async void BuyNFT(string listingId)
{
try
{
var address = await passport.GetAddress();
// 1. Prepare the fulfillment
var fulfillResponse = await orderbook.FulfillOrder(listingId, address);
// 2. Execute all required transactions
foreach (var action in fulfillResponse.Actions)
{
if (action.Type == ActionType.Transaction)
{
var txHash = await passport.ZkEvmSendTransaction(new TransactionRequest
{
to = action.To,
data = action.Data,
value = action.Value ?? "0"
});
Debug.Log($"Transaction sent: {txHash}");
// Wait for confirmation
await WaitForTransaction(txHash);
}
}
Debug.Log("Purchase complete!");
OnPurchaseComplete?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"Purchase failed: {e.Message}");
}
}
public event Action OnPurchaseComplete;
Copy
Ask AI
void AMarketplaceController::BuyNFT(const FString& ListingId)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
Passport->GetAddress(FImmutableAddressComplete::CreateLambda([this, ListingId](const FString& Address)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
// 1. Prepare the fulfillment
Immutable->FulfillOrder(ListingId, Address, TArray<FFee>(),
FImmutableFulfillOrderComplete::CreateUObject(this, &AMarketplaceController::OnFulfillmentReady),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}));
}
void AMarketplaceController::OnFulfillmentReady(const FFulfillOrderResponse& Response)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
// 2. Execute all required transactions
ExecuteActionsSequentially(Response.Actions, 0);
}
void AMarketplaceController::ExecuteActionsSequentially(const TArray<FAction>& Actions, int32 Index)
{
if (Index >= Actions.Num())
{
UE_LOG(LogTemp, Log, TEXT("Purchase complete!"));
OnPurchaseComplete.Broadcast();
return;
}
const FAction& Action = Actions[Index];
if (Action.Type == EActionType::Transaction)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
FTransactionRequest TxRequest;
TxRequest.To = Action.To;
TxRequest.Data = Action.Data;
TxRequest.Value = Action.Value.IsEmpty() ? TEXT("0") : Action.Value;
Passport->SendTransaction(TxRequest,
FImmutableTransactionComplete::CreateLambda([this, Actions, Index](const FString& TxHash)
{
UE_LOG(LogTemp, Log, TEXT("Transaction sent: %s"), *TxHash);
// Continue to next action
ExecuteActionsSequentially(Actions, Index + 1);
}),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}
else
{
// Skip non-transaction actions
ExecuteActionsSequentially(Actions, Index + 1);
}
}
Cancel Listings
Let players remove their listings from the marketplace.- TypeScript
- Unity
- Unreal
Copy
Ask AI
async function cancelListing(orderId: string) {
const provider = await getProvider();
const [address] = await provider.request({ method: 'eth_accounts' });
// Option 1: Soft cancel (gasless, instant)
await orderbook.cancelOrders([orderId], address);
console.log('Listing cancelled (soft)');
// Option 2: Hard cancel (on-chain, guaranteed)
const walletClient = createWalletClient({
chain: immutableZkEvmTestnet,
transport: custom(provider),
});
const publicClient = createPublicClient({
chain: immutableZkEvmTestnet,
transport: http(),
});
const { signableAction } = await orderbook.prepareOrderCancellations([orderId]);
const hash = await walletClient.sendTransaction({
to: signableAction.to,
data: signableAction.data,
});
await publicClient.waitForTransactionReceipt({ hash });
console.log('Listing cancelled (on-chain)');
}
Copy
Ask AI
public async void CancelListing(string orderId, bool hardCancel = false)
{
try
{
var address = await passport.GetAddress();
if (!hardCancel)
{
// Soft cancel (gasless, instant)
await orderbook.CancelOrders(new[] { orderId }, address);
Debug.Log("Listing cancelled (soft)");
}
else
{
// Hard cancel (on-chain, guaranteed)
var prepareResponse = await orderbook.PrepareOrderCancellations(new[] { orderId });
var txHash = await passport.ZkEvmSendTransaction(new TransactionRequest
{
to = prepareResponse.SignableAction.To,
data = prepareResponse.SignableAction.Data
});
Debug.Log($"Listing cancelled (on-chain): {txHash}");
}
OnListingCancelled?.Invoke(orderId);
}
catch (Exception e)
{
Debug.LogError($"Cancel failed: {e.Message}");
}
}
Copy
Ask AI
void AMarketplaceController::CancelListing(const FString& OrderId, bool bHardCancel)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
Passport->GetAddress(FImmutableAddressComplete::CreateLambda([this, OrderId, bHardCancel](const FString& Address)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
TArray<FString> OrderIds;
OrderIds.Add(OrderId);
if (!bHardCancel)
{
// Soft cancel (gasless)
Immutable->CancelOrders(OrderIds, Address,
FImmutableCancelComplete::CreateLambda([OrderId](bool Success)
{
UE_LOG(LogTemp, Log, TEXT("Listing cancelled (soft): %s"), *OrderId);
}),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}
else
{
// Hard cancel (on-chain)
Immutable->PrepareOrderCancellations(OrderIds,
FImmutablePrepareCancellationsComplete::CreateUObject(this, &AMarketplaceController::OnCancellationPrepared),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}
}));
}
void AMarketplaceController::OnCancellationPrepared(const FPrepareCancellationsResponse& Response)
{
UImmutableSubsystem* Immutable = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
UImmutablePassport* Passport = Immutable->GetPassport();
FTransactionRequest TxRequest;
TxRequest.To = Response.SignableAction.To;
TxRequest.Data = Response.SignableAction.Data;
Passport->SendTransaction(TxRequest,
FImmutableTransactionComplete::CreateLambda([](const FString& TxHash)
{
UE_LOG(LogTemp, Log, TEXT("Listing cancelled (on-chain): %s"), *TxHash);
}),
FImmutableRequestFailed::CreateUObject(this, &AMarketplaceController::OnRequestFailed));
}
Testing Your Marketplace
- Start your project (TypeScript:
npm run dev, Unity: Play Mode, Unreal: PIE) - Login with Passport
- Create a listing - List one of your NFTs for sale
- Switch accounts - Use a different browser/incognito window
- Purchase the listing - Buy the NFT with the second account
- Verify the transfer - Check that the NFT moved to the buyer
Advanced Features
Once you have the basics working, consider adding:Collection Bids
Collection Bids
Let buyers bid on any NFT in a collection. See Collection Bids.
Price Filtering
Price Filtering
Filter listings by price range. Use the
minPrice and maxPrice params in listListings.Attribute Filtering
Attribute Filtering
Filter by NFT traits (rarity, type). See Metadata Search.
Activity Feed
Activity Feed
Show recent sales using the Indexer’s activities endpoint.
On-Ramping
On-Ramping
Let players buy IMX with fiat. Available in Unity/Unreal via the Marketplace package.
Sample Projects
Explore complete marketplace implementations:Unity Sample Game
Orderbook integration on
main branchUnreal Sample Game
Full marketplace with C++ and Blueprint examples