2026-09-07

How to Verify Crypto Payments the Right Way

Never treat a client-supplied transaction hash as proof of payment. A hash can point to a failed transaction, the wrong asset, the wrong chain, a different recipient, or an amount that doesn't cover the charge. Verify the settled transfer against the charge conditions on your backend.

#background: #09090b #fill: #18181b #stroke: #e4e4e7 #font: "Helvetica Neue", Helvetica, Arial, sans-serif #fontSize: 14 #lineWidth: 1.5 #edges: rounded #bendSize: 0.3 #padding: 12 #spacing: 40 #direction: right [Payer] -> [Split address] [Split address] -> [Merchant] [Split address] --> [Klappay] Payer Split address Merchant Klappay

Design payment status as an event-driven system

Polling a block explorer from a request handler is a demo, not infrastructure. Customers approve wallet transactions after the browser closes, on a refreshed page, or on a flaky connection — so settlement has to reach you asynchronously. Klappay's live charge stream needs your secret API key, so it's your backend that holds it open and relays progress to the checkout page over its own channel — from "Waiting for payment" to "Confirming…" to "Confirmed" without a refresh. Good for the experience, but it's not your source of truth.

Fulfillment belongs on webhooks or an SDK event stream. Verify signatures, persist the event ID, and make processing idempotent — the same settlement event will arrive twice sooner or later, and it should still produce exactly one license, shipment, credit, or activation.

Keep the state model small: pending, partially_paid, confirmed, expired, underpaid. Settlement — whether the payout actually reached your wallet — is tracked separately (pending/completed/failed), because a charge can be confirmed while settlement is still in flight or fails outright. And keep business status separate from both: a charge can be paid and settled while the order is still unfulfilled because inventory, fraud review, or provisioning hasn't cleared. Collapsing those is how edge cases turn into support tickets.

Validate what wallets won't tell you

Before writing settlement logic, lock down four things:

  • Decimals. Pass amounts as decimal numbers — the SDK handles token precision internally. $4.90 is simply 4.90. Avoid floating-point arithmetic in your own business logic when calculating totals or splits.
  • Chain and contract. Require the expected Base network and the canonical USDC contract address, not merely a token symbol. Symbols can be copied, and users can send the wrong asset to a valid address.
  • Recipient address. Verify that the transfer destination matches the charge address your system generated, not an address the client supplied.
  • Amount policy. Exact amount validation is clean for one-time purchases. For donations, usage top-ups, and some invoice flows, accepting amounts at or above a minimum may be better. Make the rule explicit.
const merchant = await klap.recipients.create({
address: merchantTreasury,
label: 'merchant',
})
const platform = await klap.recipients.create({
address: platformTreasury,
label: 'platform',
})
const charge = await klap.charges.create({
amount: 4.90,
acceptedPayments: [{ token: 'USDC', network: 'base' }],
expiresIn: 3600,
externalRef: `order_${order.id}`,
splitRecipients: [
{ recipientId: merchant.id, percent: 90, label: 'merchant' },
{ recipientId: platform.id, percent: 10, label: 'platform' },
],
})

One more thing on subscriptions: an on-chain transfer isn't recurring. Wallets don't approve future payments on their own without a separate authorization design, so build each collection explicitly unless your product has a permission model that covers it.

Confirmation depth isn't something you configure — Klappay fixes a minimum per network, sized to that chain's own reorg risk, and a charge only reaches confirmed once a transfer clears it. What's actually your call is what you do at confirmed versus settled: a cheap digital good can ship the moment a charge confirms, while a high-value physical order can reasonably wait for charge.settled — the payout landing in your own wallet, a distinct and slightly later step.

Keep checkout under your control

Hosted checkout pages and drop-in widgets win when time-to-first-payment is the only metric. They start to hurt the moment you need your own pricing logic, feature gating, analytics, A/B tests, or post-payment provisioning.

An API-first design lets your application own the UI while payment infrastructure handles charge creation, address derivation, monitoring, and event delivery. Klappay is built around that boundary: your team controls the checkout and settlement destination, while the API detects and routes payments without taking custody.

That matters as soon as payment data becomes product data — showing status inside an existing modal, binding a charge to a workspace, attaching it to a creator campaign, feeding your entitlement engine. A processor-owned page looks like a neutral implementation detail until it becomes a product constraint.

Before you touch real money, run the unhappy path in a sandbox: delayed payments, duplicate events, expiration, a wrong amount, a payment that lands after the order was canceled. Validate payloads at the boundary with schemas, and log the charge ID, chain transaction hash, event ID, and internal order ID together. When someone says they paid and didn't get access, those four fields should answer it without guesswork.

The bar worth holding: a customer pays USDC on Base in seconds, and your team can prove exactly what happened without asking a processor for permission to inspect or move its own money.

Ready to integrate? Start at klappay.com.