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. Klap's escrow charges hold that line. Funds wait, but only the party who's supposed to control them can ever 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' }],
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 and 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, such as an escrow agent, a marketplace's dispute desk or a milestone approver who isn't the seller.


Klap 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 Klap has no key with any authority over that Safe. It never did, and it can't, because the Safe's only owner is the releaser.

What Klap's API does is take over the tedious part of producing that signature. prepareRelease() and prepareRefund() return the exact Safe transaction (destination, token, amount, Safe nonce) with its EIP-712 typed data, so the releaser signs it with any EIP-712 signer instead of rebuilding it by hand. Klap then submits the signed transaction on their behalf:

import { toSignableEscrowTypedData } from '@klappay/types'
const prepared = await klap.charges.prepareRelease(charge.id)
const signature = await releaser.signTypedData(toSignableEscrowTypedData(prepared.typedData))
const released = await klap.charges.release(charge.id, { signature })

Refunds work the same way, with prepareRefund() and refund(). Check prepared.destination and prepared.amount against your own records before signing. The signature is only valid while the escrow's balance is unchanged: if more funds arrive first, the call fails with escrow_invalid_signature, and you prepare and sign again. The same steps are in the CLI (klap charges prepare-release, then klap charges release --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 the sender of the most recently credited transfer instead. That isn't always the buyer: it can be an exchange's withdrawal address, or only the last of several wallets that paid, which is why you check destination before signing. 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. The check is the contract's, not Klap's application logic.

Klap's own backend account does submit the transaction and pays the gas for it. It's a relayer, not a signer. Paying gas for a transaction you can't authorize is a different role from holding the keys. It's 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 sitting in the Safe. There's nothing to reconcile afterward and nothing for you 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 has 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.

This is the same guarantee as the rest of Klap, that nobody except the intended owner can move the money, applied to a charge that isn't ready to settle the instant it's paid.