Summary
The "upto" session spend cap is checked at request entry and debited at request completion, with no reservation held across the request lifetime. Because LLM generation takes seconds, many concurrent requests on a single session all pass the entry check before any of them is charged, so the cap only stops a fraction of them — the rest are served for free.
Found while reviewing #1.
Where
python/x402/http/middleware/flask.py — _handle_session_mode (entry check → _stream_session_response, no reservation), StreamingSessionResponse.close → _accumulate_session_cost (charge at completion)
python/x402/session.py / python/x402/redis_session.py — add_cost
Details
_handle_session_mode checks session.is_exhausted and then immediately begins streaming. No budget is reserved before _stream_session_response; the only debit happens in StreamingSessionResponse.close() after the response iterator is exhausted. The check-then-serve-then-charge window spans the entire generation.
Attack
- Open one session with a small cap.
- Fire N concurrent requests on that session.
- All N read
is_exhausted == False at the gate (none charged yet) → all N stream in full.
- Only after each completes does
add_cost run; the first few fit under the cap, the rest overflow and are dropped (see the companion "over-cap cost dropped" issue) → served free.
One signed session can thus serve roughly unlimited concurrent inference regardless of the cap. Redis's atomic _ADD_COST_SCRIPT does not help — it keeps the accumulator correct but cannot un-serve work already delivered.
Impact
Cap is not a real ceiling under concurrency; operator loss scales with the client's concurrency, not the signed amount. High severity, and it amplifies the over-cap-drop issue.
Suggested fixes
- Reserve budget atomically before streaming: debit an estimate (e.g.
max_tokens-worth, or a per-request floor) at entry, then reconcile to actual at close(). Reject at the gate when the reservation would exceed remaining budget.
- And/or enforce a per-session in-flight concurrency limit so the outstanding un-reconciled exposure is bounded.
Notes
Pre-existing architectural issue, not introduced by #1. Companion to the over-cap-cost-dropped issue; a pre-serve reservation scheme would address both together.
Summary
The "upto" session spend cap is checked at request entry and debited at request completion, with no reservation held across the request lifetime. Because LLM generation takes seconds, many concurrent requests on a single session all pass the entry check before any of them is charged, so the cap only stops a fraction of them — the rest are served for free.
Found while reviewing #1.
Where
python/x402/http/middleware/flask.py—_handle_session_mode(entry check →_stream_session_response, no reservation),StreamingSessionResponse.close→_accumulate_session_cost(charge at completion)python/x402/session.py/python/x402/redis_session.py—add_costDetails
_handle_session_modecheckssession.is_exhaustedand then immediately begins streaming. No budget is reserved before_stream_session_response; the only debit happens inStreamingSessionResponse.close()after the response iterator is exhausted. The check-then-serve-then-charge window spans the entire generation.Attack
is_exhausted == Falseat the gate (none charged yet) → all N stream in full.add_costrun; the first few fit under the cap, the rest overflow and are dropped (see the companion "over-cap cost dropped" issue) → served free.One signed session can thus serve roughly unlimited concurrent inference regardless of the cap. Redis's atomic
_ADD_COST_SCRIPTdoes not help — it keeps the accumulator correct but cannot un-serve work already delivered.Impact
Cap is not a real ceiling under concurrency; operator loss scales with the client's concurrency, not the signed amount. High severity, and it amplifies the over-cap-drop issue.
Suggested fixes
max_tokens-worth, or a per-request floor) at entry, then reconcile to actual atclose(). Reject at the gate when the reservation would exceed remaining budget.Notes
Pre-existing architectural issue, not introduced by #1. Companion to the over-cap-cost-dropped issue; a pre-serve reservation scheme would address both together.