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.
| Schema | When |
|---|---|
PRODUCT_V1 | Physical or simple digital products |
BUNDLE | Multi-item bundles (e.g. license + shirt) |
BASE | Minimum 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 directly — see Reference → Metadata 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://. Avoidhttp://(some viewers refuse).
Common footguns
validate_metadataerrors are not always actionable. A schema mismatch deep in a nested field can surface as a generic "required" at a higher path. Always checkerrors[*].pathprecisely.- 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].urlpoints to a Twitter image, it can rot. Use CDN URLs oripfs://references.
Where to look in code
- Reference → Metadata schemas
- Reference → MCP tools →
validate_metadata,store_product_v1_metadata,store_bundle_metadata,store_base_metadata,render_contractual_agreement - Reference → Core SDK → Offers