Contents

Pricing

The pricing capability: a fee policy that splits a sale into recipient legs and platform revenue.

Source src/pricing.ts#L40flatFeesrc/pricing.ts#L60splitLegssrc/contract.ts#L414FeePolicy

The seam

Where does a sale’s money go? The buyer pays one price, but that price fans out: some to each seller, the rest to the platform as its fee. The Pricing port is the one seam where that split lives, so you can change the rule without touching the spend handler that posts the result.

A FeePolicy is a pure function. You hand it a price and the recipients; it hands back the ledger legs that distribute the money. It does no math on the chain, touches no store, and makes no decision about whether the buyer can afford the purchase. It only divides an amount you already decided to charge.

The economy holds the policy as a capability, on Ctx.pricing, and supplies it the way it supplies any external service. You pass one in when you build the economy.

The contract

FeePolicy takes one input object and returns an array of legs:

type FeePolicy = (input: {
  price: Amount;
  recipients: ReadonlyArray<Recipient>;
  feeBps: number;
  buyerId?: string;
  sku?: string;
}) => ReadonlyArray<Leg>;

Three fields drive the split. The price is the amount being divided. The recipients are the sellers and their shares. The feeBps is the platform’s cut in basis points, where 10000 bps is the whole price.

Each Recipient is a seller plus a share: { sellerId, shareBps }. The shares are basis points of the part left after the fee comes off the top, so 100 bps means one percent. The buyerId and sku ride along for a policy that wants to price by buyer or item, but the reference policy ignores them.

The output is credit-side only. Every leg is a credit, and a credit is stored negated, so the legs sum to -price. They don’t balance on their own.

The reference policy: a flat fee

The built-in policy is flatFee. It applies one fixed rate (whatever feeBps you pass) to every sale, and the spend operation passes the platform’s configured rate (PLATFORM_FEE_BPS, default 1530, i.e. 15.3%).

const policy = flatFee();
const legs = policy({
  price: toAmount('CREDIT', 1000n),
  feeBps: 3000,
  recipients: [{ sellerId: 'usr_seller', shareBps: 10000 }],
});
// Price 1000 at a 30% fee:
//   seller credited 700, revenue credited 300.
//   Both are credits (stored negative), so the legs sum to -1000.

The fee comes off the top first. Each recipient then takes its shareBps of what’s left. Whatever the rounding leaves behind joins the fee in the platform’s REVENUE account.

Each seller’s share rounds down, so the shares can fall a minor unit or two short of the net; revenueForSplit hands the remainder to REVENUE with the fee, so the credits sum to the exact price.

The fee itself rounds in the other direction. feeForPrice rounds the basis-point fee up to a whole credit, then caps it at the price so the fee can never exceed what the buyer paid. The cap only bites below one whole credit; real listings are hundreds of credits, where the fee is an ordinary cut.

For the split as money (par, buy, and the platform’s spread) see the money model. Pricing only divides an amount; the spread is a separate idea about how a credit is valued.

What the core assumes

The spend handler takes the legs on trust and checks the shares.

It assumes the legs are credit-side and sum to -price, because it pairs them with a single buyer debit for the same price and expects the posting to balance. A policy that returns unbalanced legs would post a transaction that doesn’t conserve value, and the integrity checks would catch it, but as a failure, not a graceful decline.

It does not trust the recipient shares blindly. Inside splitLegs, assertShareSum throws unless the shares sum to 10000 bps. The spend handler already validates this upstream, so in correct wiring the backstop never fires; it exists so a wiring mistake fails loudly here rather than silently under-crediting sellers and dumping the difference into revenue.

At the port level an empty recipient list is tolerated: with no seller to pay, the whole net becomes leftover and lands in REVENUE. A spend never reaches it that way, though — the handler refuses an empty recipients list up front, so an omitted field can’t quietly route a sale’s whole net to the platform.

Out of scope

A FeePolicy divides one price. It is not where you decide whether to charge, whether the buyer has funds, or how a refund reverses the split later. Those live in the spend and refund operations.

It also has no say off the request path. The background worker that settles payouts and realizes fees writes its own balanced legs and is given no pricing rule at all, because a sweep isn’t splitting a sale. It’s moving money the ledger already recorded.

See also