Skip to main content
Learn how to batch multiple orderbook operations into single transactions, improving user experience and reducing gas costs. Essential for marketplace shopping carts and bulk listing management.

Overview

Bulk operations enable users to:
  • Bulk Listings: Create up to 20 listings with one signature
  • Bulk Fulfillment: Buy up to 50 orders in one transaction (shopping cart)
Benefits:
  • Fewer wallet confirmations (better UX)
  • Lower gas costs (batched transactions)
  • Faster marketplace operations
  • Enable shopping cart functionality
See Getting Started for prerequisites and installation. For bulk operations, you should also understand creating individual listings and filling individual orders.

Bulk Listing Creation

Create multiple NFT listings with a single signature, improving seller experience.

Limits and Constraints

ConstraintValueNotes
Maximum listings20 per transactionBatch into multiple transactions if needed
Wallet typeEOA: 1 signature
Smart contract: N signatures
Passport wallets need multiple confirmations
ApprovalPer collectionOne-time approval per NFT collection

How It Works

The prepareBulkListings() call returns:
  1. Actions - Approval transactions (if needed) + signable message
  2. completeListings() method - Scoped method to submit signatures
const { actions, completeListings } = await sdk.prepareBulkListings({
  makerAddress: `userAddress`,
  listingParams: [...], // Array of listing configs
});

Basic Example

Create multiple ERC-721 listings at once:
import { config, orderbook } from '@imtbl/sdk';
import { ethers } from 'ethers';

async function createBulkListings() {
  <SdkInit />

  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  const `userAddress` = await signer.getAddress();

  // Define multiple listings
  const `listingParams` = [
    {
      sell: {
        type: 'ERC721',
        contractAddress: '0x123...',
        tokenId: '1',
      },
      buy: {
        type: 'NATIVE',
        amount: '1000000000000000000', // 1 IMX
      },
      makerFees: [{
        `recipientAddress`: MARKETPLACE_WALLET,
        amount: '10000000000000000', // 0.01 IMX
      }],
    },
    {
      sell: {
        type: 'ERC721',
        contractAddress: '0x123...',
        tokenId: '2',
      },
      buy: {
        type: 'NATIVE',
        amount: '2000000000000000000', // 2 IMX
      },
      makerFees: [{
        `recipientAddress`: MARKETPLACE_WALLET,
        amount: '20000000000000000', // 0.02 IMX
      }],
    },
    // ... up to 20 listings total
  ];

  // 1. Prepare bulk listings
  const { actions, completeListings } = await sdk.prepareBulkListings({
    makerAddress: `userAddress`,
    `listingParams`,
  });

  // 2. Handle approval transactions (one per collection)
  for (const action of actions) {
    if (action.type === orderbook.ActionType.TRANSACTION) {
      console.log('Requesting approval for collection...');
      const unsignedTx = await action.buildTransaction();
      const txResponse = await signer.sendTransaction(unsignedTx);
      await txResponse.wait();
      console.log('Approved!');
    }
  }

  // 3. Sign the bulk listing message
  const `signableAction` = actions.find(
    (action) => action.type === orderbook.ActionType.SIGNABLE
  );

  if (`signableAction`) {
    const signature = await signer._signTypedData(
      signableAction.message.domain,
      signableAction.message.types,
      signableAction.message.value
    );

    // 4. Submit all listings with the signature
    const results = await completeListings(signature);

    console.log('Created listings:', {
      successful: results.result.filter(r => r.success).length,
      failed: results.result.filter(r => !r.success).length,
    });

    return results;
  }
}

Mixed Token Types

Create listings for both ERC-721 and ERC-1155 tokens:
const `listingParams` = [
  // ERC-721 listing
  {
    sell: {
      type: 'ERC721',
      contractAddress: '0xAAA...',
      tokenId: '123',
    },
    buy: {
      type: 'NATIVE',
      amount: '1000000000000000000',
    },
    makerFees: [],
  },
  // ERC-1155 listing
  {
    sell: {
      type: 'ERC1155',
      contractAddress: '0xBBB...',
      tokenId: '456',
      amount: '10', // Selling 10 copies
    },
    buy: {
      type: 'NATIVE',
      amount: '5000000000000000000', // 5 IMX total
    },
    makerFees: [],
  },
];

Smart Contract Wallets (Passport)

Passport & Smart Contract WalletsWhen using smart contract wallets like Passport, users must sign separate confirmations for each listing. Unlike EOA wallets (MetaMask) which can sign once for all 20 listings, smart contract wallets require individual signatures.This is due to smart contract wallet architecture and cannot be batched.

Response Structure

The completeListings() method returns success/failure for each listing:
const results = await completeListings(signature);

console.log(results.result);
// [
//   { success: true, order: {...}, `order_hash`: "0x..." },
//   { success: true, order: {...}, `order_hash`: "0x..." },
//   { success: false, reason: "INSUFFICIENT_BALANCE" },
// ]
Handle partial failures gracefully:
const successful = results.result.filter(r => r.success);
const failed = results.result.filter(r => !r.success);

console.log(`Created ${successful.length} listings`);

if (failed.length > 0) {
  console.warn(`❌ Failed to create ${failed.length} listings:`);
  failed.forEach((f, i) => {
    console.log(`- Listing ${i}: ${f.reason}`);
  });
}

Bulk Order Fulfillment (Shopping Cart)

Buy multiple NFTs in a single transaction—the foundation of marketplace shopping carts.

Limits and Constraints

ConstraintValueNotes
Maximum orders50 per transactionBatch into multiple if needed
Currency consistencyAll orders same currencyEnforce on frontend
Best-effort fulfillmentEnabled by defaultFills available orders even if some fail

How It Works

Bulk fulfillment handles common shopping cart scenarios:
  • Some orders already filled by others
  • Some orders cancelled
  • Insufficient balance for all items
  • Orders with mixed availability
The SDK provides:
  1. fulfillableOrders - Can be executed
  2. unfulfillableOrders - Cannot be executed (with reasons)
  3. sufficientBalance - Whether user can afford fulfillable orders
  4. Actions - Transactions to execute

Basic Example

Simple shopping cart checkout:
async function checkoutCart(orderIds: string[]) {
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  const buyerAddress = await signer.getAddress();

  // Prepare bulk fulfillment
  const { result } = await sdk.fulfillBulkOrders({
    orderIds,
    takerAddress: buyerAddress,
    takerFees: [
      {
        recipientAddress: MARKETPLACE_WALLET,
        amount: '50000000000000000', // 0.05 IMX marketplace fee
      },
    ],
  });

  console.log('Cart Analysis:', {
    fulfillable: result.fulfillableOrders.length,
    unfulfillable: result.unfulfillableOrders.length,
    hasSufficientBalance: result.sufficientBalance,
  });

  // Check if user can afford fulfillable orders
  if (!result.sufficientBalance) {
    const totalCost = result.fulfillableOrders.reduce(
      (sum, order) => sum + BigInt(order.buy.amount),
      0n
    );
    console.error(`Insufficient balance. Need: ${ethers.utils.formatEther(totalCost)} IMX`);
    return;
  }

  // Execute all actions
  for (const action of result.actions) {
    if (action.type === orderbook.ActionType.TRANSACTION) {
      const unsignedTx = await action.buildTransaction();
      const txResponse = await signer.sendTransaction(unsignedTx);

      console.log(`Transaction sent: ${txResponse.hash}`);
      await txResponse.wait();
    }
  }

  console.log('Purchased', result.fulfillableOrders.length, 'NFTs!');
}

Response Structure

The response categorizes orders:
{
  result: {
    fulfillableOrders: [
      {
        order_id: "order-1",
        buy: { type: "NATIVE", amount: "1000000000000000000" },
        sell: { type: "ERC721", contract_address: "0x...", token_id: "1" }
      },
      {
        order_id: "order-2",
        buy: { type: "NATIVE", amount: "2000000000000000000" },
        sell: { type: "ERC721", contract_address: "0x...", token_id: "2" }
      }
    ],
    unfulfillableOrders: [
      {
        order_id: "order-3",
        reason: "FILLED" // Already sold
      },
      {
        order_id: "order-4",
        reason: "CANCELLED" // Seller cancelled
      }
    ],
    sufficientBalance: true,
    actions: [
      { type: "TRANSACTION", purpose: "APPROVAL" },
      { type: "TRANSACTION", purpose: "FULFILL_ORDER" }
    ],
    expiration: "2024-01-15T10:30:00Z"
  }
}

UX Strategies for Unavailable Items

When some cart items become unavailable, choose a user experience strategy:

Strategy 1: All or Nothing

Require users to fix cart before checkout:
const { result } = await sdk.fulfillBulkOrders({
  orderIds: cartOrderIds,
  takerAddress: buyerAddress,
  takerFees: [],
});

if (result.unfulfillableOrders.length > 0) {
  // Show user which items are unavailable
  console.error('Some items are no longer available:');
  result.unfulfillableOrders.forEach(order => {
    console.log(`- Order ${order.order_id}: ${order.reason}`);
  });

  throw new Error('Please remove unavailable items from cart');
}

// All items available, proceed with checkout
executeActions(result.actions);

Strategy 2: Best Effort

Automatically checkout available items without user intervention:
const { result } = await sdk.fulfillBulkOrders({
  orderIds: cartOrderIds,
  takerAddress: buyerAddress,
  takerFees: [],
});

// Show summary
console.log(`Purchasing ${result.fulfillableOrders.length} of ${cartOrderIds.length} items`);

if (result.unfulfillableOrders.length > 0) {
  console.log('The following items were removed from your cart:');
  result.unfulfillableOrders.forEach(order => {
    console.log(`- ${order.reason}`);
  });
}

// Proceed with available items
if (result.fulfillableOrders.length > 0) {
  await executeActions(result.actions);
}
Ask user to confirm before proceeding with partial cart:
const { result } = await sdk.fulfillBulkOrders({
  orderIds: cartOrderIds,
  takerAddress: buyerAddress,
  takerFees: [],
});

if (result.unfulfillableOrders.length > 0) {
  const message = `
    ${result.unfulfillableOrders.length} items are no longer available.
    Would you like to purchase the remaining ${result.fulfillableOrders.length} items?
  `;

  const shouldProceed = confirm(message);

  if (!shouldProceed) {
    console.log('Purchase cancelled by user');
    return;
  }
}

// User confirmed or all items available
await executeActions(result.actions);

Insufficient Balance Handling

When user can’t afford fulfillable items:
const { result } = await sdk.fulfillBulkOrders({
  orderIds: cartOrderIds,
  takerAddress: buyerAddress,
  takerFees: [],
});

if (!result.sufficientBalance) {
  // Calculate how much they need
  const totalCost = result.fulfillableOrders.reduce(
    (sum, order) => sum + BigInt(order.buy.amount),
    0n
  );

  // Get user's current balance
  const balance = await provider.getBalance(buyerAddress);

  const shortfall = totalCost - balance;

  console.error(`Insufficient Balance:
    Need: ${ethers.utils.formatEther(totalCost)} IMX
    Have: ${ethers.utils.formatEther(balance)} IMX
    Short: ${ethers.utils.formatEther(shortfall)} IMX
  `);

  // Optionally: suggest removing most expensive items
  const sortedByPrice = result.fulfillableOrders
    .sort((a, b) => BigInt(b.buy.amount) - BigInt(a.buy.amount));

  console.log('Consider removing:', sortedByPrice[0].order_id);
  return;
}

// Has sufficient balance, proceed

Currency Consistency Requirement

All orders must use the same currencyYou cannot mix NATIVE and ERC-20 orders in a single bulk fulfillment. Enforce this on your frontend:
function validateCartCurrency(orders: Order[]): boolean {
  if (orders.length === 0) return true;

  const firstCurrency = orders[0].buy.type;
  const firstContract = orders[0].buy.contract_address;

  return orders.every(order => {
    if (order.buy.type !== firstCurrency) return false;
    if (firstCurrency === 'ERC20' && order.buy.contract_address !== firstContract) {
      return false;
    }
    return true;
  });
}

// Before checkout
if (!validateCartCurrency(cartItems)) {
  throw new Error('All items must be priced in the same currency');
}

Partial Fills with Bulk Orders

Specify amountToFill for ERC-1155 partial fills:
const { result } = await sdk.fulfillBulkOrders({
  orderIds: ['order-1', 'order-2'],
  takerAddress: buyerAddress,
  takerFees: [],
  amountToFill: {
    'order-1': '5', // Buy 5 of available ERC-1155 items
    'order-2': undefined, // Full fill
  },
});
If requesting more than available, best-effort fills up to max:
// Order has 5 items available
amountToFill: {
  'order-1': '10', // Request 10
  // Will fill only 5 (max available)
}

Complete Shopping Cart Example

Full implementation with all strategies:
import { config, orderbook } from '@imtbl/sdk';
import { ethers } from 'ethers';

interface CartItem {
  orderId: string;
  nftName: string;
  price: string;
  currency: string;
}

async function checkoutShoppingCart(cart: CartItem[]) {
  <SdkInit />

  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  const buyerAddress = await signer.getAddress();

  try {
    // 1. Validate cart
    if (cart.length === 0) {
      throw new Error('Cart is empty');
    }

    if (cart.length > 50) {
      throw new Error('Maximum 50 items per checkout');
    }

    // 2. Calculate marketplace fee (1% of total)
    const totalValue = cart.reduce(
      (sum, item) => sum + BigInt(item.price),
      0n
    );
    const marketplaceFee = (totalValue * 100n) / 10000n;

    // 3. Prepare bulk fulfillment
    console.log('Analyzing cart...');

    const { result } = await sdk.fulfillBulkOrders({
      orderIds: cart.map(item => item.orderId),
      takerAddress: buyerAddress,
      takerFees: [{
        recipientAddress: MARKETPLACE_WALLET,
        amount: marketplaceFee.toString(),
      }],
    });

    // 4. Handle unavailable items
    if (result.unfulfillableOrders.length > 0) {
      console.warn(`⚠️ ${result.unfulfillableOrders.length} items unavailable:`);

      result.unfulfillableOrders.forEach(order => {
        const item = cart.find(c => c.orderId === order.order_id);
        console.log(`- ${item?.nftName}: ${order.reason}`);
      });

      // Ask user to confirm
      const shouldProceed = confirm(
        `Proceed with ${result.fulfillableOrders.length} available items?`
      );

      if (!shouldProceed) {
        console.log('Checkout cancelled');
        return;
      }
    }

    // 5. Check balance
    if (!result.sufficientBalance) {
      const fulfillableCost = result.fulfillableOrders.reduce(
        (sum, order) => sum + BigInt(order.buy.amount),
        0n
      );

      const balance = await provider.getBalance(buyerAddress);

      alert(`Insufficient balance
        Need: ${ethers.utils.formatEther(fulfillableCost)} IMX
        Have: ${ethers.utils.formatEther(balance)} IMX
      `);

      return;
    }

    // 6. Show final summary
    const fulfillableCost = result.fulfillableOrders.reduce(
      (sum, order) => sum + BigInt(order.buy.amount),
      0n
    );

    console.log('Final Checkout:', {
      items: result.fulfillableOrders.length,
      subtotal: ethers.utils.formatEther(fulfillableCost),
      marketplaceFee: ethers.utils.formatEther(marketplaceFee),
      total: ethers.utils.formatEther(fulfillableCost + marketplaceFee),
      expires: new Date(result.expiration),
    });

    // 7. Execute all actions
    console.log('Processing payment...');

    for (const action of result.actions) {
      if (action.type === orderbook.ActionType.TRANSACTION) {
        console.log(`Executing: ${action.purpose}`);

        const unsignedTx = await action.buildTransaction();
        const txResponse = await signer.sendTransaction(unsignedTx);

        console.log(`Transaction sent: ${txResponse.hash}`);
        const receipt = await txResponse.wait();
        console.log(`Confirmed in block ${receipt.blockNumber}`);
      }
    }

    console.log('Purchase complete!');

    return {
      purchased: result.fulfillableOrders.length,
      unavailable: result.unfulfillableOrders.length,
      transactionHash: result.actions[result.actions.length - 1].transactionHash,
    };

  } catch (error: any) {
    console.error('Checkout failed:', error.message);
    throw error;
  }
}

Best Practices

For Bulk Listings

  1. Batch wisely: Keep under 20 listings per batch
  2. Handle approvals: Cache approval status per collection
  3. Smart contract wallets: Warn users about multiple signatures
  4. Error handling: Gracefully handle partial failures
  5. Status updates: Poll for PENDING → ACTIVE transitions

For Bulk Fulfillment

  1. Currency enforcement: Validate same currency on frontend
  2. Real-time availability: Refresh cart items before checkout
  3. Balance checks: Verify sufficient funds before transaction
  4. User communication: Clearly explain unavailable items
  5. Expiration warning: Show 3-minute countdown timer
  6. Retry logic: Handle race conditions gracefully
  7. Gas estimation: Show estimated gas cost for transparency

Next Steps