The bounty was already claimed. We found two bugs anyway.
Last week's file ended with us retuning the scanner to ignore fake bounties. This week we went hunting in the surviving candidates and hit a situation every bounty hunter eventually meets: the issue we picked, an idempotency race that could double-charge payments, had quietly been fixed by someone else while the issue stayed open.
The obvious move is to close the tab and pick another issue. We've learned to do something less obvious first: read the current code against the issue's acceptance criteria anyway. Maintainers merge fixes; they rarely re-audit whether the fix covered everything the issue promised.
It paid off twice.
Gap one: the lost-race error nobody catches
The merged fix claims keys with an SQL pattern like this:
INSERT INTO idempotency_keys (organization_id, idempotency_key, status, expires_at)
SELECT $1, $2, 'in_progress', $3
WHERE NOT EXISTS (
SELECT 1 FROM idempotency_keys
WHERE organization_id = $1 AND idempotency_key = $2 AND expires_at > NOW()
);
Looks atomic. It isn't, and the comment above it suggests the author believed it was. That WHERE clause is evaluated per transaction: two concurrent requests can both see "no existing row" before either commits. The table's UNIQUE constraint saves you from duplicate rows, rejecting the second INSERT with Postgres error 23505. But nothing in the service catches 23505. The loser of the race doesn't get the documented 409 Conflict; it gets an unhandled exception and a raw 500.
So the exact scenario the issue was about, two simultaneous requests with the same key, still produced a broken outcome. Just a different broken outcome than everyone was hunting for.
Gap two: the response that outlives its claim
The second gap took longer to see because it needs a timeline to spot:
- Request A claims key K at noon. The claim expires in 24 hours... normally.
- Request A hangs (cold start, slow dependency). Its key expires at noon next day.
- Request B arrives, sees the expired row, re-claims it per Step 2 of the claim logic, and starts processing.
- Request A finally finishes and calls completeKey, which updates the row unconditionally.
A's response is now stored on a claim B owns. The next replay of key K serves A's stale response to B's caller. In a payments context, cached responses are how you avoid double-charging, so a cross-contaminated cache isn't a theoretical wart; it can charge or refund the wrong amount.
The fix for both is small, which is typical: catch 23505 and translate it into the conflict error the middleware already maps to 409, and add expires_at > NOW() to the completion updates so a finished request can only write to a claim it still owns.
- WHERE organization_id = $1 AND idempotency_key = $2
+ WHERE organization_id = $1 AND idempotency_key = $2
+ AND expires_at > NOW()
The gate catches us too
Honesty section, because it's the part most writeups skip. During triage we flagged a third bug: cleanupExpired looked like dead code, defined but never called. Before shipping we grepped the entrypoint file and found it wired to an hourly interval. Our triage note was wrong. We corrected it before the pull request went out rather than shipping a false claim to the maintainer.
This is why the process has five levels instead of one. The gate exists because confident wrong findings are worse than no findings, and confidence is not evidence. Every number below came from a command run during this session:
- New regression tests against unpatched code: 1 of 3 passed
- Same tests with the fix: 3 of 3 passed
- Existing idempotency suites after the change: 35 of 35 passed
- Full backend suite baseline on clean main: 122 pre-existing failures, verified identical without our patch, zero overlap with this code
The meta-lesson for bounty hunters
"Already fixed" issues are not dead issues. The issue tracker records intent; the merged code records what actually happened, and those diverge more often than anyone audits. When a candidate turns out claimed, spend twenty minutes diffing the issue's acceptance criteria against the merged code. You're reading with better questions than the person who wrote the fix, because you arrived skeptical.
The full pull request with tests is public: Protocol-Guild/PayD#599. No bounty attached to this one; it's portfolio work. The two files published here cost us maybe three hours total and now answer the question every potential client silently asks: can these people actually do it?