Build a marketplace in 15 minutes
What you'll build: a Next.js app that creates a seller, publishes an offer, lets a buyer commit, and redeems the voucher — all on Base Sepolia.
Time: 15 minutes. Stack: Next.js 15, TypeScript, ethers v5, Boson Core SDK. Network: Base Sepolia (configId: "staging-84532-0").
1. Scaffold
pnpm create next-app boson-market --typescript --app --no-tailwind --no-eslint
cd boson-market
pnpm add @bosonprotocol/core-sdk @bosonprotocol/ethers-sdk @bosonprotocol/metadata @bosonprotocol/ipfs-storage ethers@^52. Create the SDK once, reuse it
// src/lib/boson.ts
import { CoreSDK } from "@bosonprotocol/core-sdk"
import { EthersAdapter } from "@bosonprotocol/ethers-sdk"
import { IpfsMetadataStorage } from "@bosonprotocol/ipfs-storage"
import { validateMetadata } from "@bosonprotocol/metadata"
import { ethers } from "ethers"
const CONFIG_ID = "staging-84532-0" as const
const ENV_NAME = "staging" as const
export function getSdkForSigner(privateKey: string) {
const provider = new ethers.providers.JsonRpcProvider(
process.env.NEXT_PUBLIC_RPC_URL,
)
const wallet = new ethers.Wallet(privateKey, provider)
return {
sdk: CoreSDK.fromDefaultConfig({
web3Lib: new EthersAdapter(provider, wallet),
envName: ENV_NAME,
configId: CONFIG_ID,
metadataStorage: new IpfsMetadataStorage(validateMetadata, { url: process.env.IPFS_URL! }),
}),
wallet,
}
}3. Create a seller and publish an offer
// scripts/seed.ts
import { ethers } from "ethers"
import { getSdkForSigner } from "../src/lib/boson"
const { sdk, wallet } = getSdkForSigner(process.env.SELLER_PRIVATE_KEY!)
// 1. Create the on-chain seller (if you don't already have one)
const tx1 = await sdk.createSeller({
assistant: wallet.address,
admin: wallet.address,
treasury: wallet.address,
contractUri: "ipfs://Qm…",
royaltyPercentage: "0",
authTokenId: "0",
authTokenType: 0,
metadataUri: "ipfs://Qm…",
})
const receipt1 = await tx1.wait()
await sdk.waitForGraphNodeIndexing(receipt1.blockNumber)
const seller = (await sdk.getSellersByAddress(wallet.address))[0]
console.log("Seller created:", seller.id)
// 2. Build and pin metadata. There is no productV1MetadataFactory — construct
// the PRODUCT_V1 object directly per the schema. See Reference → Metadata.
const uuid = crypto.randomUUID()
const metadata = {
schemaUrl: "https://schema.org/Product",
type: "PRODUCT_V1" as const,
uuid,
name: "Test T-shirt",
description: "A shirt for testing.",
externalUrl: `https://example.com/${uuid}`,
licenseUrl: `https://example.com/${uuid}/license`,
image: "https://example.com/shirt.jpg",
attributes: [
{ traitType: "Type", value: "Apparel" },
{ traitType: "Size", value: "M" },
],
product: {
uuid, version: 1,
title: "Test T-shirt", description: "A shirt for testing.",
productionInformation_brandName: "Your brand",
details_offerCategory: "PHYSICAL",
visuals_images: [{ url: "https://example.com/shirt.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)
// 3. Create the offer
const tx2 = await sdk.createOffer({
price: "1000000", // 1 USDC (6 decimals)
sellerDeposit: "0",
buyerCancelPenalty: "0",
quantityAvailable: "10",
exchangeToken: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
metadataUri,
metadataHash: metadataUri.replace(/^ipfs:\/\//, ""), // 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: "<your-dr-id>", // look up via sdk.getDisputeResolvers(); must support USDC
agentId: "0",
feeLimit: ethers.constants.MaxUint256.toString(), // accept any fee
})
await tx2.wait()
console.log("Offer published.")Run it:
SELLER_PRIVATE_KEY=0x... \
NEXT_PUBLIC_RPC_URL=https://sepolia.base.org \
IPFS_URL=https://your-pinning-endpoint \
pnpm tsx scripts/seed.tsUse an authenticated pinning endpoint for
IPFS_URL(Pinata, web3.storage, your own Kubo node, …). The protocol default points at Infura, which now requires a project id.
4. Commit as a buyer
In a Next.js route handler:
// src/app/api/commit/route.ts
import { NextResponse } from "next/server"
import { getSdkForSigner } from "@/lib/boson"
const USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" // Base Sepolia USDC
export async function POST(req: Request) {
const { offerId } = await req.json()
const { sdk } = getSdkForSigner(process.env.BUYER_PRIVATE_KEY!)
// Approve the ERC-20 token if needed. First arg is the TOKEN address.
await (await sdk.approveExchangeToken(USDC, /* amount */ "1000000")).wait()
// Commit — commitToOffer(offerId, overrides?); buyer is the signer.
const tx = await sdk.commitToOffer(offerId)
const receipt = await tx.wait()
return NextResponse.json({ txHash: receipt.transactionHash })
}5. Redeem the voucher
import { getSdkForSigner } from "@/lib/boson"
const { sdk } = getSdkForSigner(process.env.BUYER_PRIVATE_KEY!)
const tx = await sdk.redeemVoucher(exchangeId)
await tx.wait()What just happened
You created an on-chain seller, pinned product metadata to IPFS, published an offer, had a buyer approve a token and commit (which mints a voucher rNFT to the buyer), then redeemed the voucher (which transitions the exchange from COMMITTED to REDEEMED and starts the dispute window).
Common gotchas
waitForGraphNodeIndexingis mandatory after writes if your next step reads from the subgraph. The subgraph lags 1–3 blocks. See Concepts → Eventing & indexing.- ERC-20 approval is per-token, not per-offer. Approving 1 USDC means you can commit to 1 USDC of any USDC-priced offers — until your allowance runs out.
- Validity dates are milliseconds, not seconds. All
*InMSparams are ms since epoch. Double-check. feeLimitis the max protocol fee you'll accept, in token base units. PassMaxUint256to accept any fee; passing0reverts withTotalFeeExceedsLimit.disputeResolverIdis env-specific — it must exist on your chain and support the offer'sexchangeToken. Look up the active set viasdk.getDisputeResolvers()before publishing.
Next
- Build → Sellers for production-grade seller flows.
- Build → Buyers for buyer-side variations (atomic commit-and-redeem, dispute flows).
- Concepts → Going to production before you swap
staging-84532-0forproduction-137-0.