Skip to content

Fill an order

Filling is a single call to fulfillOrder. Payment splits and the NFT transfer happen atomically — either everything succeeds or nothing does.

solidity
function fulfillOrder(Order calldata order, bytes calldata signature)
    external payable;

Simulate first

The order came from somewhere else and may be stale. A simulation costs nothing and turns an opaque revert into a named error before you ask a user to sign.

ts
const { request } = await publicClient.simulateContract({
  address: LOOT_MARKET,
  abi: marketAbi,
  functionName: "fulfillOrder",
  args: [order, signature],
  account: buyer,
  value: order.price,          // native ETH orders only
});

const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("fill reverted");

writeContract resolving is not success

It resolves when the node accepts the transaction, which says nothing about whether it succeeded. Always wait for the receipt and check status — otherwise a revert is reported to your user as a completed purchase.

Paying in native ETH

Set currency to address(0) and send msg.value >= order.price. Excess is refunded in the same transaction, so overpaying only costs the gas to move it. Underpaying reverts with IncorrectPayment(expected, provided).

ts
value: order.price          // exact is fine; more is refunded

Paying in an ERC-20

No ERC-20 currency is allow-listed today

Every fill is native ETH. The path below exists in the contract but reverts with CurrencyNotAllowed until an owner allow-lists a currency.

When one is enabled: msg.value must be zero (else NativeNotAccepted), and the buyer must approve LootMarket for the full price first — the contract pulls up to three separate transferFrom calls (royalty, platform fee, seller).

What the seller actually receives

Compute it before you fill, from the same inputs the contract uses:

ts
const [royalty, feeBps] = await Promise.all([
  publicClient.readContract({
    ...skins, functionName: "royaltyInfo", args: [order.tokenId, order.price],
  }),
  publicClient.readContract({ ...market, functionName: "platformFeeBps" }),
]);

const royaltyAmount = royalty[1];
const platformFee   = (order.price * BigInt(feeBps)) / 10_000n;
const sellerGets    = order.price - royaltyAmount - platformFee;

All bigint arithmetic — never convert wei to a JavaScript number. Full breakdown and a worked example: Fees & royalties.

Mapping reverts to something a user understands

ErrorWhat actually happenedSuggested message
OrderExpiredPast order.expiry"This listing has expired."
NonceUsedOrCancelledFilled or cancelled already"This listing was already sold or cancelled."
NonceBelowMinimumSeller bulk-cancelled"The seller cancelled their listings."
MakerNotOwnerSeller moved the token"The seller no longer owns this item."
MarketNotApprovedApproval revoked"The seller revoked approval; the listing is stale."
InvalidSignatureBad or tampered signature"This listing is invalid."
IncorrectPaymentSent less than price"Payment amount is incorrect."
NativeNotAcceptedSent ETH on an ERC-20 orderInternal bug — fix the caller.
CollectionNotAllowedCollection removed"This collection is not tradable."
CurrencyNotAllowedCurrency not allow-listed"This payment method is unavailable."
EnforcedPauseMarket paused"Trading is temporarily paused."

Full table with selectors: Errors.

Buying your own listing

The deployed contract does not reject it. It is economically pointless rather than dangerous: the price returns to you, but the royalty and platform fee do not — so you pay roughly 7% plus gas to move a token to yourself.

There is a SelfPurchase guard in the current contract source that will make this revert once the upgrade ships. Do not rely on either behaviour; if your UI can produce this situation, block it client-side.

Gas

Estimate per transaction. Nitro prices L1 calldata separately from L2 execution, so a hardcoded limit that works for one order can fail for another.

ts
const gas = await publicClient.estimateContractGas({
  address: LOOT_MARKET, abi: marketAbi, functionName: "fulfillOrder",
  args: [order, signature], account: buyer, value: order.price,
});
// apply a modest buffer, e.g. (gas * 120n) / 100n

After the fill

OrderFulfilled is emitted with royaltyAmount and platformFee broken out, and a standard ERC-721 Transfer moves the token. Both are in the same transaction — index either. See Events & indexing.

LootFi is not affiliated with, endorsed by, or sponsored by Valve Corporation. Counter-Strike and Steam are trademarks of Valve Corporation.