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.