Skip to content

fix(wallet): hold reservation on ambiguous x402 settlement (#128) - #138

Merged
VickyXAI merged 2 commits into
mainfrom
fix/reservation-ambiguous-settlement
Aug 29, 2026
Merged

fix(wallet): hold reservation on ambiguous x402 settlement (#128)#138
VickyXAI merged 2 commits into
mainfrom
fix/reservation-ambiguous-settlement

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Summary

  • postWithPayment tracks whether the signed (paid) request was dispatched; if the call then aborts / times out, the wallet hold is marked ambiguous instead of being released by the caller's finally.
  • WalletReservation keeps ambiguous amounts counted against headroom for a 30s grace window and prunes them on the next fresh on-chain balance read after that window (which already reflects a real settlement). Cap errs tight, never loose; a genuinely-absent spend self-heals.
  • Balance fetcher is injectable for tests; new test/reservation.local.mjs (3 tests) wired into npm test.

Closes #128

Test plan

  • npm test — 673/673 pass

… releasing it

An aborted or timed-out paid request may already have settled on-chain.
postWithPayment now marks the reservation ambiguous once the signed request
is dispatched; the reservation layer keeps the amount counted for a 30s
grace window and prunes it on the next fresh balance fetch. The cap can
only err tight, never loose.

Closes #128
…sent requests ambiguous

Adversarial review follow-ups on #138:
- grace window = request timeout + 30s margin (the gateway may settle when
  the paid work finishes, up to timeoutMs after our abort), pruned only by a
  real balance read that started after the window closed
- a signal that aborted before the paid request was dispatched, or a
  connection-refused/DNS failure, releases normally (provably unsettled)
- a paid 2xx whose body is cut off throws (ambiguous) instead of returning
  ok:true with an empty body and orphaning a sandbox
- the Infinity RPC-failure fallback never prunes ambiguous entries
- ModalCreate's insufficient-funds message reports what is actually held
- postWithPayment + signer are test-injectable; 13 reservation tests
@VickyXAI

Copy link
Copy Markdown
Contributor Author

Follow-up commit after an adversarial pass on the first cut:

  • Grace window is now timeoutMs + 30s, not a flat 30s. ModalCreate's own timeout is 90s; if the gateway settles after the paid work finishes, settlement can land up to 90s after our abort, so a flat 30s could prune before the balance read reflected it.
  • Provably-unsent failures are not ambiguous: a signal that aborted during signing (before the paid fetch went out) or an ECONNREFUSED/ENOTFOUND on the paid request releases normally.
  • Cut-off paid response is ambiguous: previously .text().catch(() => '') turned a 200 with a truncated body into ok: true, body: {}, which for ModalCreate meant a paid sandbox with no id in the tracker. It now throws and holds.
  • Prune only runs on a real on-chain read that started after the window closed; the Infinity RPC-failure fallback never prunes.
  • Insufficient-funds message names the ambiguous hold instead of "other in-flight calls".
  • postWithPayment and the signer are test-injectable; 13 tests cover the ambiguous/not-ambiguous matrix. Suite 683/683.

Not addressed here (pre-existing, flagged for follow-up): the SDK's Solana getBalance() returns 0 on RPC errors rather than throwing, so the "Infinity, don't block on flakiness" fallback never engages on Solana.

@VickyXAI
VickyXAI merged commit 6b6310e into main Aug 29, 2026
5 checks passed
@VickyXAI
VickyXAI deleted the fix/reservation-ambiguous-settlement branch August 29, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

walletReservation releases on an aborted x402 payment — but an aborted payment may still have settled

1 participant