Summary
During a real end-to-end payment test of fetch, an L402 payment threw Nip47ReplyTimeoutError: reply timeout and the CLI exited non-zero with {"error":"reply timeout..."} — but the payment settled anyway a few seconds later. Because the failure looked clean (non-zero exit, balance still unchanged at check time, lookup-invoice returned NOT_FOUND), a retry was issued and produced a second real charge for the same invoice. Net result: the service was paid twice and no content was returned either time.
Evidence
Two identical settled transactions for one fetch, 13 sats each (10 + 3 fee):
1783433306 10 + 3 fee = 13 | L402 Apps — Get APIs directory
1783433187 10 + 3 fee = 13 | L402 Apps — Get APIs directory
CLI output on each attempt:
$ npx @getalby/cli fetch "https://www.l402apps.com/api/apis" -X GET --max-amount 100 --currency BTC --unit sats --network lightning
Failed to request pay_invoice Nip47ReplyTimeoutError: reply timeout: event b455443377...
{"error":"reply timeout: event b455443377b65a128c92fb30cf296977b4e4bff5a97797d7c0e325141dfa5472"}
Likely root cause
The SDK's default NWC pay_invoice reply timeout is shorter than this wallet's actual pay_invoice round-trip. On timeout the CLI reports failure and exits, but the payment keeps routing and settles asynchronously. There is no idempotency or post-timeout reconciliation, so:
- The caller sees a failure for a payment that actually succeeded.
- The purchased content is lost (the L402 flow errors out before re-presenting the macaroon + preimage, and surfaces no reusable credential).
- A retry — which is a natural reaction, and which the payments skill's "Common Issues" table explicitly recommends ("402 payment failed → Retry") — double-pays.
Notably the timeout was specific to pay_invoice: get-balance, list-transactions, lookup-invoice, and parse-invoice all replied instantly against the same wallet/relay, so this is the long-running pay call outrunning the reply timeout, not a dead relay. Observed on the hub wallet; reproduced once (L402). x402 and MPP paid cleanly on the first attempt in the same session.
Impact
Silent, real money loss. An agent (or human) that retries on a fetch/pay failure can be charged multiple times for a single request, with nothing to show for it.
Suggested directions (for discussion)
- Raise the
pay_invoice NWC reply timeout well above expected settlement latency (and/or make it configurable).
- On a
pay_invoice reply timeout, reconcile before reporting failure: look up the invoice/payment status and, if it settled, treat it as success (and complete the L402 flow so the content isn't lost) rather than returning a bare error.
- Add idempotency so a retried
fetch for the same invoice can't pay twice.
- Separately: the payments-skill "retry on payment failure" guidance should warn that a
pay_invoice timeout may have still settled — check balance / lookup-invoice before retrying. (Skill-side; tracked with the CLI fix.)
Context
Found while consumer-testing the CLI end-to-end (discover → fetch across L402/x402/MPP) alongside #49. This is in the fetch payment flow and is independent of the discover/bridge work in that PR.
Summary
During a real end-to-end payment test of
fetch, an L402 payment threwNip47ReplyTimeoutError: reply timeoutand the CLI exited non-zero with{"error":"reply timeout..."}— but the payment settled anyway a few seconds later. Because the failure looked clean (non-zero exit, balance still unchanged at check time,lookup-invoicereturnedNOT_FOUND), a retry was issued and produced a second real charge for the same invoice. Net result: the service was paid twice and no content was returned either time.Evidence
Two identical settled transactions for one fetch, 13 sats each (10 + 3 fee):
CLI output on each attempt:
Likely root cause
The SDK's default NWC
pay_invoicereply timeout is shorter than this wallet's actualpay_invoiceround-trip. On timeout the CLI reports failure and exits, but the payment keeps routing and settles asynchronously. There is no idempotency or post-timeout reconciliation, so:Notably the timeout was specific to
pay_invoice:get-balance,list-transactions,lookup-invoice, andparse-invoiceall replied instantly against the same wallet/relay, so this is the long-running pay call outrunning the reply timeout, not a dead relay. Observed on thehubwallet; reproduced once (L402). x402 and MPP paid cleanly on the first attempt in the same session.Impact
Silent, real money loss. An agent (or human) that retries on a
fetch/pay failure can be charged multiple times for a single request, with nothing to show for it.Suggested directions (for discussion)
pay_invoiceNWC reply timeout well above expected settlement latency (and/or make it configurable).pay_invoicereply timeout, reconcile before reporting failure: look up the invoice/payment status and, if it settled, treat it as success (and complete the L402 flow so the content isn't lost) rather than returning a bare error.fetchfor the same invoice can't pay twice.pay_invoicetimeout may have still settled — check balance /lookup-invoicebefore retrying. (Skill-side; tracked with the CLI fix.)Context
Found while consumer-testing the CLI end-to-end (discover → fetch across L402/x402/MPP) alongside #49. This is in the fetch payment flow and is independent of the discover/bridge work in that PR.