2026-09-22

How to Add Escrow to Crypto Payments Without Taking Custody

A normal charge settles the moment payment is confirmed — funds land at the split address, distribution pays out the recipients, done. That's the right default for point-of-sale. It's the wrong default for a payment that should wait on something else first: a buyer confirming delivery, a dispute window closing, a milestone being signed off.

The word for "money that waits on a condition before it's released" is escrow, and it's usually where non-custodial architecture quietly breaks down. The moment funds need to sit somewhere pending a decision, most systems reach for a platform-controlled wallet to hold them — which is exactly the custody model the rest of the payment was designed to avoid. Klappay's escrow charges are built to hold that line: funds wait, but nobody except the party who's supposed to control them ever gains the ability to move them.


What changes when a charge is an escrow

Escrow is opt-in, set once at charge creation:

const charge = await klap.charges.create({
amount: 500.0,
externalRef: `order_${order.id}`,
acceptedPayments: [{ token: 'USDC', network: 'base' }],
splitRecipients: [{ recipientId: seller.id, percent: 100, label: 'seller' }],
escrow: { releaserAddress: sellerReleaserWallet },
})

Everything else about the charge — amount, splits, metadata, expiry — works exactly like a normal charge. The only thing escrow changes is where the money goes when it's detected, and who can move it from there.

Instead of paying into the charge's usual split address, an escrow charge routes payment into a dedicated Safe — a 1-of-1 smart-contract wallet, deployed deterministically per charge, owned by exactly one address: releaserAddress. If you omit it, it defaults to your own API key's payout address, which covers the common case of a merchant releasing their own charge. You'd pass something different only when the releaser is a distinct operational party — an escrow agent, a marketplace's dispute desk, a milestone approver who isn't the seller themselves.


Klappay can build the transaction. It can't sign it.

This is the part that makes escrow compatible with "we never touch the funds": releasing or refunding an escrow requires a Safe transaction signature from releaserAddress, and Klappay has no key with any authority over that Safe. It never did, and it structurally can't — the Safe's only owner is the releaser.

What Klappay's API does is remove the tedious part of producing that signature. It computes the exact transaction — destination address, live on-chain balance — so the releaser doesn't have to reconstruct it by hand, then submits the already-signed transaction on their behalf:

const released = await klap.charges.release(charge.id, { signature })
const refunded = await klap.charges.refund(charge.id, { signature })

release() sends the escrow's entire current balance to the charge's normal split address — from there, the same distribution mechanism that handles every other charge takes over and pays out the recipients. refund() sends the balance back to whichever address actually funded the charge instead. Both require the identical thing: a valid Safe signature from releaserAddress, verified by the Safe contract itself on-chain before anything moves. If the signature doesn't authorize exactly that transfer, the Safe rejects it — not Klappay's application logic, the contract.

Klappay's own backend account does submit the transaction and pays the gas for it. It's a relayer, not a signer. Sponsoring gas for a transaction you cannot construct the authorization for is a meaningfully different role than holding the keys — the same distinction that makes direct settlement non-custodial in the first place, applied to a second product surface.


The amount is read live, not fixed in advance

Neither release() nor refund() needs a pre-agreed amount, because the transaction is built against the escrow's actual balance at the moment of the call, not a number decided when the charge was created. If a customer underpaid, overpaid, or the exchange rate moved between charge creation and release, the transaction always matches whatever is actually sitting in the Safe — nothing to reconcile after the fact, and nothing you need to pass yourself.

An escrow can only be released or refunded once. The two are mutually exclusive and both are terminal: a charge that's already been released can't later be refunded, and vice versa. That's enforced server-side before anything reaches the chain, so a retried request after a timeout can't accidentally attempt both.


Where this fits, and where it doesn't yet

Escrow charges are EVM-only — the underlying Safe contract has no Tron deployment, and charge creation is rejected up front if you try to combine escrow with a Tron-only payment method. Today, an escrow charge also needs to settle within a single token/network pair; a charge that receives payment across more than one isn't yet supported for release or refund. And because holding funds pending a condition is a different risk profile than settling instantly, escrow charges carry a surcharge on top of the normal fee.

Reach for it when a payment genuinely needs to wait on something: a marketplace holding a buyer's payment until delivery is confirmed, a freelance platform releasing a milestone only after both sides sign off, a deposit that needs to be refundable up to a cutoff date. Skip it for ordinary checkout — a normal charge settles faster, has no surcharge, and doesn't need anyone to hold and later produce a signature.

Escrow, done this way, isn't a special case bolted onto a non-custodial system. It's the same guarantee — nobody except the intended owner can move the money — applied to a charge that isn't ready to settle the instant it's paid.