Repository navigation
feat(x402): Grant reconciled credit purchases and retry failed grants - #6664
Conversation
## Context A credit purchase that reconciliation confirms is money received with no credits until something grants it. ## Description `X402::Settlements::ReconcileService` runs `X402::CreditPurchases::GrantService` for a settled credit purchase that has no wallet transaction, right after confirming it or when handed one directly. The grant bills the purchase. A failed grant leaves the purchase settled, logs a warning and reports to Sentry, so the next run can retry it.
## Context Reconciliation will look up settled credit purchases left without a grant every minute. ## Description A partial index on `x402_settlements.updated_at` covers settled credit purchases that have no wallet transaction.
## Context A grant can fail after its settle, and then the money has moved while the agent holds no credits. Nothing retries it unless the middleware resends the payment. ## Description `Clock::ReconcileX402SettlementsJob` also enqueues settled credit purchases left without a wallet transaction for two minutes, so `X402::Settlements::ReconcileService` re-runs the grant every minute until it succeeds, whatever the x402 flag says.
## Context The backstop retries a grant only while the row's `updated_at` stays older than two minutes, and nothing checked that a failed grant leaves it alone. Nothing checked either that a grant raising mid-save hands back the row as stored. ## Description The backstop specs now cover a failed and a raised grant keeping the row due for another attempt, and a grant whose save raises returning the stored row.
## Context When an enqueue fails after the grant committed, for example while Redis is unreachable, reconciliation logged "credit grant failed" for a row that holds its credits. The purchase is never billed, and the warning sent the operator after a missing grant instead of a missing invoice. ## Description `X402::Settlements::ReconcileService` logs "credit grant follow-up failed" when the reloaded row already has its wallet transaction, and keeps "credit grant failed" otherwise.
## Context The clock reads purchases left without a grant in primary key order, a thousand at a time. On an index keyed on `updated_at`, Postgres has to sort every match first. Its estimate of those matches multiplies the selectivity of three correlated filters, so it soars once invoice payments, which never get a wallet transaction, fill the table. The planner then walks the whole primary key every minute instead. ## Description The partial index on settled credit purchases without a wallet transaction is now keyed on `id`, so the clock reads it in order and stops at the batch size.
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — a failed credit grant is swallowed as a successful reconciliation.
|
|
Regarding the pre-review HOLD: declining it. Letting the grant failure fail the job is a deliberate design choice, not an oversight.
The real gap is that a grant that never succeeds is retried forever without a cap or backoff. That is tracked as a follow-up: a capped, backed-off retry is where raising would earn its place. The PR description now states why the job succeeds when the grant fails. |
mikeh-lago
left a comment
There was a problem hiding this comment.
Agree with declining the automated point: with retry: 0 raising only fills the dead set while the clock already retries. Approving. The capped, backed-off retry you mention also covers rows stuck on an inconclusive chain read in #6651, so it would be good to have it before the flag goes on for anyone.
Context
Once reconciliation confirms an x402 credit purchase, the agent has paid but holds no credits until the purchase is granted. The same goes for a purchase whose grant failed after its settle.
Description
X402::Settlements::ReconcileServicegrants a settled credit purchase throughX402::CreditPurchases::GrantService, which also bills it. A failed grant leaves the purchase settled, logs a warning and reports to Sentry. When the grant committed but a job enqueued after it could not be, the warning says so.Clock::ReconcileX402SettlementsJobalso picks up settled credit purchases left without a grant for two minutes, so the grant is retried every minute. The reconciliation job still succeeds when the grant fails: a raised job would go to the dead set every minute (retry: 0).x402_settlements.id, limited to settled credit purchases without a wallet transaction, serves that lookup in primary key order.