This tutorial walks through creating a marketplace that routes sale proceeds across a seller, a platform fee, and a shared referrer pool — all in one payment call. It shows how nested splits compose, how to distribute a multi-level tree, and how to update the referrer roster without touching the marketplace split.
marketplace split
├── 93% → seller (Account)
├── 5% → platform (Account)
└── 2% → referrer pool (Split)
├── 50% → referrer_a (Account)
└── 50% → referrer_b (Account)
When a buyer pays through the marketplace split:
- The seller and platform receive their share immediately in the same transaction.
- The referrer pool's 2% is credited to its escrow balance. A second
distributecall pays referrer_a and referrer_b.
Keeping the referrer pool as a child split means you can add or remove
referrers — or change their shares — with one update_split call on the pool.
The marketplace split never needs to change.
- Node.js 18+ and npm.
- The Tributary TypeScript SDK installed (
npm install tributary-sdk). - A funded Stellar testnet identity (use friendbot).
- A wallet that can sign transactions (Freighter or a raw keypair for scripts).
The examples below assume you have already set up a client and a
signTransaction helper. See integrations.md for a
minimal client setup.
Children must exist before their parents, because a split can only reference an existing split id. Create the pool first.
import { Client, networks } from "tributary-sdk";
const client = new Client({
...networks.testnet,
rpcUrl: "https://soroban-testnet.stellar.org",
});
// Accounts involved
const platform = "G..."; // your platform address (also the tx signer)
const seller = "G..."; // seller for this listing
const referrerA = "G..."; // first active referrer
const referrerB = "G..."; // second active referrer
// Helper: create a split and return its id.
async function createSplit(
recipients: Parameters<typeof client.create_split>[0]["recipients"],
shares: number[],
controller?: string,
): Promise<bigint> {
const tx = await client.create_split({
creator: platform,
recipients,
shares,
controller: controller ?? undefined,
});
const { result } = await tx.signAndSend({ signTransaction });
return result.unwrap();
}
// The referrer pool is mutable so the roster can change later.
const referrerPool = await createSplit(
[
{ tag: "Account", values: [referrerA] },
{ tag: "Account", values: [referrerB] },
],
[5_000, 5_000], // 50 / 50
platform, // platform controls the pool
);
console.log("referrer pool id:", referrerPool);The pool is created with the platform as controller. That lets you call
update_split later to change the referrer roster.
Now wire the marketplace split to the pool created above.
const marketplaceSplit = await createSplit(
[
{ tag: "Account", values: [seller] },
{ tag: "Account", values: [platform] },
{ tag: "Split", values: [referrerPool] }, // child split
],
[9_300, 500, 200], // 93% / 5% / 2% — must sum to 10,000
);
console.log("marketplace split id:", marketplaceSplit);The marketplace split is locked (no controller) because its routing should never change. Only the pool's internals will be updated when referrers come and go.
Before accepting real money, confirm the numbers look right with
preview_payout. This is a read-only simulation that runs the same rounding
math as pay.
const USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
// A 100 USDC sale (7 decimal places → 1_000_000_000 stroops).
const saleAmount = 1_000_000_000n;
const { result } = await client.preview_payout({
id: marketplaceSplit,
amount: saleAmount,
});
if (result.isErr()) throw new Error(result.unwrapErr().toString());
const [sellerCut, platformCut, poolCredit] = [...result.unwrap()];
console.log(`seller: ${sellerCut}`); // 930,000,000 stroops (93 USDC)
console.log(`platform: ${platformCut}`); // 5,000,000 stroops (5 USDC — dust goes to last by convention)
console.log(`pool: ${poolCredit}`); // 20,000,000 stroops (2 USDC)preview_payout returns one value per recipient in recipient order. The seller
and platform amounts land directly; the pool amount is what will be credited to
the referrer pool's escrow.
See preview-payout.md for full details on the rounding behaviour.
When a buyer completes a purchase, call pay once. The seller and platform
receive their cuts in the same transaction; the referrer pool's share is
credited to its escrow balance.
const buyer = "G..."; // the buyer's address (signs the transaction)
const payTx = await client.pay({
from: buyer,
id: marketplaceSplit,
token: USDC,
amount: saleAmount,
});
await payTx.signAndSend({ signTransaction });
console.log("sale settled in one transaction");After this call:
sellerbalance: +930,000,000 stroops.platformbalance: +50,000,000 stroops.- referrer pool escrow: +20,000,000 stroops (waiting to be distributed).
Check the escrow balance any time:
const { result: poolBalance } = await client.balance({
id: referrerPool,
token: USDC,
});
console.log("pool escrow:", poolBalance); // 20_000_000nSales accumulate in the pool's escrow over time. When you are ready to pay out,
call distribute. Anyone may call it — no controller or payer signature is
required.
const distributeTx = await client.distribute({
id: referrerPool,
token: USDC,
});
await distributeTx.signAndSend({ signTransaction });
console.log("referrers paid");
// referrer_a: +10,000,000 stroops (50% of 20 USDC)
// referrer_b: +10,000,000 stroopsIf multiple sales have accumulated in escrow, distribute pays the whole
credited balance in one call, proportional to each referrer's share.
A new referrer joined. The marketplace split is untouched; only the pool
changes. The platform (controller) calls update_split:
const referrerC = "G..."; // new referrer
const updateTx = await client.update_split({
id: referrerPool,
recipients: [
{ tag: "Account", values: [referrerA] },
{ tag: "Account", values: [referrerB] },
{ tag: "Account", values: [referrerC] },
],
shares: [4_000, 4_000, 2_000], // 40% / 40% / 20% — must sum to 10,000
});
await updateTx.signAndSend({ signTransaction });
console.log("referrer roster updated");From the next sale onward, proceeds flow to the new three-way split. No existing marketplace split is affected, and the buyer's payment call is identical.
Tip: distribute the pool before updating the roster so accrued funds pay out under the old shares. Funds already in escrow are distributed using the shares active at distribute time, not at deposit time.
buyer
│
└─ pay(marketplaceSplit, USDC, amount)
│
├─ 93% → seller (immediate transfer)
├─ 5% → platform (immediate transfer)
└─ 2% → referrer pool escrow (held)
│
└─ distribute(referrerPool, USDC) ← anyone, any time
├─ 50% → referrer_a (immediate transfer)
└─ 50% → referrer_b (immediate transfer)
A direct payment (pay) settles account recipients in one transaction and
credits split recipients to their escrow. Each escrow level requires its own
distribute call, but those calls are permissionless and can be batched or
automated. See architecture.md for the full
money-flow diagram.
- Deeper trees: a referrer pool can itself contain a sub-split (e.g. a
referral agency that splits internally). Each additional level adds one
distributecall. - Batch sales: use
pay_manyto settle multiple listings in one transaction; each listing can have its own marketplace split. - Deposit instead of pay: if your contract holds sale proceeds before
routing them, use
depositto credit the marketplace split, then calldistributeon it rather thanpay. Same math, scheduled settlement. - Lock the pool: once a referrer programme is finalised, call
transfer_control(referrerPool, None)to make it immutable.
See integrations.md and api-reference.md for the full function signatures and all available options.