Royalties
Boson moves royalty handling from the voucher contract into the Diamond. This unlocks:
- Per-offer royalty splits — different offers in the same voucher collection can pay different recipients at different percentages.
- Multiple recipients per offer — split a single sale across many parties without an off-chain splitter.
- Royalty Registry compatibility — works with royaltyregistry.xyz so marketplaces that consult the registry honour the full recipient list.
- Sequential-commit royalties — secondary-market resales through the protocol pay royalties at finalisation time using the recipients in force at each sale (see Sequential commit).
The voucher contract still exposes ERC-2981 royaltyInfo(tokenId, salePrice) for marketplaces that only support a single recipient; it returns the sum of all bps and the first recipient on the list. Marketplaces that consult the Royalty Registry get the full multi-recipient breakdown.
bps: basis points. 100 bps = 1 %. The protocol stores percentages in bps so they can be expressed without floating-point.
Two-step model
- Admin pre-approves recipients on the seller, optionally setting a per-recipient minimum royalty %.
- Assistant picks a subset of approved recipients per offer, with bps that meet or exceed the admin's minimums.
The seller's treasury is always the default recipient (zero-address placeholder) and cannot be removed. If you later update the treasury via updateSeller, every offer that uses the default automatically picks up the new address — no per-offer update needed.
Configure recipients on the seller
// Admin-only. Recipients can later be referenced by address from any offer.
await (await sdk.addRoyaltyRecipients(sellerId, [
{ wallet: brandA, minRoyaltyPercentage: 250, externalId: "brand-a" }, // 2.5% minimum
{ wallet: brandB, minRoyaltyPercentage: 500, externalId: "brand-b" }, // 5% minimum
{ wallet: charityC, minRoyaltyPercentage: 100, externalId: "charity-c" }, // 1% minimum
])).wait()
// Later: update or remove
await (await sdk.updateRoyaltyRecipients(sellerId, [1], [{ wallet: brandA2, minRoyaltyPercentage: 300, externalId: "brand-a-v2" }])).wait()
await (await sdk.removeRoyaltyRecipients(sellerId, [2])).wait() // remove charityCSet the per-offer split at creation
Offers now carry a royaltyInfo field. The default recipient (treasury) is address(0).
await sdk.createOffer({
// …offer fields…
royaltyInfo: {
recipients: [
"0x0000000000000000000000000000000000000000", // = seller treasury (default)
brandA,
charityC,
],
bps: [200, 300, 100], // 2% to treasury, 3% to brandA, 1% to charityC → 6% total
},
})Constraints enforced by the Diamond:
- Every non-default recipient must be on the seller's approved list.
- Each
bps[i]must be ≥ the admin-set minimum for that recipient. - The sum of
bpsmust be ≤ the protocol'smaxRoyaltyPercentage.
Update royalties on an existing offer
await sdk.updateOfferRoyaltyRecipients(offerId, {
recipients: [ZeroAddress, brandA],
bps: [300, 400],
})
// Or batch
await sdk.updateOfferRoyaltyRecipientsBatch([offerId1, offerId2], { /* same shape */ })This only affects future secondary-market sales of vouchers from that offer; commits already paid out keep their old terms.
Reading royalty info
// Full multi-recipient breakdown — call the Diamond's ERC-2981 entry point and the
// Royalty Registry. There is no `sdk.getRoyalties` / `sdk.getEIP2981Royalties`
// at the current SDK version; either build the call yourself or read from the
// subgraph.
import IBosonOfferHandler from "@bosonprotocol/common/dist/cjs/abis/IBosonOfferHandler.json"
const diamond = new ethers.Contract(diamondAddress, IBosonOfferHandler, provider)
const { recipients, bps } = await diamond.getRoyalties(tokenId)
// ERC-2981 reduction (single recipient, summed bps) — call the voucher contract
// (not the Diamond), e.g. via the seller's collection address:
const voucher = new ethers.Contract(voucherCollectionAddress, ["function royaltyInfo(uint256,uint256) view returns (address,uint256)"], provider)
const [receiver, royaltyAmount] = await voucher.royaltyInfo(tokenId, salePrice)The Boson voucher contract exposes royaltyInfo(tokenId, salePrice) — the standard ERC-2981 entry point — and the Diamond carries the per-offer multi-recipient breakdown.
Royalty Registry setup
If your offers ever have multiple recipients, point the registry at the Boson protocol so it returns the full breakdown:
- Find the Royalty Registry address on your chain.
- Call
setRoyaltyLookupAddress(yourVoucherCollectionAddress, bosonDiamondAddress).
Marketplaces that honour the registry then enumerate every recipient on each secondary sale. Marketplaces that only respect ERC-2981 still pay the first recipient.
If you'd rather split one ERC-2981 royalty across many wallets, use a splitter contract (e.g. 0xSplits) as the first recipient with the full bps, and leave the others at 0 bps — they'll only be paid by registry-aware marketplaces.
Common gotchas
- Zero-address = treasury. If you put a real address in for the default split, you'll have to update every offer when the seller's treasury changes.
- Minimum bps is per-recipient. The minimum doesn't apply to the treasury (default) recipient.
- No retroactive royalty changes on completed exchanges. Royalties paid out on an exchange use the recipients that were in force at each sale event.
- Royalty caps are protocol-level.
maxRoyaltyPercentageis set byConfigHandlerFacet; checkprotocol.getMaxRoyaltyPercentage()before designing splits.
Related
- Sellers → Multi-collection — multiple voucher collections per seller, each with its own royalty profile.
- Buyers → Sequential commit — pays royalties on protocol-mediated resales.
- Build → Royalty recipients — what the receiving side cares about.
- Reference → Core SDK → Accounts —
addRoyaltyRecipientsand friends.