Skip to content

Add settlement confirmation polling for async facilitator jobs#1

Open
adambalogh wants to merge 4 commits into
ani/facilitator-v2from
claude/x402-settlement-drops-c02vyy
Open

Add settlement confirmation polling for async facilitator jobs#1
adambalogh wants to merge 4 commits into
ani/facilitator-v2from
claude/x402-settlement-drops-c02vyy

Conversation

@adambalogh

Copy link
Copy Markdown

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

  1. Settlement Job Polling: Added _poll_settlement_job() methods (both async and sync) that poll the facilitator's GET /settle/:jobId endpoint until the settlement reaches a terminal state (succeeded/failed).

  2. Job Status Parsing: Introduced _settle_response_from_job_status() to map facilitator job status responses to SettleResponse objects. Critically, this inspects the embedded result.success field 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).

  3. Configuration: Added three new fields to FacilitatorConfig:

    • wait_for_settlement (bool): Enable polling instead of optimistic reporting
    • settlement_poll_interval (float): Delay between status checks (default 2s)
    • settlement_poll_timeout (float): Maximum time to wait for confirmation (default 120s)
  4. Session Deadline Tracking: Enhanced UptoSession with a settlement_deadline property that extracts the signed authorization deadline from the permit payload (Permit2 or EIP-3009), falling back to created_at + maxTimeoutSeconds. This prevents sessions from outliving their signed authorization.

  5. Settlement Reaper: Updated the Flask middleware's session reaper to:

    • Force-settle sessions approaching their authorization deadline via get_settlement_due_sessions()
    • Reject new draw-downs on sessions within the safety margin to prevent accumulating cost on an expiring authorization
    • Added settlement_safety_margin parameter (default 60s) to control the deadline buffer
  6. Session 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 deadline expired) and the accumulated tab is silently lost. This change ensures:

  • Settlement is confirmed on-chain before reporting success
  • Sessions are force-settled before their authorization expires
  • New draw-downs are rejected once a session enters its settlement window

Tests

Added comprehensive unit tests covering:

  • Optimistic 202 handling when wait_for_settlement=False (default behavior)
  • Polling and confirming on-chain success
  • Detecting and surfacing on-chain failures (e.g., permit expiry) despite job "succeeded" status
  • Timeout handling when jobs don't confirm within the deadline
  • Settlement deadline extraction from signed payloads
  • Session reaper filtering for deadline-approaching sessions

All existing tests pass; new tests validate the polling logic and deadline safeguards.

https://claude.ai/code/session_01HdeaNhmieATKBRgpVtPG89

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

This comment was marked as outdated.

- _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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread python/x402/http/middleware/flask.py Outdated
Comment thread python/x402/session.py
Comment thread python/x402/redis_session.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants