In a two-sided creator marketplace, a brand posts a paid campaign, funds the budget, and a creator joins after passing Stripe Connect identity checks, delivers the work, and waits for a bank payout through Connect. On web and in the mobile app, the creator opens a specific campaign and sees a compensation card: a read-only block that lists the platform fee, a Stripe Tax estimate, and the euro amount they expect to receive. It is not checkout. It is the preview creators stare at when they decide whether the job is worth it and when they compare the UI to the wire in their bank app.
Who hits the bug: creators on that campaign detail screen. Flow: open an accepted campaign, scroll to the compensation card, read the large number at the top, then later compare it to the Connect payout. What broke: support kept getting screenshots where the headline looked like take-home pay, but the transfer was lower. Creators thought the platform withheld tax from them. The product rule after VAT funding was that creator payout is the fee only; tax on the card was informational, not added to the wire.
Two previews, one shared shape
The platform asks Stripe Tax two different questions and maps both answers into similar JSON.
When a brand funds a campaign, the funding tax preview returns subtotal (platform fee), tax, and total. Total is what the brand pays in, fee plus tax. That number belongs on brand invoicing.
When a creator opens the compensation card, the creator compensation preview may show the same fee and a tax estimate line for transparency. Under the live VAT rules, the Connect payout uses the creator fee only. The internal payout path keeps creator tax at zero: tax can display as an estimate, but it is not stacked on top of what the creator receives.
The trap: both responses reused one TypeScript DTO. The creator card wired the headline to totalCents, which on the funding path means fee plus tax. The label read like estimated creator fee including tax, so creators read brand-style total as creator-style payout.
Why TypeScript did not save us
totalCents was always set on both responses. Web and React Native shared one resolver over optional fields. Every field type-checked. Only the business meaning of total changed with the audience.
Campaign compensation card (fake marketplace)
Creator marketplace · campaign
Est. creator fee incl. tax
€2,503.25
Card headline treats total €2,503.25 as money the creator receives. Tax estimate €3.25 is baked into that headline number.
Try this on the page (demo is embedded above)
The card titled Campaign compensation card (fake marketplace) is part of this article, not a link elsewhere.
- Click Naive (total as payout). Notice one headline amount (fee plus tax combined). That matches what creators screenshot for support.
- Click Fixed (split lines). Notice Your tax estimate on its own row and Payout from the platform on the fee only. Total is not shown as cash in hand.
- With Fixed selected, click Toggle missing payoutCents once. Notice payout falls back to subtotal while the tax row stays separate.
Fake euros; same wiring mistake as production.
Split fields on the creator API path only
Funding tax previews stay as they are. Creator compensation previews gain payoutCents (the fee paid out) and taxEstimateIncludedInPayout: false. Funding responses leave those keys undefined.
export function resolveCreatorPayoutCents(preview: CompensationTaxPreview): number {
if (preview.payoutCents != null) return preview.payoutCents
if (preview.subtotalCents != null) return preview.subtotalCents
throw new Error('Creator payout preview missing payoutCents/subtotalCents')
}UI copy mirrors the split, plus a short disclaimer that the estimate is not part of the payout. Unit tests assert the display layer never selects totalCents for the payout line.
Stripe Connect and Stripe Tax still execute KYC and tax calculation. The bug was showing a brand payable total on a creator payout surface.
If total ever meant including tax on a funding screen, treat that DTO as unsafe on any what-you-receive UI until you add an explicit payout field and test the resolver. Shared types are fine. Shared headline semantics are not.
Happy coding! Sander