Repository navigation
feat(x402): Reconcile pending x402 settlements from the chain - #6651
Merged
Merged
Conversation
osmarluz
marked this pull request as ready for review
October 13, 2026 09:05
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. |
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. |
## 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.
mikeh-lago
approved these changes
Oct 13, 2026
osmarluz
force-pushed
the
feat/x402/reconciliation
branch
from
October 13, 2026 13:27
f9c8106 to
30772d0
Compare
osmarluz
added this pull request to stack #6665
October 13, 2026 13:27
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.
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::ReconcileServicereads the chain for a pending row and marks itsettledwith the chain's transaction, orfailedonce the reader proves nothing can land. A row whose transaction a settled row already holds fails astransaction_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::ReconcileX402SettlementsJobruns every minute, whatever the x402 flag says, and enqueues oneX402::Settlements::ReconcileJobper row whosereconcile_afterhas passed.X402::Chain::EvmReaderscans logs in 200-block chunks: Base's public endpoints now refuse a widereth_getLogsrange.