From 9e58323926f894ae6f2c8a74d5c7bf53c1f8f0ac Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 24 Jul 2026 21:42:45 +0200 Subject: [PATCH] server: Report payment interval in payment challenges Add payment_interval_ms to the live runner payment challenge JSON (reservation, scope, and refresh-payment responses) so paying clients know the orchestrator's debit cadence and can size and time payments to keep the session balance funded. Ported from #3998 (originally approved as #3975). Co-authored-by: Brad P Co-Authored-By: Claude Fable 5 --- doc/live-runner.md | 4 +++- server/ai_http.go | 15 +++++++++------ server/ai_http_test.go | 1 + server/rpc.go | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/doc/live-runner.md b/doc/live-runner.md index 627d9d07db..b75004183b 100644 --- a/doc/live-runner.md +++ b/doc/live-runner.md @@ -391,7 +391,9 @@ parameters. The client retries the same request with `Livepeer-Payment` and `Livepeer-Segment`, then periodically refreshes payment at `POST /apps/{runner_id}/session/{session_id}/payment`. The orchestrator accounts usage on its configured payment interval and releases the session if payment -fails. If the payment unit is `fixed` then payment is only processed once. +fails. The challenge response reports that interval as `payment_interval_ms` so +clients can size and time payments to keep the session balance funded. If the +payment unit is `fixed` then payment is only processed once. Offchain runners do not issue payment challenges. For the underlying ticket protocol, see [Payments](payments.md). diff --git a/server/ai_http.go b/server/ai_http.go index 7008112719..045e060ea3 100644 --- a/server/ai_http.go +++ b/server/ai_http.go @@ -185,6 +185,8 @@ type liveRunnerPaymentChallengeResponse struct { // Keep the URL in top-level JSON so clients do not need to parse the protobuf just to route payment. Orchestrator string `json:"orchestrator"` ManifestID string `json:"manifest_id"` + // Interval at which the orchestrator debits the session balance. + PaymentIntervalMs int64 `json:"payment_interval_ms"` } type liveRunnerTrickleChannelRequest struct { @@ -402,7 +404,7 @@ func (h *lphttp) runnerChallenge(w http.ResponseWriter, r *http.Request, priceIn respondWithError(w, err.Error(), http.StatusInternalServerError) return } - data, err := marshalLivePaymentChallengeResponse(oInfo) + data, err := marshalLivePaymentChallengeResponse(oInfo, h.node.LivePaymentInterval) if err != nil { respondWithError(w, err.Error(), http.StatusInternalServerError) return @@ -427,7 +429,7 @@ func (h *lphttp) scopePaymentChallenge(w http.ResponseWriter, r *http.Request) ( if err != nil { return false, err } - data, err := marshalLivePaymentChallengeResponse(oInfo) + data, err := marshalLivePaymentChallengeResponse(oInfo, h.node.LivePaymentInterval) if err != nil { return false, err } @@ -437,15 +439,16 @@ func (h *lphttp) scopePaymentChallenge(w http.ResponseWriter, r *http.Request) ( return true, nil } -func marshalLivePaymentChallengeResponse(oInfo *lpnet.OrchestratorInfo) ([]byte, error) { +func marshalLivePaymentChallengeResponse(oInfo *lpnet.OrchestratorInfo, paymentInterval time.Duration) ([]byte, error) { buf, err := proto.Marshal(oInfo) if err != nil { return nil, err } return json.Marshal(liveRunnerPaymentChallengeResponse{ - PaymentParams: base64.StdEncoding.EncodeToString(buf), - Orchestrator: oInfo.GetTranscoder(), - ManifestID: oInfo.GetAuthToken().GetSessionId(), + PaymentParams: base64.StdEncoding.EncodeToString(buf), + Orchestrator: oInfo.GetTranscoder(), + ManifestID: oInfo.GetAuthToken().GetSessionId(), + PaymentIntervalMs: paymentInterval.Milliseconds(), }) } diff --git a/server/ai_http_test.go b/server/ai_http_test.go index da28afe7fc..86cf80deac 100644 --- a/server/ai_http_test.go +++ b/server/ai_http_test.go @@ -664,6 +664,7 @@ func TestLiveRunnerReserveSessionOnchainReturnsPaymentChallenge(t *testing.T) { require.NotEmpty(t, challenge.PaymentParams) require.Equal(t, lp.orchestrator.ServiceURI().String(), challenge.Orchestrator) require.NotEmpty(t, challenge.ManifestID) + require.Equal(t, int64(5000), challenge.PaymentIntervalMs) require.Equal(t, challenge.ManifestID, oInfo.GetAuthToken().GetSessionId()) require.NotNil(t, oInfo.GetTicketParams()) require.NotNil(t, oInfo.GetPriceInfo()) diff --git a/server/rpc.go b/server/rpc.go index 4060d3784e..5ab4c9a355 100644 --- a/server/rpc.go +++ b/server/rpc.go @@ -496,7 +496,7 @@ func (h *lphttp) RefreshPayment(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - data, err := marshalLivePaymentChallengeResponse(oInfo) + data, err := marshalLivePaymentChallengeResponse(oInfo, h.node.LivePaymentInterval) if err != nil { respondJsonError(ctx, w, err, http.StatusInternalServerError) return