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

The metadata pipeline

Boson offers carry their off-chain detail (title, description, images, attributes, contractual agreement) as a metadata document pinned to IPFS. The Diamond stores only the IPFS URI and a content hash.

Build

Construct the metadata JSON object against the appropriate schema.

Validate

Run validate_metadata (or validateMetadata in the SDK) — Yup schema check, no return value; raises on invalid.

Pin

Upload to IPFS via storeMetadata (or any pinning service). The CID becomes your metadata URI.

Reference

Pass the ipfs://Cid… URI to createOffer. The SDK computes and stores the content hash on-chain alongside the URI.

Schemas

Three top-level schemas, defined by @bosonprotocol/metadata. The package exports the Yup schemas (productV1MetadataSchema, bundleMetadataSchema, etc.) and validateMetadata. Applications construct the metadata object literal that matches the schema — there are no high-level *Factory() helpers.

SchemaWhen
PRODUCT_V1Physical or simple digital products
BUNDLEMulti-item bundles (e.g. license + shirt)
BASEMinimum schema for opaque offers

There's also SELLER, COLLECTION, and rNFT metadata for the entities surrounding an offer.

Build → validate → pin

// Construct the PRODUCT_V1 object directlysee ReferenceMetadata schemas
// for the full required-field list (schemaUrl, type, uuid, name, description,
// externalUrl, licenseUrl, image, attributes, product, seller, shipping,
// exchangePolicy).
import { IpfsMetadataStorage } from "@bosonprotocol/ipfs-storage"
import { validateMetadata } from "@bosonprotocol/metadata"
 
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, seller, shipping, exchangePolicy
}
 
const storage = new IpfsMetadataStorage(validateMetadata, { url: process.env.IPFS_URL! })
const metadataUri = await storage.storeMetadata(metadata)
// → "ipfs://Qm…"

metadataUri is what you pass to createOffer. The SDK will compute and store the content hash on-chain alongside the URI.

Validation

validate_metadata (MCP) / validateMetadata (SDK) runs the schema check. Errors are structured per JSON-pointer:

{
  "valid": false,
  "errors": [
    { "path": "/productV1/title", "message": "required" }
  ]
}

Validate before pinning; otherwise you'll waste an IPFS write.

Pinning

@bosonprotocol/ipfs-storage is a thin wrapper over an IPFS HTTP API. Use Infura, Pinata, or self-host. Always pin at least three places if you care about durability — IPFS does not guarantee availability.

Contractual agreement

A PRODUCT_V1 metadata can carry a contractualAgreement markdown blob. The MCP render_contractual_agreement tool resolves it server-side. See Reference → MCP tools.

Limits

  • Total metadata size: 20 MB. Most offers come in under 100 KB. Large images should live on a CDN, not in IPFS metadata.
  • Image references: HTTPS or ipfs://. Avoid http:// (some viewers refuse).

Common footguns

  • validate_metadata errors are not always actionable. A schema mismatch deep in a nested field can surface as a generic "required" at a higher path. Always check errors[*].path precisely.
  • IPFS gateway lag. The pin is live on the gateway you posted to, but propagation to other gateways takes minutes. If your frontend fetches via a different gateway than your seller posted to, you may get a 404 for the first few minutes.
  • Don't bake mutable URLs into metadata. If images[0].url points to a Twitter image, it can rot. Use CDN URLs or ipfs:// references.

Where to look in code

Next