PayPal refunded $48.00. Your books still say paid.
No error anywhere.
Doorbell is the tool that looks. It finds the PayPal A message PayPal posts to your server's address when something happens, such as a refund. your shop never heard or never saved, has an AI point at the changed line of your code behind it, rings them again, and tells you exactly what it still cannot prove.
Order #2 at Maple Mug, a fictional coffee shop: a “Gift set: three coffees and a mug”. The Money sent back to the buyer after a capture. went through at PayPal. The shop's own books never heard about it, so the order still reads as paid. Nothing crashed and nothing logged an error, so nobody is looking.
Doorbell checked the shop's 3 The moment PayPal takes the buyer's money for an order. One capture is one row on this page.: the payments PayPal took, one per order. 2 refunds ($96.00) are missing from your books: the shop answered 200 but did not save them.
The chapters below follow order #2 from PayPal to your books. The other A row where the shop's books and PayPal's own answer disagree., order #3, comes back in Ring again.
Each hop said OK. The books never changed.
PayPal made the refund PayPal's record that something happened, such as a payment or a refund. It is what a webhook carries.. About 9 seconds later Doorbell heard it and checked its A mark PayPal puts on each delivery so your server can tell it really came from PayPal. with PayPal's verify-webhook-signature call. Your shop got the One try by PayPal to post a webhook to your address. A failed delivery can be tried again. and answered 200 OK in 321 milliseconds. A 200 is the polite answer, “got it, thanks”, and it tells PayPal to stop trying. So nobody tried again.
Then nothing: the books were never written. Doorbell's check at 10:42:58 looked each capture up at PayPal, read PayPal's PayPal's own list of the events it made for your app. In the Sandbox an event shows up in it 80 to 163 s late., and found the gap.
The handler says OK before it saves the refund, so the save can be lost
The AI read what changed in the shop's handler and proposed two lines as the cause. Its category is work after acknowledgement. In plain words: a changed line answers PayPal before the booking work is finished, so that work can be lost after the 200.
The AI only proposes. Two checks run in plain code, with no model involved, and both pass.
Same messages, two builds. Only one saves them.
To test it, Doorbell replays the two real signed events against the shop's good build and its bad build. Both answer 200, 200, so from the outside they look the same. Only the books differ.
On the same events, the good build books the refund and the bad build does not. That is a tested result for the whole change, not a proof about any one line.
Fixing the code does not rewrite the books. Only another delivery of the same event can.
The shop's developer deploys the last good build. The shop now runs good.ts instead of ack-drop.ts, planted by us. A refund that arrives from now on is booked.
But the refund that already came is not booked: PayPal got its 200 and stopped trying.
Doorbell looked again at 10:43:12: the books still differ from PayPal on two captures.
Claude also proposed a patch for the cited lines. Plain code checked it first. Then it was run on real signed events and compared with the shop's last good build and with PayPal's own answer.
Proposed by Claude · not deployed in this demo
// Maple Mug webhook handler. PayPal posts here when a payment is captured or refunded.
import { bookingOf, rank } from "../lib/shop/events.ts";
import type { Handler } from "../lib/shop/types.ts";
lines of the file that the patch does not change
if ((await ctx.verify(event)) !== "SUCCESS") return { status: 401 };
const booking = bookingOf(event);
if (!booking) return { status: 200 };
// Never move an order backwards: a late "paid" must not undo a "refunded".
const current = await ctx.books.get(booking.captureId);
if (current && rank(current.state) > rank(booking.state)) return { status: 200 };
await ctx.books.set(booking.captureId, { state: booking.state, amount: booking.amount, eid: event.id, at: ctx.now() });
// Acknowledge first, book later.
queue.push(event);
return { status: 200 };
};
Highlighted: the lines the AI cited as the cause.
Behaves like the shop's last good build and PayPal's answer on these 15 inputs.The 15 inputs are 5 deliveries from 3 orders, run three ways: in arrival order, reversed, and with one amount changed.
What the code checked
- The edits name changed lines of the diff, and replace or delete only lines the cause cited.
- The patched file parses.
- It uses only the imports and names a handler may use.
- Lines of the bad deploy that the cause did not cite are unchanged, byte for byte.
- The edits stay inside the bad deploy's changed hunks.
What the replay compared
- The events in the order they arrived: same answers and same books as the good build. (5 deliveries)
- The same events, last one first: same answers and same books as the good build. (5 deliveries)
- One amount in each body changed: 401 and nothing booked, like the good build. (5 deliveries)
- The books equal PayPal's own answer for the order (GET capture). (6 comparisons)
The same patch with its signature check removed (written by us) fails the tampered control and passes the other three.
Compared with the shop's last good build, the patch still has: a comment that may no longer match the code; “queue”, a value nothing reads. The checks do not look at comments or leftover code.
The demo does not run this patch. It runs good.ts, the shop's last good build, which the patch matches on these 15 inputs.
The comparison ran at 10:43:12 UTC, on this run's own signed events. The edits are a stored recording from claude-sonnet-5-5; this page makes no model call.
Ask PayPal to send it again.
Doorbell can ask PayPal's Asking PayPal to deliver the same event to your webhook one more time. call to send the same event again, but only while the event is in PayPal's events list. Then it checks that the event arrived and that the books now match PayPal. Ring again rings each open gap: order #2, then order #3.
A replay of the real resend from Oct 20, 2026, sped up. Nothing is sent to PayPal.
Each wait was cut to about a second: order #2's 66 s and 66 s between resends, the 19 s before order #3's first resend.
2 of 3 captures did not match PayPal.
$96.00 across 2 refunds not in books.
First, what Doorbell cannot prove
- 3 rows are inside PayPal's event-list lag (median about 80 s, up to 163 s measured): an event missing from the list proves nothing yet.
- Orders that were approved but never captured cannot be counted (PayPal has no list API for them).
- An event that is not in PayPal's event list cannot be shown to have existed.
- Transaction Search lags about 97-128 minutes, so the last 2 hours of transactions were not used.
- The proposed patch's comments and leftover code are not checked.
- This certificate is not signed: anyone could edit it.
What it did check
- Each of the 3 captures, looked up again at PayPal and compared with the books.
- The AI's cited line is a changed line, and its category fits what the shop answered.
- A replay of the real signed events on both builds showed the difference the AI's category predicts.
- Claude's proposed patch passed 5 code checks. Replayed on 15 signed events, it behaved like the shop's last good build and PayPal's answer.
- The resend has not run yet, so the gaps are still open.
Open the owner view, a printable page in plain sentences.
PayPal refunded $48.00. Your books still say paid.
PayPal says
Refunded
$48.00
Your books say
Paid
$48.00
Order #2, gift set. No error anywhere.
Doorbell finds the PayPal A message PayPal posts to your server's address when something happens, such as a refund. your shop never heard or never saved,has an AI point at the changed line of your code behind it,rings them again,and tells you exactly what it still cannot prove.
More
Doorbell is the tool that looks.
Order #2 at Maple Mug, a fictional coffee shop: a “Gift set: three coffees and a mug”. The Money sent back to the buyer after a capture. went through at PayPal. The shop's own books never heard about it, so the order still reads as paid. Nothing crashed and nothing logged an error, so nobody is looking.
Doorbell checked the shop's 3 The moment PayPal takes the buyer's money for an order. One capture is one row on this page.: the payments PayPal took, one per order. 2 refunds ($96.00) are missing from your books: the shop answered 200 but did not save them.
The chapters below follow order #2 from PayPal to your books. The other A row where the shop's books and PayPal's own answer disagree., order #3, comes back in Ring again.
Each hop said OK. The books never changed.
A 200 tells PayPal to stop trying, so nobody tried again.
- 10:42:28PayPal created the refund PayPal's record that something happened, such as a payment or a refund. It is what a webhook carries.
- 10:42:37Doorbell heard it, A mark PayPal puts on each delivery so your server can tell it really came from PayPal. checked
- 10:42:37.982Your shop answered 200 OK, 321 ms later
- 10:42:58Doorbell's check: books never changed
More
PayPal made the refund event. About 9 seconds later Doorbell heard it and checked its signature with PayPal's verify-webhook-signature call. Your shop got the One try by PayPal to post a webhook to your address. A failed delivery can be tried again. and answered 200 OK in 321 milliseconds. A 200 is the polite answer, “got it, thanks”.
Then nothing: the books were never written. Doorbell's check at 10:42:58 looked each capture up at PayPal, read PayPal's PayPal's own list of the events it made for your app. In the Sandbox an event shows up in it 80 to 163 s late., and found the gap.
The handler says OK before it saves the refund, so the save can be lost
The AI only proposes; two checks in plain code, with no model involved, both pass.
The AI proposed this change in handlers/ack-drop.ts, planted by us
− await ctx.books.set(booking.captureId, { state: booking.state, amount: booking.amount, eid: event.id, at: ctx.now() });
- The cited line is a changed code line
- The category fits the symptom: the shop answered 200 OK, but its books do not match PayPal
AI receipt: claude-sonnet-5-5 picked 1 of 8 categories (work after acknowledgement) and cited 2 changed lines. The answer is a stored recording; this page makes no model call. On a held-out set sealed before the run (2026-10-26) it named the right category in 34 of 35 cases; the README has the rest.
More
The AI read what changed in the shop's handler and proposed two lines as the cause. Its category is work after acknowledgement. In plain words: a changed line answers PayPal before the booking work is finished, so that work can be lost after the 200.
Same messages, two builds. Only one saves them.
To test it, Doorbell replays the two real signed events against the shop's good build and its bad build.
Good build
good.ts
Answers 200, 200
Books say
Refunded
$48.00
Bad build
ack-drop.ts, planted by us
Answers 200, 200
Books say
No entry
Confirmed by replay
More
Both answer 200, 200, so from the outside they look the same. Only the books differ.
On the same events, the good build books the refund and the bad build does not. That is a tested result for the whole change, not a proof about any one line.
Fixing the code does not rewrite the books.
Only another delivery of the same event can.
Proposed by Claude · not deployed in this demo
// Maple Mug webhook handler. PayPal posts here when a payment is captured or refunded.
import { bookingOf, rank } from "../lib/shop/events.ts";
import type { Handler } from "../lib/shop/types.ts";
lines of the file that the patch does not change
if ((await ctx.verify(event)) !== "SUCCESS") return { status: 401 };
const booking = bookingOf(event);
if (!booking) return { status: 200 };
// Never move an order backwards: a late "paid" must not undo a "refunded".
const current = await ctx.books.get(booking.captureId);
if (current && rank(current.state) > rank(booking.state)) return { status: 200 };
await ctx.books.set(booking.captureId, { state: booking.state, amount: booking.amount, eid: event.id, at: ctx.now() });
// Acknowledge first, book later.
queue.push(event);
return { status: 200 };
};
Highlighted: the lines the AI cited as the cause.
Behaves like the shop's last good build and PayPal's answer on these 15 inputs.
Books still say Paid $48.00
More
The shop's developer deploys the last good build. The shop now runs good.ts instead of ack-drop.ts, planted by us. A refund that arrives from now on is booked.
But the refund that already came is not booked: PayPal got its 200 and stopped trying.
Doorbell looked again at 10:43:12: the books still differ from PayPal on two captures.
Claude also proposed a patch for the cited lines. Plain code checked it first. Then it was run on real signed events and compared with the shop's last good build and with PayPal's own answer.
The 15 inputs are 5 deliveries from 3 orders, run three ways: in arrival order, reversed, and with one amount changed.
What the code checked
- The edits name changed lines of the diff, and replace or delete only lines the cause cited.
- The patched file parses.
- It uses only the imports and names a handler may use.
- Lines of the bad deploy that the cause did not cite are unchanged, byte for byte.
- The edits stay inside the bad deploy's changed hunks.
What the replay compared
- The events in the order they arrived: same answers and same books as the good build. (5 deliveries)
- The same events, last one first: same answers and same books as the good build. (5 deliveries)
- One amount in each body changed: 401 and nothing booked, like the good build. (5 deliveries)
- The books equal PayPal's own answer for the order (GET capture). (6 comparisons)
The same patch with its signature check removed (written by us) fails the tampered control and passes the other three.
Compared with the shop's last good build, the patch still has: a comment that may no longer match the code; “queue”, a value nothing reads. The checks do not look at comments or leftover code.
The demo does not run this patch. It runs good.ts, the shop's last good build, which the patch matches on these 15 inputs.
The comparison ran at 10:43:12 UTC, on this run's own signed events. The edits are a stored recording from claude-sonnet-5-5; this page makes no model call.
Ask PayPal to send it again.
Doorbell can ask PayPal's Asking PayPal to deliver the same event to your webhook one more time. call to send the same event again, but only while the event is in PayPal's events list.
A replay of the real resend from Oct 20, 2026, sped up. Nothing is sent to PayPal.
#1 House blend, 250 g
$14.00Tied
PayPal says Paidbooks say Paid
#2 Gift set: three coffees and a mug
$48.00Gap
PayPal says Refundedbooks say Paidbooks say Refunded
#3 Gift set: three coffees and a mug
$48.00Gap
PayPal says Refundedbooks say Paidbooks say Refunded
More
Then it checks that the event arrived and that the books now match PayPal. Ring again rings each open gap: order #2, then order #3.
Each wait was cut to about a second: order #2's 66 s and 66 s between resends, the 19 s before order #3's first resend.
2 of 3 captures did not match PayPal.
$96.00 across 2 refunds not in books.
First, what Doorbell cannot prove
- 3 rows are inside PayPal's event-list lag (median about 80 s, up to 163 s measured): an event missing from the list proves nothing yet.
- Orders that were approved but never captured cannot be counted (PayPal has no list API for them).
- An event that is not in PayPal's event list cannot be shown to have existed.
- Transaction Search lags about 97-128 minutes, so the last 2 hours of transactions were not used.
- The proposed patch's comments and leftover code are not checked.
- This certificate is not signed: anyone could edit it.
What it checked
- Each of the 3 captures, looked up again at PayPal and compared with the books.
- The AI's cited line is a changed line, and its category fits what the shop answered.
- A replay of the real signed events on both builds showed the difference the AI's category predicts.
- Claude's proposed patch passed 5 code checks. Replayed on 15 signed events, it behaved like the shop's last good build and PayPal's answer.
- The resend has not run yet, so the gaps are still open.
Open the owner view, a printable page in plain sentences.