Stuck Deposits
Three clients pay. The service that's meant to tell you about payments tells you about one on time, one late and one never. Trust it, and you chase a client who already paid. Check the payments yourself and you catch it.
01Try it
Three clients pay three bills, for real. The service that reports payments is simulated: it reports one on time, one late, and forgets one.
- Client Anothing yet
- Client Bnothing yet
- Client Cnothing yet
- Client A—
- Client B—
- Client C—
- Client A—
- Client B—
- Client C—
Three bills for 5,800 USDC. All three clients pay at once. Then see what the payment service says, and what really happened.
15 tries an hour each. Every payment is real and uses up the lab's test money.
Why check for yourself
Messages go missing
The service tries to tell you, but messages get lost. Nothing breaks on your side. You just never find out.
The payment record stays
The payment itself is on a public record that anyone can check. Looking at it takes one quick step and needs nobody to remember to tell you.
A gap is a question
When the service and the record disagree, don't chase the client. Mark it paid from the record and ask the service what happened.
The code
What runs behind the buttons, in one sentence. The code itself is one click away.
The bad version only marks a bill paid when the service says so. The good version checks the payments itself every few seconds and asks the service about anything it missed.
Show the code anyway
app.post("/webhooks/deposits", async (req) => { const e = verify(req); if (e.type === "deposit.confirmed") { await invoices.markPaid(e.invoiceId, e.amount); }});// and nothing else ever marks an invoice paidconst onChain = await getLogs(USDC.Transfer, { to: openInvoiceAddresses, fromBlock });for (const inv of openInvoices) { const tx = onChain.find(l => l.to === inv.address); if (!tx || confirmations(tx) < N) continue; await invoices.markPaid(inv.id, tx.value, { source: "chain", tx: tx.hash }); if (!webhooks.has(tx.hash) && blocksSince(tx) > GRACE) { await tickets.open("provider", { invoice: inv.id, tx: tx.hash }); }}Still curious?
A few more questions people ask. Open what you need.
What's real here?
The payments are real, and so is the careful side's check: it reads them from the public record itself. The payment service and its messages are simulated.
Why use a payment service at all?
Watching payments across many networks is real work, and these services usually do it well. Keep them. Just don't treat their message as the only proof.
What about the late one?
B shows the other problem: the careless side gets it right in the end, but first it chases a client who already paid. The careful side marked B paid straight away and only noted that the service was slow.
Not sure this could happen to you?
That's what the call is for. Tell me how your product works, and I'll tell you what applies to you and what I'd check first.