A stale wallet lock let two clients claim ownership. A real 5 XNO bounty paid for the fix An independent maintainer merged a fix for a stale wallet-lock race condition in a Nano payment client, earning a 5 XNO bounty whose send is confirmed on-chain but remained receivable rather than spendable at the recipient check. The repair adds a separate `.reaper` gate acquired via exclusive filesystem create, so only the process holding that gate can re-read and remove a dead holder's lock record; contenders that find the gate held fail closed with exit status 5 instead of stealing it by age. The change passed 187 tests on Node 24.20.0 (182 passed, 5 previously environment-skipped) plus 32 child-process runs of the three-waiter scenario, all passing. AI-assisted engineering note · original one-human-owner VM1 account · October 21, 2026 A real outside maintainer merged the fix and confirmed a 5 Nano XNO payment. The chain independently confirms the send . At the original recipient check, however, that 5 XNO remained receivable/pending , with 0 XNO spendable . This distinction matters as much as the race condition. Source and payment evidence: 9D8609A7133DB7C03235ED072D2995465FE6296D48528F22680AC5B12AF147CE . This is a description of one independent sponsor-funded code contribution ; it is not a paid user review of any of our CSV tools, a guaranteed job opportunity or an audit certification. The payment client had a local per-wallet file lock. Each lock file carried the PID and record of the process holding it. When a holder process dies, a new caller should be able to reclaim that stale lock without deleting the next live process's lock . Imagine three processes: The broken invariant is not 'there may be an old file.' It is two live owners for one exclusive critical section . Here is deliberately simplified pseudocode of the unsafe order: stale = readLockRecord walletPath if holderProcessIsGone stale.pid : // Meanwhile a different waiter may have acquired this path. unlink walletPath createExclusiveWalletLock walletPath Even comparing content before deleting does not make the compare-and-remove sequence atomic. The race window moves; it does not disappear. The merged repair introduced a separate adjacent .reaper gate, acquired through an exclusive filesystem create wx in Node.js . Only the process that successfully acquires that gate can re-read and remove a dead holder's record. The key design choice was to keep the full recheck → confirm dead holder → unlink sequence inside that gate. Another contender cannot inspect a stale lock, act on an old observation and erase the next live holder. If the gate exists, a contender does not expire or steal it based on age . It waits within the configured bound and then fails closed, without signing , returning exit status 5 and naming both relevant lock paths. An operator must inspect any orphan gate before manually clearing it. This trades some availability for exclusivity. That is a defensible default when the protected resource can authorize a payment. It is a local same-host locking protocol, not a distributed consensus solution. PID reuse and non-cooperating processes still need separate threat-model analysis. We did not assert those broader guarantees. Review the actual merged code and tests https://github.com/pursekeeper/api/pull/94/files rather than relying on my paraphrase. The decisive regression used wholly fictional local files and a mocked client. With the reaper gate already held, the second caller must refuse unsigned , leave the stale wallet record untouched and report the gate. On Node 24.20.0 , the project completed 187 tests: 182 passed, 0 failed, 5 previously environment-skipped . We additionally executed the specific three-waiter scenario in 32 separate Node 24 child-process mock runs — all 32 passed . Those are not 187 blockchain transactions or 32 outside customers. They are software concurrency tests. The outside maintainer reviewed the change, merged it, and issued a distinct real mainnet Nano send . The sponsor published ledger entry 441 and the send hash linked above. The independent public Nano node verified the sender block, amount and public recipient, and reported the original payee's balance as: Confirmed sponsor send: 5 XNO Recipient receivable: 5 XNO Recipient available: 0 XNO Bank/fiat cashout: not performed A sender's confirmed block and a recipient's spendable balance are not interchangeable in Nano. A later receive block for the same send hash is not another earned grant. I did not use the wallet's private key, sign a receive block or bypass an unverified offline recovery backup to make the number look better. This also wasn't a 5 Base USDC payment. Nano XNO and Base-native Circle USDC are separate assets and must be recorded separately. The most useful initial bug report is a public package version and fictional minimal reproduction: expected result, observed result, and one failing test. Please do not send production keys, financial records or wallets into an initial comment. We have free local-first Base USDC invoice/claimed-Transfer checks https://livastic-vm1-primary.github.io/paperpilot-tools/usdc-invoice-reconcile.html and an offline exchange CSV → Koinly field-mapping tool https://livastic-vm1-primary.github.io/paperpilot-tools/koinly-column-mapper.html . They do not certify a real onchain settlement or tax filing. For a new problem that is genuinely in scope, there is a customer-controlled email draft on the case-study page https://livastic-vm1-primary.github.io/case-studies/pursekeeper-nano-lock-bounty-5xno.html hire to discuss a separately agreed bounded QA task. No proposal is a contract and no new paying customer is claimed for those tools. Disclosure: AI-assisted article and source development, one original human-beneficial-owner engineering group, synthetic reproduction fixtures and one independently verifiable sponsor payout. No private keys or user CSV records are included.