2026-09-18
What Self-Custody Payments Actually Require

A customer sends USDC. The transaction confirms on Base. The money lands in a wallet your business controls, not in a processor balance waiting for a withdrawal request. That's the practical promise of self-custody payments, and it changes more than where a balance appears.
For an engineering team, custody is an architecture decision. It determines who can move funds, who carries counterparty risk, what reconciliation data exists, and whether your checkout is yours. A crypto processor that holds merchant funds can make onboarding feel familiar, but it also adds a second ledger and an approval step between a confirmed blockchain payment and your working capital.
Self-custody removes that layer. The engineering work stays, but it moves somewhere you can see it: systems your team runs and can inspect, instead of a dashboard you have to trust.
What self-custody payments mean
A self-custody payment system detects an on-chain transfer and routes it to a destination the merchant controls, or to the recipients the merchant defined. The infrastructure can create charges, derive addresses and watch the chain. It should never be able to withdraw, freeze, pool, lend or delay the funds.
That line gets blurry because plenty of products say "non-custodial" while still routing payments through a managed wallet or an intermediary ledger. Ask a simpler question. When the customer signs the transaction, which address receives the asset? If the processor controls that address, the processor has custody at settlement, whatever the dashboard calls it.
Under direct settlement, the on-chain transaction is the source of truth. A processor's dashboard can still help with monitoring and support, as an observation layer on top of that truth. It shouldn't be the system deciding whether you can touch money that's already yours.
Why custody becomes a product problem, not just a treasury one
Custody usually gets framed as a compliance or treasury matter. It also shapes your checkout, since it changes what your app is allowed to know.
When funds settle into a processor's account, your app has to wait on that processor's internal state before it can act. A payment can be visible on-chain and still locked for fulfillment and refunds until a separate ledger marks it settled. Your customer never sees that gap, but your support inbox eventually will.
A direct-settlement model gives your backend a cleaner boundary. Your app creates a charge with an expected amount, an expiry and a set of recipients. The customer pays the address Klap derived for that charge. Your system gets a verified status event once the network's confirmation depth is reached: Base needs 15 blocks, Ethereum 12, Arbitrum 120. Reorg risk differs on every chain, and "confirmed" should mean the same thing everywhere. By the time you see the event, the funds are already at their destination, with no settlement step waiting behind a processor's release schedule.
That doesn't mean every business should ship on the first block. A digital good with low fraud exposure can act sooner than a high-value physical order. You pick the policy, and a processor's one-size-fits-all release schedule doesn't get a say.
The shape of a direct-settlement charge
A sound design separates the payment intent from the payment evidence. The intent is the charge: what's owed, in which token, on which network, before what deadline, and to whom. The evidence is a transaction with the right transfer, amount and destination. Your app connects the two.
Deterministic payment addresses make this practical. Instead of reusing one wallet and matching incoming transfers by amount or memo, Klap predicts a unique address for every charge, derived from the charge's own ID, before it's even funded. Each address traces back to a specific order, and your checkout needs no wallet-management logic.
const charge = await klap.charges.create({amount: 49.0,acceptedPayments: [{ token: 'USDC', network: 'base' }],expiresIn: 3600,externalRef: `order_${orderId}`,})await orders.markAwaitingPayment(orderId, charge.id)
Your server creates the charge, stores charge.id against your own order ID, and treats anything the client renders as display information only. Fulfillment happens on the server, after a signed webhook, an SSE event or an SDK call reports a terminal state. A closed wallet popup never triggers it.
The checkout itself can still be entirely yours. Klap's checkout-kit is headless on purpose. It hands your frontend the charge address and the exact amount. You render them in your own design system instead of redirecting to someone else's page.
Payment status is not a boolean
An on-chain payment has a lifecycle, and treating it as a single paid: true field is how teams end up with brittle fulfillment code. A charge moves through pending, partially_paid, confirmed, expired or underpaid. Settlement, the payout reaching your recipients, is tracked as its own state: pending, completed or failed. A confirmed payment and a completed settlement are two different facts, reported separately on purpose.
Use real-time events for speed and an idempotent backend for correctness. Server-sent events suit an open checkout that must update the instant a wallet broadcasts a transaction. Webhooks suit the customer who already closed the tab. Both paths should lead to the same order transition. Both can be retried too, so store the webhook delivery ID and reject anything you've already processed. Fulfillment has to be safe to run twice. Retries are normal behavior, and you shouldn't leave them as an exception to patch later.
Self-custody doesn't make key management optional
Here's the trade-off, stated plainly: if your business controls the receiving wallet, your business controls its own security model. A processor can't lose access to funds it never held, but it also can't recover a compromised private key or reverse a mistaken transfer for you. Neither can Klap. That responsibility stays with you.
For most teams, the answer is a defined operational setup, and a laptop wallet or a seed phrase in a password manager won't cut it. Use separate hot wallets for routine settlement and multisig for treasury. Document who can sign, and test your incident process. How strict to be depends on how much money moves and who may move it.
Keep receiving and treasury movement separate where you can. A receiving address only handles predictable inbound activity. Sweeping and long-term storage can follow stricter rules. That limits the blast radius of a single compromised credential and makes accounting easier to reason about.
You also need to own recipient configuration the same way you'd own production code. Klap's split recipients are registered once, by address. Every charge afterward references them by ID. A compromised checkout request can't smuggle in a new destination address. A compromised deployment environment that changes which recipient ID your app sends absolutely can. Configuration changes deserve the same review and audit trail as a pull request.
Recipient splits happen at settlement, not after it
Marketplaces and creator platforms usually pay more than one party. A custodial processor collects the funds first, updates an internal balance, then pays everyone out later. That adds payout work and leaves the money with the intermediary in the meantime.
Klap's splits define the destinations before payment happens, up to five recipients per charge, each referenced by ID with a percentage of the charge:
const creator = await klap.recipients.create({ address: creatorWallet, label: 'creator' })const charge = await klap.charges.create({amount: 25.0,acceptedPayments: [{ token: 'USDC', network: 'base' }],expiresIn: 1800,splitRecipients: [{ recipientId: creator.id, percent: 85, label: 'creator' },],})
The platform's 15% is not listed: whatever the recipients don't take settles to the payout address on your API key. Once the customer pays, settlement routes according to that configuration, and no pooled balance sits in the middle waiting to be distributed later.
That has a real cost: refunds get harder once recipients have been paid. Klap's API has no refund endpoint for a standard split charge, only for a charge created with escrow, where funds sit in a predictable, verifiable holding contract until you release or refund them. That's the direct-settlement model told honestly. Once funds reach the recipients you configured, reversing the payment is a business decision your team makes, not an API call you fire and forget.
Build for observability, not blind trust
The strongest argument for self-custody isn't that vendors are untrustworthy. Payment systems should need as little of your trust as possible.
Your team should be able to inspect the charge, its derived address and the recipients it settled to. Finding out where a customer's payment went shouldn't take a support ticket. When something looks wrong, verify it directly against the chain rather than trusting a dashboard's word for it.
Klap is built around that model. You create the charge and watch the event. Then you verify the state, and funds settle directly to addresses you control. The infrastructure detects and routes. It never takes custody of merchant or customer funds.