Documentation Index Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
Use this file to discover all available pages before exploring further.
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
Bulk Listing Creation
Create multiple NFT listings with a single signature, improving seller experience.
Limits and Constraints
Constraint Value Notes Maximum listings 20 per transaction Batch into multiple transactions if needed Wallet type EOA: 1 signature Smart contract: N signatures Passport wallets need multiple confirmations Approval Per collection One-time approval per NFT collection
How It Works
The prepareBulkListings() call returns:
Actions - Approval transactions (if needed) + signable message
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 { Orderbook } from '@imtbl/orderbook' ;
import { Environment } from '@imtbl/config' ;
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
Constraint Value Notes Maximum orders 50 per transaction Batch into multiple if needed Currency consistency All orders same currency Enforce on frontend Best-effort fulfillment Enabled by default Fills 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:
fulfillableOrders - Can be executed
unfulfillableOrders - Cannot be executed (with reasons)
sufficientBalance - Whether user can afford fulfillable orders
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 ),
0 n
);
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 );
}
Strategy 3: Hybrid (Recommended)
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 ),
0 n
);
// 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 currency You 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)
}
Full implementation with all strategies:
import { Orderbook } from '@imtbl/orderbook' ;
import { Environment } from '@imtbl/config' ;
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 ),
0 n
);
const marketplaceFee = ( totalValue * 100 n ) / 10000 n ;
// 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 ),
0 n
);
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 ),
0 n
);
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
Batch wisely: Keep under 20 listings per batch
Handle approvals: Cache approval status per collection
Smart contract wallets: Warn users about multiple signatures
Error handling: Gracefully handle partial failures
Status updates: Poll for PENDING → ACTIVE transitions
For Bulk Fulfillment
Currency enforcement: Validate same currency on frontend
Real-time availability: Refresh cart items before checkout
Balance checks: Verify sufficient funds before transaction
User communication: Clearly explain unavailable items
Expiration warning: Show 3-minute countdown timer
Retry logic: Handle race conditions gracefully
Gas estimation: Show estimated gas cost for transparency
Next Steps
Create Listings Single listing creation guide
Order Management Query bulk-created orders
Fill Orders Single order fulfillment guide
Cancel Orders Soft and hard cancellation
Fees Understanding marketplace fees