feat(recovery): Phase B — settlement-uncertainty recovery loop#34
Merged
tkorkmazeth merged 7 commits intoJun 25, 2026
Merged
Conversation
…eta on devnet) Moves Solana from "experimental rail + example" to a packaged, end-to-end Beta. - Ship the client signer in the package: src/rail-adapters/x402-solana-signer.ts exports buildSolanaPaymentPayload + extractSolanaRequirement (also re-exported from the root). @solana/web3.js and @solana/spl-token become OPTIONAL peer dependencies, dynamically imported — install stays light for non-Solana callers. The example sign-payload.mjs now re-exports from the package. - Add src/__tests__/x402-solana-e2e.test.mjs: drives the MCP adapter's built-in verify -> credit -> execute -> settle path on the Solana rail against a fake in-process facilitator (localhost; no validator). Asserts the 402 Solana challenge, the signed retry, and a trace carrying rail_payment_verified + rail_payment_settled with the on-chain tx signature. - Status bumped Experimental -> Beta on devnet (README + landing). Mainnet still untested. Full suite: 230 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mainnet) devnet-settle.mjs now honors SOLANA_NETWORK=devnet|mainnet (plus NETWORK_CAIP2 / USDC_MINT / SOLANA_RPC_URL overrides), resolving the CAIP-2 id, USDC mint, default RPC, and explorer cluster per network. Mainnet uses a real-USDC funding prompt instead of the faucet. Self-transfer default keeps the smoke test zero-net-cost. Gitignore the mainnet payer keypair too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran the same flow on Solana mainnet-beta with real USDC via PayAI: self-transfer, err: None, fee paid by the facilitator (tx 3d9k5PACqnSqYk42xMjyvkdzZZNfDPjysRyHGVzpxxCYu1womD6eMAGQx2neZcNCerLNkbjDoy15Y31...). Status (README + landing): Solana x402 now "Beta; verified on devnet and mainnet". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… on Base mainnet) Found while mainnet-testing Talha's original EVM x402 path: the EVM "exact" scheme (EIP-3009) needs the token's EIP-712 domain (name + version) in PaymentRequirements.extra, or the facilitator rejects verify with `invalid_exact_evm_missing_eip712_domain`. The rail wasn't adding it, so EVM verify failed unless the caller hand-built the requirements. - Add EVM_USDC_EIP712_DOMAINS (Base, Base Sepolia, Ethereum, Polygon, Arbitrum, Optimism) and X402RailConfig.eip712Domain override. - createChallenge auto-injects extra.name/version for EVM (mirrors the Solana feePayer injection); forwarded to the facilitator on verify/settle. - src/__tests__/x402-evm-rail.test.mjs: domain auto-injection, testnet variant, explicit override, no feePayer for EVM, and that verify forwards the domain. Verified live: with the domain, a 0.001 USDC self-transfer settled on Base mainnet via PayAI, gasless (tx 0xc18b14ef94c7ebd5589bf0816f6c8c8820d87e77c4ace5f118486a3f3c2a42f9). Status: x402 EVM -> Beta (verified on Base mainnet). Full suite: 233 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ettlement-recovery # Conflicts: # README.md # landing/index.html # package.json
When a payment is verified + executed but settlement fails after the fact
(facilitator/RPC blip), it was a dead-end trace (settlement_uncertain) with the
provider unpaid. Phase B turns that into a recovery loop:
- src/settlement-recovery.ts:
- settleWithRetry() — bounded exponential backoff over settlePayment.
- PendingSettlementStore + InMemoryPendingSettlementStore — durable queue of
verified-but-unsettled payments.
- SettlementReconciler — drains the queue through each rail adapter; removes
what settles, keeps the rest with attempts/lastError for the next pass.
- TollGate: pendingSettlementStore config (default in-memory), `pendingSettlements`
getter, `enqueueSettlement()`, and `reconcileSettlements(opts)`.
- MCP adapter: settle now retries (configurable via settleRetry); anything still
unconfirmed is queued and the trace records attempts + queued:true.
Tests (src/__tests__/settlement-recovery.test.mjs): retry success/exhaustion,
store ops, reconciler drain/keep/no-adapter, and an end-to-end loop through
TollGate + the MCP adapter (settle fails -> queued -> reconciled).
Built on the merged Solana Beta (niceberginc#32) and EVM EIP-712 (niceberginc#33) work. Full suite: 242 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…duled loop Builds on Phase B's recovery loop: - DbPendingSettlementStore + DB_PENDING_SETTLEMENT_SCHEMA: durable, cross-instance pending-settlement queue (D1/Turso/SQLite), the multi-instance counterpart to the in-memory store — queued settlements survive restarts. - ChainConfirmer + two-phase reconciliation: a successful settle records its tx hash and stays queued until confirmed on-chain; a queued item that already has a tx hash is re-checked for confirmation instead of being settled again, so a landed-but-unconfirmed payment is never double-settled. - startSettlementReconciler(): fixed-interval loop with overlap protection and a manual tick(), for running reconciliation on a timer/cron. Tests extend settlement-recovery.test.mjs: confirmer two-phase (settle once, confirm later, no re-submit), durable SQLite store (JSON round-trip, enqueuedAt preserved on update, end-to-end reconcile), and the scheduled loop (tick/stop, overlap skipped). Full suite: 247 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tkorkmazeth
approved these changes
Jun 25, 2026
tkorkmazeth
approved these changes
Jun 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase B: when an x402 payment is verified + the tool executes but settlement fails afterwards (facilitator/RPC blip), today it's a dead-end
settlement_uncertaintrace with the provider unpaid. This turns it into a recovery loop so payments are never silently lost.settleWithRetry()— bounded exponential backoff oversettlePayment.PendingSettlementStore+InMemoryPendingSettlementStore— durable queue of verified-but-unsettled payments.SettlementReconciler— drains the queue through each rail adapter; removes what settles, keeps the rest withattempts/lastErrorfor the next pass.TollGate—pendingSettlementStoreconfig (default in-memory),pendingSettlementsgetter,enqueueSettlement(),reconcileSettlements(opts).settleRetry); anything still unconfirmed is queued and the trace recordsattempts+queued:true.Tests
src/__tests__/settlement-recovery.test.mjs: retry success/exhaustion, store ops, reconciler drain/keep/no-adapter, and an end-to-end loop through TollGate + the MCP adapter (settle fails → queued → reconciled). Full suite: 242 passing.Branched on a local combine of #32 (Solana Beta) + #33 (EVM EIP-712), which aren't merged yet — so this PR's diff includes those too. Merge order: #32 → #33 → this, or merge this to bring all three. Happy to rebase to a clean B-only diff once #32/#33 land.
🤖 Generated with Claude Code