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.
Reading through
_transport.pyand_client.py— the transparent-402 design is clean, and materializing the request body before the retry (theRequestNotReadbranch) is a nice catch. One interaction with the SDK layer above it looks worth a second look.X402OpenAIsubclassesopenai.OpenAIand forwards constructor kwargs. It sets a defaulttimeoutbut doesn't pinmax_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_requestsigns 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, onechat.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:
max_retries=0inX402OpenAI.__init__(callers can still opt in), so the retry decision sits with the code that knows a payment happened; orHappy to send a PR for either if it's useful.