Appearance
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 refundedPaying 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
| Error | What actually happened | Suggested message |
|---|---|---|
OrderExpired | Past order.expiry | "This listing has expired." |
NonceUsedOrCancelled | Filled or cancelled already | "This listing was already sold or cancelled." |
NonceBelowMinimum | Seller bulk-cancelled | "The seller cancelled their listings." |
MakerNotOwner | Seller moved the token | "The seller no longer owns this item." |
MarketNotApproved | Approval revoked | "The seller revoked approval; the listing is stale." |
InvalidSignature | Bad or tampered signature | "This listing is invalid." |
IncorrectPayment | Sent less than price | "Payment amount is incorrect." |
NativeNotAccepted | Sent ETH on an ERC-20 order | Internal bug — fix the caller. |
CollectionNotAllowed | Collection removed | "This collection is not tradable." |
CurrencyNotAllowed | Currency not allow-listed | "This payment method is unavailable." |
EnforcedPause | Market 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) / 100nAfter 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.