Skip to content
Money flow

Paying venues their share, without manual accounting

The platform collects money for sports venues and for players who run their own games. I designed and built how that money is split, held, paid out and explained, so each venue and organiser gets their share, the platform keeps its fee, and nobody has to work it out in a spreadsheet.

Market
Court booking, Australia
My role
Lead engineer and backend architect
Who gets paid
Venues and organisers
Payments
Stripe Connect

ProblemA marketplace that takes money for others must pay each of them their exact share, keep its own fee, and survive refunds that arrive after the money has gone out.

What I didI built connected accounts for venues and organisers, one fee model, admin-run payouts with a refund buffer, billing withheld from payouts, and a statement for every payout run.

ResultPayouts run from one screen and explain themselves, the platform's fee is kept on every payment, and no one does the accounting by hand.

The problem

Collecting money for others means paying them back exactly

The platform is a court booking marketplace in Australia. Players find a venue, book a court, pay alone or split the cost with friends, and join games and tournaments. Venues use a portal to run their courts, prices, memberships and events. Some players also run their own paid games and competitions, without a venue behind them.

In every case the money goes through the platform. A player pays once, in the app. That one payment belongs to several parties at the same time: most of it to the venue or the organiser, a fee to the platform, and a processing cost to the card network. Later, some of it may need to go back to the player as a refund.

So the platform has to answer four questions for every dollar. Who does it belong to? When can it leave? What if it has to come back? And how will the person who receives it know it is right? If any one of those answers is a person with a spreadsheet, the business does not scale past a handful of venues, and every payout becomes a support conversation.

I was the lead engineer and backend architect on this platform. Other developers worked mainly on the mobile app and the venue portal screens. The server, the payment model, the payouts and the documents were mine to design and build.

What made it hard

Refunds after payouts, deals off the books, and fees on fees

Refunds can arrive after the money has left. A player cancels a booking on Friday for a game next week. If the venue's balance was paid out to its bank on Thursday, the refund has nothing to come from. With connected accounts, a refund reverses the venue's transfer. If the venue's balance is already zero, the account goes negative, and that is the platform's problem, not the venue's.

Not all money is a card payment. The platform charges each venue a subscription and optional add-ons. These are negotiated one by one, so they are an amount and a plan name set by an admin, not a price list. The card processor never sees this side of the deal. Yet it has to be collected, and the easiest place to collect it is from the venue's own takings before they are paid out.

Organisers want different pricing. One organiser wants to say "entry is $20" and have players pay a little more to cover fees. Another wants players to pay exactly $20 and will absorb the fees. Both are reasonable. The first one is harder than it looks: the processing fee is a percentage of the total, so adding the fee on top raises the total, which raises the fee. You are charging fees on fees, and you have to solve for the total that leaves the organiser with exactly what they asked for.

And small maths errors become visible. A payout statement is a promise that the numbers add up. One wrong cent on one line is enough for a venue to stop trusting the whole document.

Follow the money

From one payment to a line on a statement

It helps to follow one booking all the way through.

First, the player pays the price they were shown. The payment is a destination charge on the venue's connected account. The charge carries an application fee: the platform's fee plus the processing cost. So when the payment settles, the venue's account receives the price minus both, and the platform keeps exactly its fee. The player is never charged more than the booking price for a normal booking.

Second, if the booking is split, each co-player pays their share plus a small split fee, and the application fee on that share is the split fee. The venue still receives its full share. The main booker's payment carries the fee a single payment would have carried. The result is that the venue nets the same amount whether one player pays or four.

Third, the money sits in the venue's balance. Connected accounts are set to a manual payout schedule, so nothing leaves on its own. Each venue chooses how often it wants to be paid: weekly, fortnightly or monthly.

Fourth, an admin runs the payout. The server reads the venue's available balance, keeps back a refund buffer, withholds any subscription or add-on charges that are due, and sends the rest to the venue's bank.

Fifth, the statement is issued. On every payout run, the server builds a statement from the period's payment records, renders it as a PDF, stores it, and emails it to the venue. Every number on it comes from the same records the payout came from.

A sample statement

What a venue sees for one payout period

EventStateAmount
Sales at list priceSalesA$1,250.00
Discounts you fundedDiscount−A$50.00
Paid by customersGrossA$1,200.00
Platform and processing feesFees−A$81.40
Refunds to customersRefund−A$40.00
Subscription withheldWithheld−A$60.00
Kept back as refund bufferRetained−A$50.00
Paid to your bankPayoutA$968.60
Example amounts only, not real rates or a real venue. The real statement also lists every transaction on the following pages, and its ledger total matches the transfers for the period.
Connected accounts

Only venues that can be paid can be booked

Every venue gets a connected account. The venue completes onboarding with the card processor's own hosted pages, so identity and bank details are never typed into our apps. The portal shows the account's status: whether payments and payouts are enabled, and what is still missing.

One bug taught me to make this status a hard rule. A venue without a working connected account still showed up in search and availability. A player could pick a court, choose a time, and only fail at the very last step, when the payment had nowhere to go. I added one check, kept in sync by the account update webhook, and wired it into every place where the server decides that a venue is bookable: the venue list, the detailed availability, the booking options and the creation of a temporary booking. A venue that cannot take payments is now simply not offered.

The onboarding link had its own trap. The processor's onboarding links expire within minutes, and the server was handing back a stored link, so "Complete onboarding" opened a dead page. Now the server creates a fresh link every time it is asked, and the portal checks the account again when the venue comes back to the tab.

Organisers who run paid games without a venue get their own connected account too, always as an individual, never as a business. The app shows them one of four honest states: create your profile, finish onboarding, fix what the processor needs, or ready. When it is ready, it shows the bank the money goes to by name and the last four digits, which is enough to recognise the account and not enough to misuse it. A paid game that collects money in the app cannot be created until that profile is ready, so the first player's payment never fails. Organisers then pay themselves out from the same Wallet screen: the whole available balance goes to their bank, and a statement email lists each competition with its entries, the money collected and what the organiser keeps.

The fee model

One place that knows every fee

Fees used to live in several places: a percentage here, a surcharge there. I moved them into one fee configuration on the server: the processing cost, the platform fee, the split fee and the cancellation rules, with small helper functions around them. Every charge reads from it.

Then I built a quote endpoint on top of the same configuration. Given an amount, it returns the full breakdown: what the player pays, what the venue receives, the processing cost, the platform fee and each player's part of a split. The apps show that breakdown instead of doing their own maths, so the price on a button cannot drift from the real charge.

For organisers I added a single choice, asked in plain words when they set the price: "this price includes the payment processing fee". If it does, the player pays the face price and the fees come out of the organiser's side. If it does not, the server grosses the charge up so the organiser receives exactly the price they set and the player pays the fees on top. Players only ever see one line for this, "Payment processing fee", on the checkout and on the receipt.

The processing cost at payment time is an estimate. International and wallet cards can cost more. So when a charge succeeds, the server reads the real fee from the processor's balance transaction and records it, together with the country of the card. The statement uses what really happened, not what we expected.

Payouts

Moving money only on purpose

A transfer is not a payout. When a player pays, money moves into the venue's connected account. That is a transfer. A payout is when money leaves that account for the venue's bank. I kept these apart on purpose. Venues are on a manual payout schedule, and payouts are run by an admin at the frequency the venue chose.

The admin screen shows each venue's available and pending balance, a button to run the payout and the payout history. A payout cannot be undone, so it has a confirmation dialog that states the exact amount for each currency. The button and the success message state amounts, not a count. A failed balance fetch shows an error with a retry, so it cannot be mistaken for a zero balance. If one currency fails while another has already gone out, the admin is told exactly that, not a flat "failed" while money is in flight.

Every payout keeps back a refund buffer, a minimum balance an admin sets in the platform settings. Refunds reverse the venue's transfer, so this buffer is what stops a later refund from driving the venue's account negative. The value is read on every payout, so a change in settings applies to the very next run.

Between the buffer and the withheld charges, a payout is often smaller than the balance on screen. Unexplained, that looks exactly like a bug, and the admin is the one who has to answer the venue. So the result of every payout names both deductions.

The platform's own money works the same way. Its accumulated fees are read live from the processor, its payouts to its own bank are manual, and a double-click cannot pay twice because each payout request carries an idempotency key.

Billing

Charges that are withheld once, and never forgiven by accident

Subscriptions and add-ons are raised into a ledger of charges. Each charge is unique for a venue, a kind, a code and a period start. That one rule means a re-run, or two overlapping scheduled jobs, cannot bill a venue twice. Without a ledger there is no answer to a simple question: which payout paid for July?

At payout time, outstanding charges are withheld from the venue's balance, capped at what is available. A venue that owes more than it earned keeps the rest outstanding instead of going negative. Charges are marked as collected only after the processor accepts the payout, so a failed payout never quietly forgives them. A waived charge stays in the ledger as waived, with no deletion, so the decision can be checked later.

When a charge cannot be withheld because there was no balance that period, the statement prints the amount owing with the platform's bank details, the statement number as the payment reference and a due date. At that point the statement is an invoice, and an invoice without payment instructions is useless.

Statements

A document that explains every dollar

The payout statement is a multi-page PDF. Page one has the earnings summary and the balance reconciliation. The transaction ledger then flows across as many pages as it needs, with running totals carried from page to page. Every figure is derived from the transaction list, so the summary cannot drift from the detail, and the ledger total is stated to equal both the net amount and the transfers for the period.

I made a few choices for the reader, not the engineer. Sales are grouped by type, so a venue can see what it sold without adding up rows. The platform fee and card processing are one line, because a venue reads "what did the platform take" as a single number. Discounts the venue funded with its own codes have their own line above sales, because otherwise a promotion looks exactly like a bad month. And there is no tax column of zeroes while the platform is not registered for tax, because a column of zeroes reads as "charged at zero percent", which is a different and untrue claim.

Each document carries a short code that is checked on a public verification page, so a venue's accountant can confirm that a statement is real and has not been edited. If the statement fails to build, the payout still goes through. A document problem never blocks money.

Writing the statement found real bugs. While I was checking a sample statement by hand, one split booking showed the fees one cent higher than the arithmetic said. The cause was floating-point maths: a percentage of a round amount came out a hair above a whole cent, and rounding up turned that into a full extra cent, on exactly the larger bookings. I moved the fee calculation to whole-number maths. Another check showed that a cancelled booking kept advertising a payout to the venue that never happened, because the venue's share was written once at payment time and never updated. Now refunds, partial captures and released holds all update it.

The decisions

Four choices behind payouts that explain themselves

Where does a venue's balance come from?

Option

Our own balance number per venue, updated as money moves

Chosen

The processor's balance for money, a ledger for charges, statements built from payment records

Each fact has one source, so there is no second number that can drift.

What happens to refunds after a payout?

Option

Pay out the whole balance every time

Chosen

Keep an admin-set refund buffer in every venue's balance

A later refund always has something to come from, and no account goes negative.

Who pays the processing fee on a paid game?

Option

Always take it from the organiser

Chosen

The organiser chooses, and the server grosses up when the player pays

It fits each organiser's pricing, and the organiser still receives exactly what they set.

What about the credits screens nobody could use?

Option

Keep them hidden for a later version

Chosen

Remove them from every app and from the server

Credits were never built. The screens read hardcoded data and showed venues invented balances.

In code

Grossing up so the organiser nets exactly their price

// Written for this page. Not client code. Amounts in cents.
// Processing cost = percent of the charge + a fixed part. Rates are made up.
const RATE_PPM = 20_000; // 2 percent, in parts per million
const FIXED = 30;

const feeOn = (charge: number) =>
  Math.ceil((charge * RATE_PPM) / 1_000_000) + FIXED;

// Smallest charge that leaves at least `net` after the fee on that charge
function grossUp(net: number): number {
  let charge = Math.ceil(((net + FIXED) * 1_000_000) / (1_000_000 - RATE_PPM));
  while (charge - feeOn(charge) < net) charge += 1;
  return charge;
}

// grossUp(2000) -> 2072: the player pays $20.72, the organiser nets $20.00
A fresh sketch with made-up rates: integer maths only, the percentage in parts per million, and a final check so the net is never a cent short.
What changed

Payouts that run, add up and explain themselves

No manual accountingVenue shares, platform fees and withheld charges are worked out by the server from payment records.
A statement per payoutEvery payout run issues, stores and emails a statement that reconciles to the period's transfers.
Fee kept on every paymentThe platform's fee is taken as the application fee on each charge, whether one player pays or four.
What I learned

Money you hold for others needs a story, not just a number

The first lesson was that a payout is not finished when the money moves. It is finished when the person receiving it can see why it is that amount. A large part of the work in this system went into explaining: the statement, the named deductions, the discount line, the real fee instead of the estimate. A correct number without an explanation still becomes a support call.

The second lesson was to keep one source for each fact. The money balance comes from the processor. The charges come from our ledger. The statement comes from the payment records. When I needed a number, I derived it from one of those, and I avoided storing a second copy that could drift. Two of the bugs I found here came from copies: a fee calculated in floating point, and a venue's share written once and never updated.

The third lesson was to make dangerous actions slow and safe ones fast. Running a payout cannot be undone, so it asks for confirmation and states the amount. Booking a court is safe only if the venue can take money, so the server checks that before it shows the venue at all.

The fourth lesson was about removing things. Taking out the credits screens was not a feature anyone would notice. But a half-built feature that shows invented balances to a venue is worse than no feature, because it teaches people not to trust the numbers.

Where this applies

Any platform that takes money and pays it on

The same design fits any marketplace that collects payments for others. A tutoring platform pays its tutors. A rental site pays its hosts. An event platform pays its organisers. A food platform pays its restaurants. Each one has a fee to keep, refunds that arrive late, deals that never touch the card processor, and partners who want to know why this week's payout is smaller than last week's.

The pattern is the same each time. Take the platform's fee when the payment is made. Keep the money until you choose to move it. Hold back enough for refunds. Withhold what partners owe you, once, from a ledger. And issue a statement that is built from the same records as the payout, so the two can never disagree.

Running a marketplace that pays others?

If your payouts need a spreadsheet, or your partners keep asking why a payout is smaller than they expected, let's talk. I can help you design payouts and statements that add up and explain themselves.

Let's talk