Skip to content

feat(live-runner): self-funding metered single-shot calls - #50

Draft
rickstaa wants to merge 5 commits into
ja/live-runnerfrom
rs/single-shot-ongoing-payments
Draft

feat(live-runner): self-funding metered single-shot calls#50
rickstaa wants to merge 5 commits into
ja/live-runnerfrom
rs/single-shot-ongoing-payments

Conversation

@rickstaa

@rickstaa rickstaa commented Jul 29, 2026

Copy link
Copy Markdown
Member

Metered (hour/720p) single-shot calls currently pay only the initial 402 challenge. On go-livepeer master (since livepeer/go-livepeer#4000), ProxyLiveRunnerSingleShot reserves a paid session per request and debits it every -livePaymentInterval (5s default) — balance is checked before each debit, and the first under-funded tick releases the session and cancels the in-flight request. The signer's live type prerolls only ~10s, so any metered call longer than that dies unless the caller hand-writes a payment loop.

This makes call_runner own that loop, the single-shot sibling of #31: whatever holds the session open funds it, and callers never hand-manage a background task.

What this adds

  • Non-stream calls: after the paid 402 retry, when the payment type is metered (live/lv2v), a payment task runs for exactly the lifetime of the awaited request and is cancelled the moment the response lands. Zero new public API — no auto_pay flag: an opt-out is "broken mode" (the caller is blocked awaiting the response and can't fund manually), offchain (no signer_url) is the natural no-payments path, and the loop can't overspend (the signer bills only elapsed seconds).
  • Streamed calls (stream=True): the stream outlives the call, so LiveRunnerCallStream owns the task — auto-started, cancelled by aclose() before transport teardown, plus a read-only released flag set when the orchestrator reports the session gone. No public stop_payments(): a single-shot stream is its request, so "keep stream, stop funding" isn't a real state.
  • Cadence: first follow-up one interval after the challenge payment (which carries the preroll), at 60% of the challenge's payment_interval_ms when advertised (go-livepeer rs/singleshot-payment-interval), 3s fallback on master.
  • Loop policy: 404 → session released, stop (+released on streams); 409 → fixed-price, stop; 403 → mismatch, stop + log; 482 → skip the cycle; transient errors retry next tick. Loop failures never mask the request result — the orchestrator cancels the call itself if funding actually stops.
  • Session-scoped payments: the loop targets …/apps/{runner_id}/session/{manifest_id}/payment (derived from the runner URL + challenge manifest_id) for real liveness semantics, falling back to the generic /payment credit endpoint when the URL shape doesn't match.
  • Plumbing (re-derived from the closed single-shot branch): post_empty hoisted into http.py with typed status errors, send_payment(payment_url=), payment_interval_msserver_payment_interval on results/streams, challenge session_id carried onto streams.
  • Fixed pricing is untouched: the payment type gates the loop, and follow-up payments would 409 anyway.

Commits

  • 323c964 payment plumbing (post_empty, payment_url kwarg, interval parse)
  • 3936c77 fund metered single-shot calls while they run
  • e1c6f8f stream owns the payment loop
  • f7ddca0 session-scoped payment endpoint with generic fallback
  • b0f46d4 examples/text/client.py: self-funding SSE client

Testing

  • Unit tests are deliberately left out of this PR — they'll land with the broader test reorg (a fake orch+signer suite covering cadence, cancel-on-return, offchain/fixed no-loop, and 404/409/403/482 exists and is ready to resurrect).
  • Pending: on-chain Arbitrum smoke test (metered call outliving one 5s tick through the remote signer), which also confirms the session-scoped payment URL shape against a real orch.

Relation to #31

Lands first and defines the loop policy; #31 (persistent sessions) can rebase onto it and reuse the same helper. reserve_session(auto_pay=True) and stop_payments() stay in #31 — reserved sessions outlive their calls, so external funding control is a real use case there.

🤖 Generated with Claude Code

rickstaa and others added 4 commits July 29, 2026 13:37
- hoist _post_empty into http.py as post_empty (TCPConnector(ssl=False),
  typed status errors via the shared error mapper) so payment callers can
  branch on 404/409/403/482
- route LivePaymentSession.send_payment through it; the new payment_url
  kwarg targets a specific payment endpoint such as the session-scoped
  one
- parse payment_interval_ms from payment challenges and surface it as
  server_payment_interval on call results and streams
- carry the challenge session_id and the fixed-price payment_session
  drop through call_runner(stream=True)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The orchestrator debits the prepaid balance on -livePaymentInterval
while a metered (hour/720p) single-shot request is in flight and
cancels it on the first under-funded tick; the signer's preroll only
covers ~10s. Spawn a payment loop inside call_runner for the lifetime
of the awaited request: first payment after one interval, cadence at
60% of the challenge's payment_interval_ms with a 3s fallback, stop on
404 (released) / 409 (fixed-price) / 403 (mismatch), skip on 482, retry
transient errors. No opt-out flag: offchain mode is the no-payments
path and fixed pricing never starts the loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A streamed single-shot response outlives the call_runner call, so the
LiveRunnerCallStream owns its funding: the loop starts with the stream,
aclose() cancels it before tearing down the transport, and a 404 from
the payment endpoint sets a read-only released flag. No public
start/stop controls - a single-shot stream is its request, so stopping
funding without closing is not a meaningful state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed endpoint

Derive .../apps/{runner_id}/session/{session_id}/payment from the
single-shot runner URL and the challenge manifest_id so the loop gets
liveness semantics (404 once the session is released, 409 for fixed
pricing) instead of blind-crediting the generic /payment endpoint,
which stays as the fallback when the URL shape does not match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58a4f8f9-e69e-4667-9ff6-70e1a820888e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Streams the story via call_runner(stream=True); with --signer-url the
call pays its 402 challenge and keeps funding itself while the stream
runs, demonstrating that metered single-shot calls need no payment
code on the caller side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rickstaa
rickstaa force-pushed the rs/single-shot-ongoing-payments branch from ce44571 to b0f46d4 Compare July 29, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant