Skip to content

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

Description

@aurumflux20

Reading through the wallet accounting in src/wallet/reservation.ts and src/tools/modal.ts — the reservation layer does the right thing for the parallel-batch case it was built for. One edge looks unhandled.

postWithPayment puts a single 30s AbortController across the whole handshake, and the signed payment request shares that same signal (modal.ts L197 for the probe, L204 for the paid request). If the budget expires during the paid request, the fetch aborts and throws. The caller's finally then releases the reservation unconditionally (modal.ts L460-461).

But as the header comment in reservation.ts notes, x402 is fire-and-forget per request — an aborted request may already have been received and settled on-chain. In that case the money is gone and the reservation is released anyway, so totalReserved() under-counts and the next hold() sees headroom that doesn't exist.

Not a double-spend, and low severity — it's the same class of local-accounting drift the module already guards against, just on the ambiguous-failure path rather than the concurrency one.

Worth noting there's recent precedent: hpp-io/x402-mcp-bridge hit this exact shape and shipped v0.1.15 last week — on an ambiguous settle outcome they now hold the reservation rather than release it, so the cap can only ever err tight, never loose, and a genuinely-absent spend self-heals at the next ledger reset. Erring toward "spent" seems like the right default here too.

Happy to send a PR if useful — the change is small: distinguish an aborted/ambiguous failure from a definitive one before releasing in the finally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions