feat(echo): run on-chain with metered payments - #50
Merged
Conversation
echo was the one example without an on-chain path, and the pricing column in the repo README was blank for it. A live stream is exactly what metered pricing exists for: the session is billed per second for as long as the client holds it, with no call boundary to bill against. Registering a price makes the orchestrator meter the session, so the client keeps it funded for its lifetime and lets go of the funding before releasing it. The upfront 402 payment only covers the signer's preroll, so a stream that runs longer is the case this example puts on-chain. Needs livepeer/livepeer-python-gateway#53: before it, a reserved session could not pay again and died at the preroll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The commands pointed at ~/samples/bbb_720p.mp4, which only exists on one machine, so the first thing a reader runs fails. Generate the clip with ffmpeg instead: no download to rot, and -t sets the length, which matters on-chain where the stream has to outlive the preroll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The on-chain section showed only the file command, so running the ffmpeg or webcam pipe against a paid stack fails at the payment challenge and ffmpeg reports a broken pipe, which reads like a camera problem rather than an unpaid caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Metered sessions need the session-scoped payment URL that go-livepeer #4008 adds to the runner payment challenge. It landed after v0.9.0, so on the release tag the SDK cannot keep a session funded and echo stops at reservation. Pin the orchestrator and signer to the master build that carries it, and re-pin once a release ships with the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The signer signs at most 100 tickets per payment, and a payment needs fee / ticketEV of them. A metered session pays for a 10s window upfront, so at -ticketEV=1e9 the demo orchestrator topped out around 0.067 USD per hour and echo's advertised 0.10 asked for 150 tickets, which the signer refused. Raise it to 1e10: echo now signs 15 tickets upfront and 6 per top-up, and the ceiling moves to about 0.67 USD per hour. Correct the ticket math in echo's .env.example, which claimed the old value had room to spare. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a fully on-chain (paid) path for the echo example using metered (per-second) pricing, and updates the shared demo stack configuration to support metered sessions end-to-end.
Changes:
- Add on-chain compose overlay + env template for
echo, and document how metered sessions charge multiple times over the session lifetime. - Extend
echorunner registration to accept a configurable USD/hour price; extend theechoclient to optionally pay via a remote signer and hold funding while the session is in-use. - Update shared orchestrator/signer compose configuration (image pin +
-ticketEV) to support metered session payments.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates examples table to reflect echo pricing as hourly/metered. |
| echo/runner.py | Adds --price and registers the runner with a USD/hour price for metered billing. |
| echo/client.py | Adds --signer support and keeps the session funded while in an async with block. |
| echo/README.md | Documents on-chain paid run flow and explains why metered sessions pay multiple times. |
| echo/compose.yml | Updates usage comment to match current CLI (--mode). |
| echo/compose.onchain.yml | New overlay to run echo on-chain and register a non-zero price. |
| echo/.env.example | New env template for on-chain setup + ticket/price cap guidance. |
| compose.orchestrator.yml | Pins orchestrator image to master SHA to pick up metered-session payment URL support. |
| compose.onchain.yml | Pins signer image to same SHA and raises -ticketEV to avoid exceeding signer ticket limits. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
api-proxy and vllm restate the whole orchestrator command to add -liveRunnerConfig, since `extends` cannot append a flag, so each kept its own -ticketEV and never saw the shared bump. vllm is metered, so it would have hit the signer's 100 ticket cap the moment its price rose above about 0.067 USD per hour. Bump both, and correct the ticket math the fixed price examples quote in their .env.example. Also say in the README that the demos now run a master build rather than the v0.9.0 release image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hello-world and tiles both explain their one idea in a "what this shows" section and close with a Run without Docker recipe; echo had neither, so its distinguishing idea (a session the client holds open, which is what makes billing a lifecycle) only appeared inside the on-chain section. Add both, in the same order the other examples use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same ceiling and the same cap rule, said in half the lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The on-chain command passes both --discovery and --signer, which reads as if the signer takes part in discovery. It does not: the SDK only falls back to signer discovery when no discovery URL is given, and the demo signer runs without -remoteDiscovery anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (2)
echo/client.py:205
- To support the documented
--discovery ''usage (blank meaning “omit discovery so the SDK can fall back to the signer”), normalizeargs.discoverybefore callingreserve_session. Right now an empty string is passed through asdiscovery_url, which is likely treated as an invalid URL rather than “unset.”
session = await reserve_session( # Livepeer: 1
discovery_url=args.discovery, # omit if the signer does discovery itself
app=APP_ID,
signer_url=args.signer.strip() or None,
)
echo/README.md:105
- This sentence suggests
v0.9.0is sufficient for "Run without Docker", but a few lines later you note the paid/metered path requires a build afterv0.9.0(#4008). Reword here to avoid implying thatv0.9.0supports the on-chain metered flow.
Start an orchestrator built from go-livepeer `v0.9.0` or newer (see [Build from source](https://docs.livepeer.org/v1/orchestrators/guides/install-go-livepeer#build-from-source)), then the app and client directly:
Keep the rule and one worked number. Why 1e9 was too low belongs in the commit that raised it, not in the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prose said "make one if you have nothing to hand" while the block made one unconditionally. Put the condition on the line it applies to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (1)
echo/client.py:202
- If the user follows the README instruction to disable orchestrator discovery with
--discovery '', this currently passes an empty string through toreserve_session(...). That isn’t the same as “omitting” the discovery URL and may be treated as an invalid URL by the SDK. Coerce blank/whitespace-only values toNone(as you already do for--signer) so the paid-path fallback works reliably.
session = await reserve_session( # Livepeer: 1
discovery_url=args.discovery, # omit if the signer does discovery itself
app=APP_ID,
signer_url=args.signer.strip() or None,
)
The signer discovery note is true of every example, so it belongs in the repo README, not here. The closing contrast restated what this section and tiles already say about metered vs fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
echo was the only example with no on-chain path, and the lone blank in the repo README's pricing column. A live stream is what metered pricing exists for: there is no call boundary to bill against, so the session is billed per second for as long as the client holds it.
compose.onchain.ymland.env.example, mirroringtiles, withPRICEas USD per hour.runner.pytakes--priceand registers it; the base compose still registers free.client.pytakes--signer, and holds the session inasync withso funding stops before the session is released.hello-worldandtiles.Shared stack changes
Two things in the shared demo stack had to change before the documented commands worked. Both affect every on-chain example, neither changes what the fixed-price ones do.
payment_urland the SDK fails at reservation withLive runner payment challenge missing payment_url.compose.orchestrator.ymlandcompose.onchain.ymlnow pin the master buildsha-cc49228. Re-pin to a release tag once one ships with #4008.-ticketEV. The signer signs at most 100 tickets per payment, and a payment needsfee / ticketEVof them. A metered session pays for a 10s window upfront, so at1e9the demo orchestrator topped out near 0.067 USD per hour and the advertisedPRICE=0.10asked for 150 tickets:numTickets 150 exceeds maximum of 100. Raised to1e10, which puts the ceiling near 0.67 USD per hour.api-proxyandvllmrestate the whole orchestrator command to add-liveRunnerConfig, sinceextendscannot append a flag, so each carried its own-ticketEVand would have missed the bump entirely.vllmis metered, so it would have hit the same ticket cap as soon as its price rose above about 0.067 USD per hour. Both are bumped to match, and the ticket math quoted in the fixed-price examples'.env.examplefiles is corrected (the ceiling is now ~0.0019 USD per call, not ~0.00019).Why this example matters on-chain
hello-world,tilesandapi-proxyare all fixed price: the upfront 402 payment settles the whole bill and the orchestrator starts no ticker. echo is the first example where payment is a lifecycle rather than a transaction, so it is the one that actually exercises the metered path end to end.Tested
Run end to end on Arbitrum One with a funded signer, using the compose files in this branch and the command from the README, against livepeer/livepeer-python-gateway
ja/live-runner@995f93f(includes livepeer/livepeer-python-gateway#53):A 30s 720p30 clip went in and 900 frames came back out with the blur applied. The session paid nine times, not once: 15 tickets upfront for the signer's 10s preroll, then eight top-ups of 6 tickets each while the orchestrator debited every 5s. On client exit the orchestrator logged
live runner session released ... duration=26.69sfollowed byDone processing payments for session, so funding stopped before release, which is what the client'sasync with sessionis for.Because both shared-stack changes touch every on-chain example,
tileswas run on-chain too, on the same pinned stack: 9 paid tile calls, one signed payment each at 6 tickets, nono ticketsand no ticket-cap errors. The fixed-price path is unaffected.Also lint/format via pre-commit.