> ## 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.

# Fill Orders

> Complete guide to fulfilling NFT orders (buying) using the Immutable Orderbook SDK

Learn how to fulfill (buy) NFT listings on the Immutable Orderbook. This guide covers filling both ERC-721 and ERC-1155 orders, including partial fills and fee handling.

<Info>
  See [Getting Started](/docs/products/orderbook/overview#getting-started) for prerequisites and installation.
</Info>

## Quick Overview

Fulfilling an order involves these steps:

1. **Call fulfillOrder()** - SDK prepares transaction actions and validates fees
2. **Execute approval** (if needed) - One-time ERC-20 approval for currency
3. **Execute fulfillment** - Submit the buy transaction
4. **Wait for confirmation** - Order status transitions to `FILLED`

## Understanding Actions

The `fulfillOrder()` call returns **actions** that must be executed in order:

| Action Type     | Purpose                                | When Needed                                               |
| --------------- | -------------------------------------- | --------------------------------------------------------- |
| `APPROVAL`      | Approve Seaport to spend ERC-20 tokens | First time using that currency, or insufficient allowance |
| `FULFILL_ORDER` | Execute the trade                      | Always                                                    |

```typescript theme={null}
const { actions, order, expiration } = await sdk.fulfillOrder(
  orderId,
  `takerAddress`,
  `takerFees` // optional
);

console.log({
  actions: actions,         // Actions to execute
  order: order,             // Order with CURRENT fees (may differ from query)
  expiration: expiration,   // Transaction expires in 3 minutes
});
```

<Warning>
  **3-Minute Expiration:** Transaction data expires **3 minutes** after generation. If the user doesn't submit within this window, you must call `fulfillOrder()` again to get fresh data with re-validated fees.
</Warning>

<Info>
  **Fee Validation:** The `order` object returned contains the **most current fees**, which may differ from when you queried the listing (e.g., protocol fee reductions during promotions). Always display the fees from this response to users before they sign.
</Info>

## Filling an ERC-721 Order

ERC-721 orders are all-or-nothing—you must purchase the entire NFT.

### Basic Example

```typescript theme={null}
import { orderbook } from '@imtbl/sdk';
import { ethers } from 'ethers';

async function buyNFT(orderId: string) {
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  const buyerAddress = await signer.getAddress();

  // 1. Prepare fulfillment
  const { actions, order, expiration } = await sdk.fulfillOrder(
    orderId,
    buyerAddress,
    [] // no taker fees for this example
  );

  console.log('Order expires:', new Date(expiration.toString()));
  console.log('Total cost:', order.buy.amount); // Cost in wei

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

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

      // Wait for confirmation
      const receipt = await txResponse.wait();
      console.log(`Confirmed in block ${receipt.blockNumber}`);
    }
  }

  console.log('NFT purchased successfully!');
}
```

### With Taker Fees

Marketplaces can charge taker fees when facilitating purchases:

```typescript theme={null}
const { actions } = await sdk.fulfillOrder(
  orderId,
  buyerAddress,
  [
    {
      recipientAddress: '0x...', // Marketplace wallet
      amount: '10000000000000000', // 0.01 IMX flat fee
    },
  ]
);
```

<Tip>
  **Taker Fee Flexibility:** Taker fees can be different for each fulfillment. Unlike maker fees (set at listing creation), marketplaces can set custom taker fees per transaction.
</Tip>

## Filling collection bids, trait bids, and metadata bids

**Collection bids**, **trait bids**, and **metadata bids** are criteria-style buy orders: the seller (taker) must tell the orderbook **which `tokenId`** they are selling into the bid. In the Orderbook SDK, pass **`tokenId` as the fifth argument** to `fulfillOrder` (after `orderId`, `takerAddress`, `takerFees`, and `amountToFill`). For a typical ERC-721 fill, set **`amountToFill`** to `undefined` and supply **`tokenId`** as a string.

```typescript theme={null}
const { actions } = await sdk.fulfillOrder(
  criteriaOrderId,
  sellerAddress,
  [], // taker fees
  undefined, // amountToFill — omit for standard ERC-721 criteria fills
  '123', // tokenId — required for collection / trait bids
);
```

* **Collection bid:** any token from the collection can be used (subject to order rules). See [Collection bids](/docs/products/orderbook/collection-bids#accepting-collection-bids-sellers).
* **Trait bid:** the same **`tokenId`** requirement applies, and the token’s **indexed metadata must match** every trait filter on the bid. If metadata is missing or does not match, fulfillment will fail—treat as a validation error and choose another asset or refresh metadata. See [Trait bids](/docs/products/orderbook/trait-bids#filling-a-trait-bid-seller).
* **Metadata bid:** the token’s **`metadata_id`** in the indexer must match the bid’s `metadataId`. See [Metadata bids](/docs/products/orderbook/metadata-bids#filling-a-metadata-bid-seller).

## Filling an ERC-1155 Order (Partial Fills)

ERC-1155 orders support **partial fills**—buy any quantity up to the available amount.

### Full Fill

Buy all available items:

```typescript theme={null}
const { actions } = await sdk.fulfillOrder(
  orderId,
  buyerAddress,
  [] // no taker fees
  // amountToFill not specified = full fill
);
```

### Partial Fill

Buy only some of the available items:

```typescript theme={null}
// Listing has 10 items available, buy only 3
const { actions } = await sdk.fulfillOrder(
  orderId,
  buyerAddress,
  [], // taker fees
  {
    amountToFill: '3', // Buy 3 of 10 items
  }
);
```

### Best-Effort Fulfillment

If you request more items than available, the orderbook attempts a "best-effort" fill:

```typescript theme={null}
// Listing has 5 items available, but request 10
const { actions } = await sdk.fulfillOrder(
  orderId,
  buyerAddress,
  [],
  {
    amountToFill: '10', // Request 10 items
    // Will fill only 5 (max available)
  }
);
```

<Info>
  **Best Effort:** If `amountToFill` exceeds available quantity, the orderbook fills up to the maximum available. No error is thrown—you get what's available.
</Info>

## ERC-1155 Taker Fee Rules

For ERC-1155 orders, taker fees have special rules:

<Warning>
  **Critical: Taker Fee for Full Order**

  The taker fee `amount` must always reflect the **COMPLETE** order, even for partial fills. The orderbook automatically pro-rates the fee based on quantity executed.

  **Example:**

  ```typescript theme={null}
  // Listing: 10 items at 1 IMX each = 10 IMX total
  // Marketplace wants 1% fee

  // ❌ WRONG for partial fill (3 items):
  takerFees: [{
    amount: '30000000000000000', // 0.03 IMX (scaled for 3 items) - INCORRECT
  }]

  // CORRECT for any fill (3, 5, or 10 items):
  takerFees: [{
    amount: '100000000000000000', // 0.1 IMX (1% of 10 IMX total) - CORRECT
  }]
  // Orderbook will charge: 0.03 IMX for 3 items, 0.05 IMX for 5 items, etc.
  ```
</Warning>

### Complete ERC-1155 Example

```typescript theme={null}
async function buyPartialERC1155(orderId: string, quantityToBuy: string) {
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  const buyerAddress = await signer.getAddress();

  // Get order details first to calculate fee
  const { result: order } = await sdk.getListing(orderId);

  // Calculate 1% marketplace fee on FULL order value
  const fullOrderValue = BigInt(order.buy.amount);
  const marketplaceFee = (fullOrderValue * 100n) / 10000n; // 1% in basis points

  // Prepare fulfillment
  const { actions, order: validatedOrder, expiration } = await sdk.fulfillOrder(
    orderId,
    buyerAddress,
    [
      {
        recipientAddress: MARKETPLACE_WALLET,
        amount: marketplaceFee.toString(), // Full order fee
      },
    ],
    {
      amountToFill: quantityToBuy, // Partial quantity
    }
  );

  // Show user what they're buying
  const itemsAvailable = order.sell.amount;
  const itemsToBuy = Math.min(parseInt(quantityToBuy), parseInt(itemsAvailable));
  const pricePerItem = fullOrderValue / BigInt(itemsAvailable);
  const totalCost = pricePerItem * BigInt(itemsToBuy);

  console.log({
    itemsToBuy: itemsToBuy,
    pricePerItem: ethers.utils.formatEther(pricePerItem),
    totalCost: ethers.utils.formatEther(totalCost),
    expiresAt: new Date(expiration.toString()),
  });

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

  console.log(`Purchased ${itemsToBuy} items!`);
}
```

## Approval Handling

For ERC-20 currency listings, buyers must approve Seaport to spend tokens:

```typescript theme={null}
const { actions } = await sdk.fulfillOrder(orderId, buyerAddress, []);

for (const action of actions) {
  if (action.type === orderbook.ActionType.TRANSACTION) {
    if (action.purpose === orderbook.TransactionPurpose.APPROVAL) {
      console.log('Requesting ERC-20 approval...');
      // User approves Seaport to spend their ERC-20 tokens
    } else if (action.purpose === orderbook.TransactionPurpose.FULFILL_ORDER) {
      console.log('Executing purchase...');
      // The actual buy transaction
    }

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

<Info>
  **One-Time Approval:** Users only need to approve each ERC-20 currency once (or when allowance is insufficient). Subsequent purchases with the same currency skip the approval step.

  **Native Currency:** Listings priced in NATIVE (IMX) never require approval—only a fulfillment transaction.

  For architectural context, see the [Approval Pattern](/docs/products/orderbook/overview#approval-pattern) documentation.
</Info>

## Fill Status

After fulfillment, check the fill status to see how much has been filled:

```typescript theme={null}
const { result: order } = await sdk.getListing(orderId);

const fillStatus = order.fill_status;
console.log({
  status: fillStatus.name,              // UNFILLED, PARTIAL, or FILLED
  numerator: fillStatus.numerator,      // Amount filled
  denominator: fillStatus.denominator,  // Total amount
  percentage: (parseInt(fillStatus.numerator) / parseInt(fillStatus.denominator)) * 100,
});

// Check if completely filled
if (fillStatus.numerator === fillStatus.denominator) {
  console.log('Order completely filled!');
} else {
  // Calculate remaining quantity (useful for ERC-1155)
  const remaining = BigInt(fillStatus.denominator) - BigInt(fillStatus.numerator);
  console.log(`Order partially filled, ${remaining} items still available`);
}
```

| Fill Status      | ERC-721 | ERC-1155 Example      |
| ---------------- | ------- | --------------------- |
| Unfilled         | `0/0`   | `0/10`                |
| Partially filled | N/A     | `3/10` (30% filled)   |
| Fully filled     | `1/1`   | `10/10` (100% filled) |

<Tip>
  **Status Transitions:** Orders transition from `ACTIVE` → `FILLED` asynchronously after the transaction confirms. For partial ERC-1155 fills, status remains `ACTIVE` until completely filled.
</Tip>

## Complete Purchase Flow

Full example with error handling and user feedback:

```typescript theme={null}
import { Orderbook } from '@imtbl/orderbook';
import { Environment } from '@imtbl/config';
import { ethers } from 'ethers';

async function completePurchaseFlow(orderId: string, quantity?: string) {
  <SdkInit />

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

  try {
    // 1. Get order details
    const { result: order } = await sdk.getListing(orderId);

    console.log('Order Details:', {
      seller: order.account_address,
      nft: `${order.sell.contract_address}:${order.sell.token_id}`,
      price: ethers.utils.formatEther(order.buy.amount),
      currency: order.buy.type,
      available: order.sell.amount || '1',
    });

    // 2. Calculate marketplace fee (1%)
    const marketplaceFee = {
      recipientAddress: MARKETPLACE_FEE_WALLET,
      amount: (BigInt(order.buy.amount) * 100n / 10000n).toString(),
    };

    // 3. Prepare fulfillment
    const fulfillmentParams: any = {
      orderId,
      takerAddress: buyerAddress,
      takerFees: [marketplaceFee],
    };

    if (quantity && order.sell.type === 'ERC1155') {
      fulfillmentParams.amountToFill = quantity;
    }

    const { actions, order: validatedOrder, expiration } =
      await sdk.fulfillOrder(...Object.values(fulfillmentParams));

    // 4. Warn user about expiration
    const expirationDate = new Date(expiration.toString());
    const timeLeft = expirationDate.getTime() - Date.now();

    if (timeLeft < 60000) {
      console.warn('⚠️ Transaction expires in less than 1 minute!');
    }

    // 5. Display final costs to user
    console.log('Final Costs:', {
      itemCost: ethers.utils.formatEther(validatedOrder.buy.amount),
      protocolFee: ethers.utils.formatEther(
        validatedOrder.fees.find(f => f.type === 'PROTOCOL')?.amount || '0'
      ),
      royalty: ethers.utils.formatEther(
        validatedOrder.fees.find(f => f.type === 'ROYALTY')?.amount || '0'
      ),
      marketplaceFee: ethers.utils.formatEther(marketplaceFee.amount),
    });

    // 6. Execute all actions
    for (let i = 0; i < actions.length; i++) {
      const action = actions[i];

      if (action.type === orderbook.ActionType.TRANSACTION) {
        console.log(`Step ${i + 1}/${actions.length}:`, 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}`);
      }
    }

    // 7. Poll for status update
    console.log('Waiting for order status to update...');
    let attempts = 0;
    while (attempts < 10) {
      await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s

      const { result: updatedOrder } = await sdk.getListing(orderId);

      if (updatedOrder.status.name === 'FILLED' ||
          updatedOrder.fill_status.numerator !== '0') {
        console.log('Purchase confirmed!', {
          filled: `${updatedOrder.fill_status.numerator}/${updatedOrder.fill_status.denominator}`,
          status: updatedOrder.status.name,
        });
        break;
      }

      attempts++;
    }

    return { success: true, orderId };

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

    // Handle common errors
    if (error.message.includes('insufficient funds')) {
      console.error('❌ Buyer has insufficient balance');
    } else if (error.message.includes('FILLED')) {
      console.error('❌ Order already filled');
    } else if (error.message.includes('expired')) {
      console.error('❌ Transaction expired, please retry');
    }

    throw error;
  }
}
```

## Error Handling

Common errors during fulfillment:

| Error                    | Cause                      | Solution                         |
| ------------------------ | -------------------------- | -------------------------------- |
| `INSUFFICIENT_FUNDS`     | Buyer lacks currency       | Show balance, request funding    |
| `ORDER_FILLED`           | Order already purchased    | Refresh listing, show "Sold Out" |
| `ORDER_EXPIRED`          | Listing expired            | Remove from UI                   |
| `TRANSACTION_EXPIRED`    | Took longer than 3 minutes | Call `fulfillOrder()` again      |
| `APPROVAL_FAILED`        | User rejected approval     | Retry approval step              |
| `INSUFFICIENT_ALLOWANCE` | Approval amount too low    | Request higher allowance         |

## Checking Token Balance

Before fulfilling orders, validate that buyers have sufficient funds:

Use ethers.js to check balance:

```typescript theme={null}
// Check native currency (IMX) balance
const provider = new ethers.providers.Web3Provider(window.ethereum);
const balance = await provider.getBalance(buyerAddress);
console.log('Balance:', ethers.utils.formatEther(balance), 'IMX');

// Check ERC-20 token balance
const tokenContract = new ethers.Contract(
  ERC20_ADDRESS,
  ['function balanceOf(address) view returns (uint256)'],
  provider
);
const tokenBalance = await tokenContract.balanceOf(buyerAddress);
```

## Best Practices

1. **Show Expiration Timer:** Display 3-minute countdown to user
2. **Validate Balance:** Check buyer has sufficient funds before calling `fulfillOrder()` (see above)
3. **Display All Fees:** Show protocol fee, royalty, maker fee, taker fee separately
4. **Handle Partial Fills:** For ERC-1155, show available quantity and let users choose amount
5. **Optimistic UI:** Show "Purchase Pending" immediately, update to "Purchased" after confirmation
6. **Refresh Listings:** Poll or use webhooks to detect when orders are filled by others

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Listings" icon="tag" href="/docs/products/orderbook/create-listings">
    Learn how to create NFT listings
  </Card>

  <Card title="Order Management" icon="list-check" href="/docs/products/orderbook/order-management">
    Query order status after purchase
  </Card>

  <Card title="Bulk Operations" icon="layer-group" href="/docs/products/orderbook/bulk-operations">
    Fill multiple orders in one transaction (shopping cart)
  </Card>

  <Card title="Cancel Orders" icon="xmark" href="/docs/products/orderbook/cancel-orders">
    Cancel listings with soft or hard cancels
  </Card>

  <Card title="Example App" icon="code" href="https://github.com/immutable/ts-immutable-sdk/tree/main/examples/orderbook/fulfill-listing-with-nextjs">
    View complete Next.js example on GitHub
  </Card>
</CardGroup>
