Contents

A marketplace fee split

One sale, several sellers: the fee off the top, shares in basis points, no unclaimed remainder.

Source src/pricing.ts#L52splitLegssrc/operations/spend.ts#L80spend

A collab sells: an artist and an animator split the price, and the platform keeps its cut. That is one spend with several recipients — never several transfers to reconcile after the fact.

The split rule is owned by the pricing policy and runs inside the sale’s one balanced transaction:

  1. The platform’s fee comes off the top.
  2. Each recipient takes their shareBps of the net — basis points, and the shares must sum to exactly 10_000, so no share of the price can go unclaimed.
  3. Whatever rounding leaves over goes to the house, not into thin air: the books stay balanced to the minor unit.
import {
  SYSTEM,
  createEconomy,
  credits,
  earned,
  encodeAmount,
  spend,
  systemActor,
  topUp,
  userActor,
} from '@pwngh/economy-lab';

import type { SnippetReport } from './context.ts';

// One spend, two sellers: the platform fee comes off the top, each recipient takes their
// basis points of the net, and any rounding leftover stays with the house — the shares must
// sum to exactly 10,000 so no remainder goes unclaimed.
export async function run(): Promise<SnippetReport> {
  const economy = await createEconomy();
  await economy.submit(
    topUp({
      idempotencyKey: 'idem_fund',
      actor: systemActor('payments'),
      userId: 'usr_b',
      amount: credits(1_000),
      source: 'card',
    }),
  );
  await economy.submit(
    spend({
      idempotencyKey: 'idem_sale',
      actor: userActor('usr_b'),
      orderId: 'ord_f1',
      buyerId: 'usr_b',
      sku: 'Collab Piece',
      price: credits(1_000),
      recipients: [
        { sellerId: 'usr_artist', shareBps: 6_000 },
        { sellerId: 'usr_animator', shareBps: 4_000 },
      ],
    }),
  );
  const [artist, animator, house] = await Promise.all([
    economy.read.balance(earned('usr_artist')),
    economy.read.balance(earned('usr_animator')),
    economy.read.balance(SYSTEM.REVENUE),
  ]);
  await economy.close();

  return {
    lines: [
      'a 1,000-credit sale, split 60/40 behind the default fee',
      `artist (6,000 bps):   ${encodeAmount(artist)}`,
      `animator (4,000 bps): ${encodeAmount(animator)}`,
      `house (fee + rounding leftover): ${encodeAmount(house)}`,
    ],
    consolePath: '/market',
  };
}

Each seller’s cut lands in their earned balance, which is the balance requestPayout later pays out as real money. A refund of the order unwinds every share — both sellers and the fee — in one opposite posting.

See also