Skip to content

feat(x402): Reconcile pending x402 settlements from the chain - #6651

Merged
osmarluz merged 5 commits into
mainfrom
feat/x402/reconciliation
Oct 14, 2026
Merged

osmarluz merged 5 commits into
mainfrom
feat/x402/reconciliation

Conversation

@osmarluz

@osmarluz osmarluz commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Context

When an x402 settle doesn't succeed, Lago keeps the settlement pending, because the money may still have moved. Only the chain can resolve it, and no organization can be enabled before something does.

Description

  • X402::Settlements::ReconcileService reads the chain for a pending row and marks it settled with the chain's transaction, or failed once the reader proves nothing can land. A row whose transaction a settled row already holds fails as transaction_already_recorded. A row whose transaction a failed row still holds stays pending and reports to Sentry. A failed row keeps no transaction hash, so an unconfirmed hash from a settle answer never blocks the payment it may belong to. A chain error leaves the row pending and reports to Sentry. The finish for each kind (the grant, or the invoice payment) comes in follow-ups.
  • Clock::ReconcileX402SettlementsJob runs every minute, whatever the x402 flag says, and enqueues one X402::Settlements::ReconcileJob per row whose reconcile_after has passed.
  • X402::Chain::EvmReader scans logs in 200-block chunks: Base's public endpoints now refuse a wider eth_getLogs range.

@osmarluz
osmarluz marked this pull request as ready for review October 13, 2026 09:05
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: PASS · CI green

PASS — The scheduled reconciliation follows the existing queue and uniqueness patterns, safely handles concurrent resolution and duplicate transaction hashes, and has focused coverage for both chain families, final/transient outcomes, and stale rows.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: PASS · CI green

PASS — The reconciliation flow follows existing service, locking, queue-routing, and clock-job patterns. Focused specs cover scheduling, due-row fan-out, EVM/SVM finality, stale rows, chain-read failures, and transaction-hash conflicts; no blocking correctness or sibling-call-site gap is evident.

@osmarluz
osmarluz requested a review from mikeh-lago October 13, 2026 10:05
## Context

Base's public RPC endpoints keep narrowing the `eth_getLogs` range they
accept: Base Sepolia now refuses anything wider than 200 blocks with HTTP
413. The EVM chain reader asked for 1,000, so every read that reached the
log scan failed as unreachable.

## Description

- `X402::Chain::EvmReader` scans logs in 200-block chunks. The 50-chunk
  cap stays, so a cancellation is still searched about 5 hours past the
  authorization's window.
## Context

A settle that doesn't succeed leaves its x402 settlement pending, and only
the chain can say whether the money moved. Nothing reads it yet.

## Description

- `X402::Settlements::ReconcileService` reads the chain for a pending row
  and marks it settled with the chain's transaction, or failed once the
  reader proves nothing can land. Any chain error leaves it pending and
  reports to Sentry.
- A transaction another row already holds fails the row as
  `transaction_already_recorded`.
- The write re-checks the row under its lock, so a row resolved during
  the chain read is left alone.
## Context

Pending x402 settlements must be read from the chain once their window
closes, whatever the organization's x402 flag says, so that switching
x402 off never strands one.

## Description

- `Clock::ReconcileX402SettlementsJob` runs every minute and enqueues one
  `X402::Settlements::ReconcileJob` per pending row whose
  `reconcile_after` has passed.
- Both jobs are unique with a 10-minute lock, so a slow chain read is
  never doubled and a lost worker frees its row before the stuck-pending
  page.
## Context

A pending settlement can carry a transaction hash from a settle answer that
the chain never confirmed. Kept on a failed row, or treated as proof by the
reconciliation, such a hash could fail a payment that did land: both rows
ended failed, nothing was credited and nothing alerted.

## Description

- A failed settlement no longer keeps its transaction hash, so an
  unconfirmed hash never holds the network-and-hash index.
- `transaction_already_recorded` applies only when a settled row holds the
  transaction. While the holder is still pending, the row stays pending and
  the next run reads it again.
## Context

A row whose chain transaction another row holds waits until that holder
settles or releases it. A failed row never does either. No code leaves a
hash on a failed row, but a hand fix can, and the row then waited silently
on every run while blocking the payer's next purchase. Failing a row as
`transaction_already_recorded` also left no trace of the row that holds
the payment.

## Description

- A row whose transaction a failed row holds stays pending and reports to
  Sentry, naming both rows. A pending holder still makes the row wait.
- Failing a row as `transaction_already_recorded` logs both rows' ids.
@osmarluz
osmarluz force-pushed the feat/x402/reconciliation branch from f9c8106 to 30772d0 Compare October 13, 2026 13:27
@osmarluz
osmarluz added this pull request to stack #6665 October 13, 2026 13:27
@osmarluz
osmarluz merged commit e31154d into main Oct 14, 2026
12 checks passed
@osmarluz
osmarluz deleted the feat/x402/reconciliation branch October 14, 2026 08:52
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