Publish an offer
Publishing an offer has three steps: build the metadata, pin it to IPFS, and call createOffer.
For the metadata pipeline in detail, see The metadata pipeline.
SDK
import { ethers } from "ethers"
// Build a PRODUCT_V1 metadata object that conforms to productV1MetadataSchema.
// See Reference → Metadata schemas for the complete shape (required fields:
// schemaUrl, type, uuid, name, description, externalUrl, licenseUrl, image,
// attributes (≥ 2), product, seller, shipping, exchangePolicy).
const uuid = crypto.randomUUID()
const metadata = {
schemaUrl: "https://schema.org/Product",
type: "PRODUCT_V1" as const,
uuid,
name: "Hand-thrown mug",
description: "Stoneware, 12 oz.",
externalUrl: `https://example.com/${uuid}`,
licenseUrl: `https://example.com/${uuid}/license`,
image: "https://cdn.example.com/mug.jpg",
attributes: [
{ traitType: "Material", value: "Stoneware" },
{ traitType: "Size", value: "12 oz" },
],
product: {
uuid,
version: 1,
title: "Hand-thrown mug",
description: "Stoneware, 12 oz.",
productionInformation_brandName: "Your brand",
details_offerCategory: "PHYSICAL",
visuals_images: [{ url: "https://cdn.example.com/mug.jpg", tag: "product" }],
},
seller: { defaultVersion: 1, name: "Your brand", contactLinks: [{ tag: "email", url: "mailto:hi@example.com" }] },
shipping: { returnPeriod: "14" },
exchangePolicy: { uuid: crypto.randomUUID(), version: 1, label: "fairExchangePolicy", template: "ipfs://Qm-template", sellerContactMethod: "email", disputeResolverContactMethod: "email" },
}
const metadataUri = await sdk.storeMetadata(metadata)
const tx = await sdk.createOffer({
price: "1000000", // 1 USDC
sellerDeposit: "0",
buyerCancelPenalty: "0",
quantityAvailable: "10",
exchangeToken: USDC_ADDRESS,
metadataUri,
metadataHash: metadataUri.replace(/^ipfs:\/\//, ""), // the IPFS CID is the hash
validFromDateInMS: Date.now(),
validUntilDateInMS: Date.now() + 30 * 24 * 60 * 60 * 1000,
voucherRedeemableFromDateInMS: Date.now(),
voucherRedeemableUntilDateInMS: 0,
voucherValidDurationInMS: 30 * 24 * 60 * 60 * 1000,
disputePeriodDurationInMS: 7 * 24 * 60 * 60 * 1000,
resolutionPeriodDurationInMS: 7 * 24 * 60 * 60 * 1000,
collectionIndex: "0",
disputeResolverId: "5", // your chain's DR; look up via getDisputeResolvers
agentId: "0",
feeLimit: ethers.constants.MaxUint256.toString(), // accept any fee
})
await tx.wait()What this does
- Writes the offer to the Diamond (
OfferHandlerFacet.createOffer). - Emits
OfferCreated(offerId, sellerId, offerStruct, dispoeResolverId, agentId, /* … */). - Does not mint vouchers yet. Vouchers are minted lazily as buyers commit — unless you opt in to pre-minted vouchers.
Date fields are milliseconds
All *InMS fields are milliseconds since Unix epoch. The SDK converts them to seconds when writing on-chain.
| Field | Meaning |
|---|---|
validFromDateInMS | Buyers can commit starting here |
validUntilDateInMS | Buyers can commit until here |
voucherRedeemableFromDateInMS | Vouchers redeemable starting here |
voucherRedeemableUntilDateInMS | 0 = use voucherValidDurationInMS |
voucherValidDurationInMS | If Until is 0, vouchers expire this many ms after commit |
disputePeriodDurationInMS | How long after redemption the buyer can raise a dispute |
resolutionPeriodDurationInMS | How long both parties have to mutually resolve a dispute before escalation |
Atomic create + commit (orchestration)
To create an offer and commit a buyer in the same transaction (e.g. a buyer-initiated drop), use the Orchestration mixin or the MCP create_offer_and_commit tool. See Atomic commit-and-redeem for the full flow.
Common gotchas
feeLimitis the maximum fee you'll accept, in token base units. PassMaxUint256to accept any protocol fee, or a concrete max value to bail if the protocol bumps fees out from under you. Do not pass0— that means "max fee = 0" and reverts withTotalFeeExceedsLimit.disputeResolverIdmust be registered and willing to handle your offer'sexchangeToken. Look up active DRs and their supported tokens viasdk.getDisputeResolversor the subgraph before hard-coding an id. Each chain × env has a different DR id space.agentId = 0means "no protocol agent". Set it only if you have a registered agent.- The offer is immutable. No update; only void and re-create.
metadataHashis the IPFS CID (without theipfs://prefix), not a separate keccak hash.