Add settlement confirmation polling for async facilitator jobs#1
Open
adambalogh wants to merge 4 commits into
Open
Add settlement confirmation polling for async facilitator jobs#1adambalogh wants to merge 4 commits into
adambalogh wants to merge 4 commits into
Conversation
Draw-down ("upto") sessions reuse one signed Permit2 authorization whose
on-chain deadline is fixed at session creation (created_at + maxTimeoutSeconds)
and never refreshed. Two issues caused accumulated tabs to be settled after
that deadline — where the on-chain settle reverts — and the failure to be
swallowed silently:
1. The reaper only settled sessions on idle-timeout or exhaustion, so a
continuously-busy session could outlive its authorization deadline before it
was ever settled. Add a deadline-aware safeguard:
- UptoSession.settlement_deadline derives the hard deadline from the signed
payload (Permit2 deadline / EIP-3009 validBefore), falling back to
created_at + maxTimeoutSeconds.
- SessionStore/RedisSessionStore.get_settlement_due_sessions() returns
sessions within a safety margin of that deadline.
- The reaper force-settles due sessions first, and the middleware stops
accepting new draw-downs against a near-deadline session (returns 402) so
the client re-signs a fresh authorization.
2. The facilitator settles asynchronously (202 + job id) but the client
reported success the instant the job was enqueued, so the reaper marked the
session settled before anything hit the chain — and a later revert was lost.
Add opt-in wait_for_settlement to the facilitator client: on 202 it polls
GET /settle/:jobId and inspects result.success (a reverted job is reported by
the queue as "succeeded", so job state alone is not trusted). A revert or
timeout is surfaced as a real failure so the session is kept for retry.
Adds unit tests for deadline derivation, due-session selection, and the
polling success/failure/timeout paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdeaNhmieATKBRgpVtPG89
- _handle_session_mode now rejects reuse of a session that is mid-settlement (adds `session.settling` to the guard, matching _resume_session_request). Reusing a settling session served a response while add_cost() rejected the charge — i.e. unbilled inference. - Clamp settlement_poll_interval / settlement_poll_timeout to non-negative in the facilitator client init (both dataclass and dict config paths). A negative interval made the sync poller's time.sleep() raise and the async poller busy-loop. Adds tests: settling session returns 402 (active session still reused), and negative poll values clamp to 0 via both config paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HdeaNhmieATKBRgpVtPG89
…lines Follow-up to the settlement-confirmation work. Adds strict, fail-closed admission for upto draw-down sessions so the settlement-deadline safeguard cannot be sidestepped. Signed deadline is now authoritative (x402-foundation#4): extraction of the signed Permit2 `deadline` / EIP-3009 `validBefore` is factored into `signed_authorization_deadline()` and exposed as `UptoSession.signed_deadline`, kept separate from `settlement_deadline`'s advertised-window fallback. Because `PaymentPayload.payload` is a free-form dict, that fallback can outlive the true on-chain deadline; the middleware must not trust it when deciding to serve paid work. New-session admission and session reuse now key off the *signed* deadline only and refuse when it is absent/unparseable, rather than assuming `created_at + maxTimeoutSeconds`. The reaper keeps the best-effort fallback for settling tabs that already accumulated. Minimum validity window (#3): a fresh authorization is admitted only when its signed deadline is further out than the settlement safety margin. This stops a client from opening sessions with almost-expired authorizations to drive a rapid force-settle / re-sign churn (each settlement a full-gas on-chain tx). Paid work is never streamed on rejection — the 402 short-circuits before `create_session` / streaming. Tests: signed-deadline extraction (Permit2, EIP-3009, malformed → None) and middleware admission (reject missing deadline, reject near-expiry, admit valid). Full unit suite passes (753). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167k5thKVVQJMSmEWnn5mHP
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.
Description
This PR adds support for confirming settlement completion when the facilitator returns a 202 Accepted response with a job ID. Previously, the client would optimistically report success immediately upon receiving a 202, without waiting for the actual on-chain settlement to complete.
Key Changes
Settlement Job Polling: Added
_poll_settlement_job()methods (both async and sync) that poll the facilitator'sGET /settle/:jobIdendpoint until the settlement reaches a terminal state (succeeded/failed).Job Status Parsing: Introduced
_settle_response_from_job_status()to map facilitator job status responses toSettleResponseobjects. Critically, this inspects the embeddedresult.successfield rather than trusting the job state alone—a job marked "succeeded" at the queue level may still have failed on-chain (e.g., Permit2 deadline expired).Configuration: Added three new fields to
FacilitatorConfig:wait_for_settlement(bool): Enable polling instead of optimistic reportingsettlement_poll_interval(float): Delay between status checks (default 2s)settlement_poll_timeout(float): Maximum time to wait for confirmation (default 120s)Session Deadline Tracking: Enhanced
UptoSessionwith asettlement_deadlineproperty that extracts the signed authorization deadline from the permit payload (Permit2 or EIP-3009), falling back tocreated_at + maxTimeoutSeconds. This prevents sessions from outliving their signed authorization.Settlement Reaper: Updated the Flask middleware's session reaper to:
get_settlement_due_sessions()settlement_safety_marginparameter (default 60s) to control the deadline bufferSession Store: Both in-memory and Redis implementations now support
get_settlement_due_sessions()to identify sessions requiring immediate settlement.Why This Matters
Long-lived draw-down sessions reuse a single signed payment authorization with a fixed deadline. If settlement occurs after that deadline, the on-chain transaction reverts (e.g., Permit2
deadlineexpired) and the accumulated tab is silently lost. This change ensures:Tests
Added comprehensive unit tests covering:
wait_for_settlement=False(default behavior)All existing tests pass; new tests validate the polling logic and deadline safeguards.
https://claude.ai/code/session_01HdeaNhmieATKBRgpVtPG89