Skip to content

feat(recovery): Phase B — settlement-uncertainty recovery loop#34

Merged
tkorkmazeth merged 7 commits into
niceberginc:mainfrom
boymak:feat/phase-b-settlement-recovery
Jun 25, 2026
Merged

feat(recovery): Phase B — settlement-uncertainty recovery loop#34
tkorkmazeth merged 7 commits into
niceberginc:mainfrom
boymak:feat/phase-b-settlement-recovery

Conversation

@boymak

@boymak boymak commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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_uncertain trace with the provider unpaid. This turns it into a recovery loop so payments are never silently lost.

  • 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.
  • TollGatependingSettlementStore config (default in-memory), pendingSettlements getter, enqueueSettlement(), reconcileSettlements(opts).
  • MCP adapter — settle now retries (configurable 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). Full suite: 242 passing.

⚠️ Stacking note

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

boymak and others added 7 commits June 25, 2026 15:08
…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 tkorkmazeth merged commit 1591305 into niceberginc:main Jun 25, 2026
1 check failed
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.

2 participants