SCRATCH NOTES — do not treat as design doc. Written while
NETWORK-PRICING-ARCHITECTURE.htmlandPRICING-EXECUTION-PLAN.htmlare being edited concurrently by another worker. Source:gh pr diff 3992 --repo livepeer/go-livepeer(state: DRAFT, author rickstaa, branchja/live-pricing, base is a feature branch notmain, stacked with #3938).
The user's mental model does not match what #3992 actually does. There is no type=fixed
per-request/per-image payment type in this PR — the string fixed does not appear anywhere in the
diff. What #3992 actually ships is:
- A new runner price schema (
LiveRunnerPriceInfo) that advertises a decimal USD price with aunit ∈ {hour, 720p}discriminator (flat-hourly vs per-pixel), converted to wei for the wire. - A new signer job
type=livethat bills elapsed wall-clock seconds (time-metered, 10s minimum), alongside the existing pixel-basedtype=lv2v.
Both mechanisms are time-based / streaming (live video). Neither is a "flat $X per request/image". So:
- Per-call (tools) and per-image (nano-banana) are NOT natively covered by #3992. Those are discrete
request/response caps; #3992's
type=livemeters seconds, which is the wrong basis for a per-call fee. price=0.001is NOT$0.001 per request. It is$0.001 per hour(unit=hour) or per 720p-hour (unit=720p), USD, prorated to elapsed seconds, then converted to wei at settlement.- Tokens is a real gap, but it is NOT the only gap relative to the user's thesis: per-call and
per-image still need the
billing_unit_kind/billing_unit_quantityquantity design (PR4/PR5). Under the design's per-unit-quantity mechanism (not #3992), quantity=1 handles per-call/per-image trivially and tokens is the residual B3 blocker.
Net: #3992 validates two pillars of our design (runner-advertised decimal USD price; signer branching
fee logic on a type), but it does not reduce the need for the quantity-stamping design for discrete
caps, and it does not solve the tokens gap.
ai/runner/live_runner.go — LiveRunnerPriceInfo (diff hunk @ type LiveRunnerPriceInfo struct, ~L79):
// BEFORE
PricePerUnit int64 `json:"price_per_unit"`
PixelsPerUnit int64 `json:"pixels_per_unit"`
Unit string `json:"unit,omitempty"` // "USD" | "WEI"
// AFTER
Price json.Number `json:"price"` // decimal, e.g. "0.001990656" (string on wire)
Currency string `json:"currency,omitempty"` // runner side: must be "usd" (default)
Unit string `json:"unit,omitempty"` // "hour" (default) | "720p"priceRat()(ai/runner/live_runner.go, new): parsesPriceas a positivebig.Rat; requires it.normalizeLiveRunnerPriceInfo()(new): currency defaults tousdand must beusd(price_info.currency must be usd); unit defaults tohour, allowed{hour, 720p}elseprice_info.unit must be hour or 720p.normalizeHeartbeat()(~L869): now callsnormalizeLiveRunnerPriceInfoinstead of the old positive-int +USD|WEIcheck.
ai/runner/live_runner.go — newConverterForRunner() / convertPrice() (~L1663):
unit=hour→ denom3600→ wei per second; convertedUnit = "seconds".unit=720p→ denom3600 * 1280*720*30(720p@30fps) → wei per pixel-second; convertedUnit = "720p-pixel-seconds".- Conversion goes through
core.NewAutoConvertedPrice("USD", …)+common.PriceToInt64(PriceFeedWatcher), emitting{Price: <wei>, Currency: "wei", Unit: "seconds"|"720p-pixel-seconds"}. PaymentInfo()(~L958) anddiscoveryRunner()(~L1608) now callrunner.convertPrice().
server/ai_http.go:
runnerOrchInfo()(~L411): readspriceInfo.Price.Int64()(must be >0), setsnet.PriceInfo{PricePerUnit: <wei>, PixelsPerUnit: 1}→TicketParams.DiscoverLiveRunners()(~L814): advertises{Price: <wei str>, Currency: "wei", Unit: "720p-pixel-seconds"}.
server/remote_discovery.go:
validateRunner()/validateRunnerPrice()(new, replacingremoteDiscoveryRunnerEligible): requirescurrency == "wei"andunit ∈ {seconds, 720p-pixel-seconds}; positive decimal price; enforces the globalBroadcastCfg.MaxPrice()(no longer per-capability/model max — see test churn in 1f). (This is "option 1" from go-livepeer issue #3985 to unblock apps in remote discovery — confirmed by j0sh in PR comments.)
server/remote_signer.go — GenerateLivePayment():
const RemoteType_Live = "live"(~L36) added next toRemoteType_LiveVideoToVideo = "lv2v".RemotePaymentStategains aTypefield (~L220);RemotePaymentRequest.Typedoc now listslive, lv2v(~L245).- Type is locked per session: if
state.Type != "" && state.Type != req.Type→job type mismatch(400) (~L429). First payment storesstate.Type = req.Type. - Billing branch (~L531):
pixels := int64(0) billableUnits := int64(req.InPixels) if <lv2v ...> { pixels = int64(pixelsPerSec * billableSecs) // pixel-based billableUnits = pixels } else if req.Type == RemoteType_Live { if billableSecs <= 0 { billableSecs = 10 } // 10-second minimum on first tick billableUnits = int64(math.Ceil(billableSecs)) // <-- bills ELAPSED SECONDS } else if req.Type != "" { error "invalid job type" } fee := calculateFee(billableUnits, initialPrice) // wei/second * seconds
- Test
TestGenerateLivePayment_LiveBillsElapsedSecondsconfirms: first tick bills the 10s minimum (10s * 3 wei/s = 30 wei), follow-up bills real elapsed (12s * 3 wei/s = 36 wei). So it is a running per-session, per-second meter, not a per-request flat fee and not a single per-session fee.
- Per-capability/model max-price filtering in remote discovery is removed in favor of a single global
BroadcastCfg.MaxPrice()(TestRemoteSigner_Discovery_FiltersRunnerDiscoveryPricingrewritten). - Runner wire prices in every test switch from
{price_per_unit,pixels_per_unit,unit:"WEI"}to{price, currency:"wei", unit:"seconds"|"720p-pixel-seconds"}.
rickstaa flagged (PR comment) that server/live_payment_processor.go processOne still derives billable
amount from pixels (720p@30fps) regardless of the new price unit — j0sh confirmed "great catch, fix
incoming". So even the elapsed-seconds path is not fully wired through the live payment processor yet.
This is a draft PR.
| User's claim | Reality in #3992 | Accurate? |
|---|---|---|
A type=fixed payment type is added |
No fixed type. New type is type=live (elapsed-seconds). unit=hour is the closest "flat/pixel-agnostic" concept, but it's still time-metered. |
❌ |
price=0.001 ⇒ orch pays ~$0.001 per request |
price is USD per hour (or per 720p-hour), prorated to elapsed seconds. Not per-request. |
❌ |
| Per-session configurable | Billing is a running per-second meter within a session (SequenceNumber/LastUpdate), 10s min on first tick. Not a single per-session flat charge, not per-request. No "per-session flat" mode. | |
| wei or USD? | Runner advertises USD (currency must be usd); converted to wei for the wire/settlement. Both, at different layers. |
✔ (clarified) |
Design's closed enum today: unit_kind ∈ {megapixel, image, second, characters, call} (+ tokens = B3
blocker). Mapping each to how it would price under (a) #3992's live-runner mechanisms and (b) the
execution-plan quantity design (PR4/PR5):
| unit_kind | Natural pricing basis | Covered by #3992? | Covered by design PR4/PR5 (qty)? | Gap / notes |
|---|---|---|---|---|
| second (video/lv2v) | per elapsed second | ✅ type=live (elapsed s) + unit=hour; type=lv2v = pixel/s |
✅ qty = requested_seconds | #3992 is the proof for this path |
| megapixel/pixel (image, lv2v) | per pixel | ✅ unit=720p → wei/pixel-second (streaming only) |
✅ qty = w×h×n / 2²⁰ | For discrete image gen, #3992's pixel-second is stream-shaped; discrete needs qty extractor |
| image (per-image, nano-banana) | flat per image, qty = n | ❌ not time-based; no per-image charge | ✅ qty = num_images, price fixed/image | #3992 does NOT cover this |
| call (tools) | flat per call, qty = 1 | ❌ #3992 meters seconds, not calls | ✅ qty = 1, price fixed/call | #3992 does NOT cover this |
| characters (TTS) | per character, qty = len(text) | ❌ | ✅ qty = len(text) | Covered by design, not #3992 |
| tokens (LLM, gemini-text) | per token, qty = input/output tokens | ❌ | ❌ (B3: enum omits tokens) |
Residual gap everywhere |
Key correction to the thesis: "fixed" (quantity=1 per-call, or qty=n per-image) is not a new
payment type at all — it is just the ordinary per-unit-quantity mechanism with quantity = 1 (call) or
quantity = num_images (image). #3992's type=live is an orthogonal, time-based thing and does not
supply that. So per-call + per-image remain squarely in PR4 (gateway quantity) + PR5 (signer stamp).
Does #3992's "fixed" reduce the need for billing_unit_kind/billing_unit_quantity?
- For second caps: partially — #3992 shows the signer can bill on a runner-declared decimal price and
branch on a
type. But the quantity is still needed to make image/tools/TTS/tokens reconcile. - For call/image: no reduction. The compute question is simpler (qty trivially 1 or n), but you still
need the fields on the event so pymthouse meters per-unit (PR5/PR6) and the signer can derive
fee = per_unit_price × qty(PR8). - For pixel/second: existing mechanism (and #3992's lv2v/720p path) applies.
- Tokens: residual, unaffected by #3992.
What #3992 gets right for our design:
- Runner advertises its own price. The runner sets
price(decimal USD) +unitin its heartbeat; the orch converts + advertises it on/discovery; the signer honors it. This is exactly the "runner configs its own pricing" goal (NETWORK-PRICING-ARCHITECTURE Part 0.5: descriptoroffering.priceauthoritative; one-orch→many-runner: each runner ships its own price, orch aggregates, signer stamps). - Decimal USD on the wire.
priceasjson.Number(string) +currencymirrors ourdisplay_usd/display_unitintent — a strong precedent to makeoffering.price.display_usdthe wire price the runner declares (not a hand-keptprice.json). - A pricing discriminator on the price.
unit ∈ {hour, 720p}is a real precedent for a pricing kind/type field on the price segment — analogous to ourunit_kind/quantity_sourceadditive fields (PR1). #3992 = "flat vs per-pixel"; ours generalizes to{megapixel, image, second, characters, call, tokens}. - Signer branches fee on a
type+ locks it per session.type ∈ {lv2v, live}withjob type mismatchguard is the same shape PR5/PR8 need to branchfee = per_unit × qtybybilling_unit_kind.
Where it does not yet fit (and must not be over-read):
- Its
type/unitvocabulary is live-video-specific (hour,720p,seconds,720p-pixel-seconds,type=live). It is not a general unit_kind system and cannot represent image/call/characters/tokens. - It removed per-capability/model max price filtering in remote discovery in favor of a global max — orthogonal to our per-cap pricing, but worth noting it narrows price policy granularity.
- The live payment processor still bills pixels (1g) — the elapsed-seconds path is not end-to-end.
Impact on execution-plan PRs:
- PR3 (orch aggregation): #3992 changes the shape of the runner price the orch reads from
/discovery({price, currency:"wei", unit:"seconds"|"720p-pixel-seconds"}instead of{price_per_unit, pixels_per_unit}). PR3's aggregator must parse the new decimal-price+currency/unitschema (and mapunit→pixels_per_unit/unit_kind) when merging intoCapabilitiesPrices. Do not code PR3 against the oldprice_per_unit/pixels_per_unitfields. - PR4 (gateway quantity): unchanged in intent, and specifically still required for
call/image/characters/tokens — #3992 gives you
secondfor free but nothing else. Thetype=livevstype=lv2vrequesttypeis the same field the gateway sends; PR4/PR5 should extend the sametypediscriminator rather than invent a parallel one. - PR5 (signer stamp): #3992 is a near-template — it already added a
TypetoRemotePaymentStateand branches billing on it. PR5 should stampbilling_unit_kind/billing_unit_quantityoncreate_signed_ticketthe same way, and PR8'sfee = per_unit × qtymirrors thecalculateFee( billableUnits, initialPrice)line. Watch: #3992 renamed the emitted log fieldcost_per_pixel→cost— PR5's event-schema/forbidden-field work must account for that rename. - B3 (tokens blocker): unchanged. #3992 does nothing for tokens; the closed enum must still add
tokens(and audittrack, clip, mesh, minute, training, audio_seconds) before freezing.
-
Do not adopt
type=fixedlanguage — it does not exist. Correct the design docs' framing: the live-runner contributes a time/second pricing path (type=live,unit=hour|720p), not a per-call/per-image fixed type. Per-call/per-image "fixed" is justquantity = 1/quantity = nunder the existing per-unit mechanism — no new type needed. -
Reconcile the wire schema. Adopt #3992's decimal
price+currency+unitas the runner price on the wire, and make PR2/PR3 read that shape. Map:- live
unit=hour→unit_kind = second(flat/time),pixels_per_unit = 1. - live
unit=720p→unit_kind = megapixel/pixel(resolution-scaled). This keeps one vocabulary; the live-runner's{hour,720p}becomes a special case ofunit_kind.
- live
-
Map unit_kinds to pricing "type" cleanly (two types, not a zoo):
per_unit(quantity-metered): megapixel, image, second, characters, tokens —fee = price × qty.fixed(quantity ≡ 1): call — a degenerateper_unitwith qty=1, so it needs no special code. Treat "fixed" asper_unitwithquantity_source = const_1, avoiding a parallel fee path.
-
Keep the signer branching pattern from #3992 for PR5/PR8: a single
type/unit_kinddiscriminator, state-locked per session (job type mismatchguard),fee = per_unit_price × billing_unit_quantity. Do not fork into a separate "fixed" signer path. -
Tokens is the real residual (B3). #3992 does not help. Add
tokensto the enum + PR4/PR8 extractors- §5 parity matrix, and audit the other live display units before freezing.
-
Flag the two #3992-specific follow-ups so our PRs don't inherit them: (a) the
live_payment_processor.gopixel-based accounting bug (1g) is still open; (b) remote discovery moved to a global max price.
Bottom line for the docs: fold #3992 in as evidence the architecture works for the second unit and
for runner-declared decimal USD pricing, and as the template for the signer's type-branched fee — not
as a per-call/per-image "fixed" solution. Per-call and per-image stay on PR4/PR5 (quantity = 1 / n), and
tokens remains the one true gap (B3).