Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Publish an offer · Boson Protocol
Skip to content

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

FieldMeaning
validFromDateInMSBuyers can commit starting here
validUntilDateInMSBuyers can commit until here
voucherRedeemableFromDateInMSVouchers redeemable starting here
voucherRedeemableUntilDateInMS0 = use voucherValidDurationInMS
voucherValidDurationInMSIf Until is 0, vouchers expire this many ms after commit
disputePeriodDurationInMSHow long after redemption the buyer can raise a dispute
resolutionPeriodDurationInMSHow 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

  • feeLimit is the maximum fee you'll accept, in token base units. Pass MaxUint256 to accept any protocol fee, or a concrete max value to bail if the protocol bumps fees out from under you. Do not pass 0 — that means "max fee = 0" and reverts with TotalFeeExceedsLimit.
  • disputeResolverId must be registered and willing to handle your offer's exchangeToken. Look up active DRs and their supported tokens via sdk.getDisputeResolvers or the subgraph before hard-coding an id. Each chain × env has a different DR id space.
  • agentId = 0 means "no protocol agent". Set it only if you have a registered agent.
  • The offer is immutable. No update; only void and re-create.
  • metadataHash is the IPFS CID (without the ipfs:// prefix), not a separate keccak hash.

Next