Why we bill off-chain instead of running an on-chain splitter
Platform-fee architecture is a load-bearing decision. We picked the boring answer for a reason — and a smart-contract splitter is still on the roadmap as an opt-in.
If you're building a non-custodial billing platform on stablecoins, the obvious-looking answer for collecting your fee is an on-chain splitter contract: customer pays the contract, the contract sends 99% to the merchant and 1% to you, atomically. Same block, no AR, no invoices. We considered this seriously — and chose the boring alternative. Here's why.
What "non-custodial" actually means
The non-custodial promise is a property of customer funds, not platform fees. When a customer pays for your SaaS subscription, that money goes to you, full stop. We don't sit between them and you on-chain, we don't operate a pool, we don't issue payouts. That's the part that keeps us out of money-transmission and CASP scope, and it's what we structurally cannot give up.
Merchant-to-platform billing is a different animal. It's a normal B2B SaaS relationship between you and us. Stripe bills its merchants this way. Coinbase Commerce bills its merchants this way. Treating it like one isn't a compromise on the non-custodial story — it's just being precise about what the property is.
The case against an on-chain splitter
- Audit risk on a fund-routing contract is existential. A single bug in a splitter steals merchant money — not platform money — and the platform still owns the incident.
- Iterating fee tiers in a splitter is a per-chain redeploy + audit. Iterating fee tiers in a database is one UPDATE.
- Per-payment gas overhead lands on the customer. Stablecoin fees are sensitive enough that a 3-cent splitter pass-through is worth optimising away.
- Multiple chains × multiple stablecoins × subscription / one-time × refund paths is a lot of code surface. The Pareto frontier on day one is to ship the rails first and instrument the fee layer separately.
What we actually do
Customer payments hit the merchant's wallet 1:1 on-chain. Our chain-reader workers observe the deposit, match it to the payment intent, and emit a confirmed event. At confirm time we compute the platform fee (a pure, bigint-safe function with merchant-favoured-floor rounding — Stripe convention) and write it to two columns:
// computePlatformFee — pure function, bigint internals, merchant-favoured floor
export function computePlatformFee(amountMinor: bigint, feeBps: number) {
// floor((amount * fee_bps) / 10_000) — merchant keeps the rounding remainder
const fee = (amountMinor * BigInt(feeBps)) / 10_000n;
return { fee, net: amountMinor - fee };
}Once a month we aggregate every confirmed payment for the workspace, generate a fee statement at /v1/workspaces/:ws/fee-statements, and bill the merchant in any supported stablecoin. No customer ever pays gas for our fee, no contract ever sits between them and the merchant.
When we'll add the splitter
It's on the roadmap as an opt-in for merchants who specifically prefer atomic on-chain settlement of platform fees — usually because their accounting model treats the platform fee as a customer-side cost rather than a platform-side B2B charge. We'll build, audit, and publish it once volume justifies the audit cost. Until then, the answer is a database column called fee_bps and a monthly invoice.