2026-09-20
How Crypto Split Payment Distributions Actually Settle

A charge with recipients defined is not the same thing as recipients getting paid. charge.confirmed tells you a customer's funds landed at the charge's split address. It does not tell you the seller's wallet, the platform fee wallet, or the referral wallet have received anything yet. That second step is distribution, and it has its own mechanics, failure states and webhook.
Most integrations never need to think about this, because the default worker handles it end to end. But once you operate split payments in production (reconciling payouts, alerting on stuck money, or explaining to a merchant why their wallet balance hasn't updated), you need to know what happens in that gap.
Distribution is a separate event, not a side effect
A charge that settles across a (token, network) pair creates its own distribution record the moment payment is confirmed. That record moves through a status of its own, pending, processing, completed, failed or below_minimum, independent of the charge's own status. A charge can sit at confirmed for several minutes while its distribution is still pending, and that is completely normal.
charge.settled is the event that means money reached the recipients. If your fulfillment logic only listens for charge.confirmed, you know a payment happened but not that the split paid out. For a single-recipient charge the difference rarely matters. For a marketplace paying a seller and taking a platform fee, it separates "the customer paid" from "the seller has been paid", and your support team will get asked about those two facts separately.
Distribution is permissionless, on purpose
The mechanism underneath a split is 0xSplits, and its distribute() function is public. Anyone can call it, including people who have nothing to do with Klap. Klap runs a worker that claims and executes distributions automatically, but it has no exclusive right to do so.
Every distribution carries a distributorFeePercent, frozen at charge creation: a small cut of the split balance paid to whoever successfully calls distribute() first. Klap's worker claims a distribution after a five-minute grace period from the moment it became payable. Before that window closes, a merchant running their own automation can call distribute() first and collect that fee instead.
You don't need to work around this. It is what keeps the split working when Klap's own infrastructure goes down: the underlying contract does not depend on Klap's worker being alive, only on someone eventually calling a public function.
The state machine behind a payout
A distribution's lifecycle is intentionally narrow:
pending: queued, not yet claimed by a distributor.processing: a distributor has claimed it and is submitting the on-chain transaction.completed: the recipient split has the funds.failed: every automatic retry has been exhausted.below_minimum: what the split holds hasn't covered the gas to pay it out for 24 hours, so nothing was sent. Klap firescharge.settlement_below_minimum, and a new payment on the same pair re-arms it.
Failures retry on a fixed backoff: immediately, then after 5 minutes, 30 minutes, 2 hours and 1 day. If the fifth attempt still fails, the distribution moves to failed and Klap fires charge.settlement_failed. That webhook is the one to alert on. It means a payment was detected and confirmed, but the money has not reached its recipients and automatic retries are done.
Two safety nets sit underneath that state machine, and both help if you're debugging a stuck payout:
- If a worker process dies mid-transaction, a distribution left in
processingfor more than 20 minutes is reset topendingso another attempt can claim it. A container restart won't make a payout silently vanish. - If a split's on-chain balance reads zero well past the point it should have been distributed, and no matching outgoing transfer is found, Klap logs a suspicious state instead of assuming everything is fine. An empty split with no record of paying it out gets investigated.
Watching distributions from your own side
The Node SDK exposes the same state the worker operates on, so you can reconcile independently of webhook delivery:
for await (const event of klap.distributions.streamPending()) {if (event.type === 'distribution.available') {console.log(event.distribution.splitAddress,event.distribution.estimatedRewardAmount,event.distribution.graceEndsAt,)}if (event.type === 'distribution.claimed') {console.log('claimed', event.splitAddress)}}
streamPending() is a live SSE feed of distribution.available and distribution.claimed events, scoped to your own environment. It is no substitute for a snapshot, so connect it first, then call list() or listAll() to read the current pending set, and apply every event you received in between as an idempotent add or remove on top of that snapshot:
for await (const distribution of klap.distributions.listAll()) {console.log(distribution.splitAddress, distribution.recipients)}
Each PendingDistribution carries the split address, the exact recipient list and percentages, the frozen distributorFeePercent, an estimatedRewardAmount, and graceEndsAt. That is enough to decide in real time whether a payout is still waiting on Klap's worker or already claimed.
When this matters enough to act on
For most integrations, nothing here changes your code. The default worker settles distributions within its grace period and charge.settled fires.
It starts to matter once a single split holds enough value that latency or reliability around the payout becomes a product concern: a high-volume marketplace, a payroll-style disbursement, or a platform whose sellers watch their wallet balance closely. Three things are worth building around:
- Alert on
charge.settlement_failed, and on silence too. A recipient frozen by the token issuer holds the payout without failing it. A charge stuck atconfirmedwith nocharge.settledafter a reasonable window is money detected but not delivered, which is a support and reconciliation problem for you, not one your customer sees. - Use
distributions.streamPending()/list()to reconcile payout state directly against your own records, whether or not a webhook delivery succeeded. - Decide deliberately whether to ever claim a distribution yourself. Running your own distributor after the grace period expires is possible and collects the
distributorFeePercentreward, but it adds operational surface area most teams don't need. Know it's an option before you build it, not after a support ticket forces the question.
A split recipient list says where money should go. Distribution is the separate, sometimes public process that carries it out. Treat it as its own event in your system, and "the seller hasn't been paid yet" becomes a state you can query.