Skip to content

SDK-level retries can sign a second payment: transport mints a fresh payment on every 402, max_retries defaults to 2 #34

Description

@aurumflux20

Reading through _transport.py and _client.py — the transparent-402 design is clean, and materializing the request body before the retry (the RequestNotRead branch) is a nice catch. One interaction with the SDK layer above it looks worth a second look.

X402OpenAI subclasses openai.OpenAI and forwards constructor kwargs. It sets a default timeout but doesn't pin max_retries, so the SDK default of 2 applies unless a caller overrides it. The OpenAI SDK retries on connect/read timeouts and on 408/409/429/5xx.

X402Transport.handle_request signs a payment every time it sees a 402, and holds no state across calls (__slots__ = ("_inner", "_x402")). The paid retry is returned directly with no exception handling, so a timeout on it propagates up to the SDK.

Put together: if the paid request times out (or the upstream returns a retryable 5xx after accepting payment), the SDK retries the whole request. That new request re-enters the transport, receives a fresh 402, and signs a second payment payload for the same logical call. With the default max_retries=2, one chat.completions.create() can settle up to three payments.

Where I can't see the whole picture: whether the second payment actually settles depends on the facilitator and the upstream 402 handler. Since each payload carries its own authorization/nonce, a server has no protocol-level reason to treat them as the same call — but if your intended facilitators do dedupe across nonces for a window, this is a non-issue and I'd be glad to be corrected.

If it is real, a couple of options that don't fight the SDK:

  • Default max_retries=0 in X402OpenAI.__init__ (callers can still opt in), so the retry decision sits with the code that knows a payment happened; or
  • Have the transport cache the signed payment headers keyed by the request body digest for a short window, so an SDK-level retry replays the same payment rather than minting a new one.

Happy to send a PR for either if it's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions