From 9fe674ff655008ad61e39eb07adb8615cafab467 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 17 Jun 2026 17:39:11 +0200 Subject: [PATCH] docs: add run-a-job and payment-provider walkthroughs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft developer/operator docs for the clearinghouse: - run-a-job.md (Tier 1, consume path): find an app, call it through the SDK across HTTP/SSE/trickle/WebSocket transports, and pay via a signer. Grounded in the livepeer-python-gateway SDK and example apps. - payment-provider.md (Tier 2, operator role): payments on Livepeer, then three paths — use a hosted signer, run your own remote signer, or run the clearinghouse suite (auth/metering/billing via OpenMeter, OAuth, Stripe). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/payment-provider.md | 192 +++++++++++++++++++++++++++++++++++++++ docs/run-a-job.md | 145 +++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 docs/payment-provider.md create mode 100644 docs/run-a-job.md diff --git a/docs/payment-provider.md b/docs/payment-provider.md new file mode 100644 index 0000000..f580dad --- /dev/null +++ b/docs/payment-provider.md @@ -0,0 +1,192 @@ +--- +title: "Payment Provider" +description: "Operate the network's payment infrastructure: use a hosted signer, run your own, or run the full clearinghouse suite for auth, metering, and billing." +--- + +> **Source doc — Tier 2 (Payment Provider role).** Written to split into the official Mintlify docs as a new operator role beside Orchestrator / Gateway / Delegator. The consume side (running a job, attaching a key) lives in **[Run a job on the network](./run-a-job.md)**. +> +> **Status legend:** *Shipped* — in go-livepeer today. *In build* — designed, not yet shipped (`livepeer/clearinghouse` is a scaffold; items marked *in build* are placeholders to finalize at build time). + +## Payments on Livepeer + +Livepeer is a decentralized network: every job is paid by sending the orchestrator a **probabilistic-micropayment (PM) ticket** that settles on-chain in ETH. A **signer** holds the wallet and mints those tickets behind a plain HTTP endpoint — so your app pays with just a URL and a header, no wallet or crypto in your code. + +Because the signer is a **separate node**, a local SDK can run jobs and pay orchestrators directly — no gateway in the path. Anything beyond raw payment — identity, balances, currency, billing — is added by a **clearinghouse** that sits *beside* the signer, never inside it. So there are two components, and you can stop at the first: + +- **Remote signer** — mints the PM tickets that pay orchestrators. *(Shipped.)* +- **Clearinghouse** — the optional accounting layer on top: identity, balances, a spending gate, metering, funding. *(In build.)* + +## Choose how you pay + +By how much you want to operate yourself: + +| Path | You run | Use when | +|---|---|---| +| **[Use a hosted signer](#use-a-hosted-signer)** | nothing | you just want a key — someone else runs the infrastructure | +| **[Run a remote signer](#run-a-remote-signer)** | a signer node | you pay for *your own* jobs from a trusted network — no accounts needed | +| **[Run the clearinghouse suite](#run-the-clearinghouse-suite)** | signer + accounting | you serve *other* customers: identity, balances, metering, billing | + +## Use a hosted signer + +A hosted signer is a **managed clearinghouse** — someone else runs the signer, the accounting, and the funding rails, and gives you a key. Crucially, they bill you in **fiat** (cards, credits, dollars), so you never touch crypto, hold a wallet, or manage ETH. For your app it's the [signer step in Run a job](./run-a-job.md#4-pay-for-the-job-the-signer-step): sign up, top up, and pass `signer_url` + `signer_headers`. + +[PymtHouse](https://pymthouse.com) is a community-hosted option — billing, identity, payments, a UI, multi-cloud deploy — already onboarding apps, and more will follow. Each clearinghouse run *for the network* is an SPE governance proposal built on the same lean core described below. + +## Run a remote signer + +The signer is a standalone go-livepeer node (`-remoteSigner`) that holds the Ethereum key and signs PM tickets on demand, keeping the key out of the untrusted media path. It is **stateless**: it mints tickets, optionally asks a webhook "ok?", and tracks no customers or balances. + +```mermaid +sequenceDiagram + participant G as Gateway / SDK + participant S as Remote signer + participant O as Orchestrator + G->>S: POST /sign-orchestrator-info + S-->>G: address, signature + G->>O: GetOrchestratorInfo(address, sig) + O-->>G: OrchestratorInfo (TicketParams, price) + loop per payment + G->>S: POST /generate-live-payment (OrchestratorInfo + signed state) + Note over S: mint PM ticket batch (ETH)
optional auth webhook → allow / deny
emit create_signed_ticket + S-->>G: payment, segCreds, new signed state + G->>O: POST /payment + end +``` + +**Run it right next to your app.** With no webhook configured (`-remoteSignerWebhookUrl` unset), a signer is a complete, self-contained way to pay for *your own* jobs — put it on the same private network as the SDK that requests jobs and you need no clearinghouse, no auth, no gate: + +```bash +livepeer -remoteSigner -network arbitrum-one-mainnet -ethUrl \ + -ethKeystorePath -maxPricePerUnit -httpAddr 0.0.0.0:8935 +``` + +Every `/generate-live-payment` request is then allowed and signed from the signer's own wallet. The only guardrails are pricing caps and a hard ≤100 tickets-per-request cap — there is **no per-caller auth, spend cap, or rate limit**. So: + +- **Safe for self-use** — your app and your signer in one trust domain, paying for your jobs. No attach key, no gate. +- **Never expose it** — anyone who can reach it can drain your wallet. Keep it internal behind infrastructure controls; being stateless, run it redundantly behind round-robin DNS. + +The moment you want *others* to call it, point it at a webhook — that's the clearinghouse. + +Key properties: + +- **Needs an on-chain network** (ETH connectivity to manage tickets). Your SDK stays offchain — it only needs the signer URL and a header. +- **Stateless via a signed state blob.** Each response returns a blob the caller stores and sends back next time; the signer signs and verifies it, so no shared DB is needed. +- **Pricing is enforced on the signer** (`-maxPricePerUnit`, max ticket EV). +- **Key custody.** For production, delegate signing to the [`external-signer`](https://github.com/livepeer/external-signer) sidecar (Web3Signer → Turnkey / Fireblocks / Privy / Dfns) so the key never leaves your custody provider: `livepeer -remoteSigner -ethExternalSigner http://127.0.0.1:8550`. + +## Run the clearinghouse suite + +The clearinghouse **suite** is our payment logic packaged to run inside *your* app or infrastructure — so you can serve *other* customers without building accounting from scratch. It turns "mint a ticket" into "draw down this customer's balance," and integrates with established auth, metering, billing, and fiat-payment systems rather than reinventing them. It runs on an **unmodified** signer, plugging into three touchpoints the signer already exposes. *(In build.)* + +| Touchpoint | Direction | Purpose | +|---|---|---| +| **Auth webhook** (`-remoteSignerWebhookUrl`) | signer → clearinghouse | the gate — allow/deny per payment | +| **Request headers** (`-remoteSignerHeaders`) | gateway → signer → webhook | customer identity (e.g. an API key) | +| **`create_signed_ticket` events** | signer → bus (Kafka) | usage metering (fee in wei, pixels, `num_tickets`, `manifest_id`) | + +It is built on **ports & adapters** — a stable interface (*port*) with swappable implementations (*adapter*) — so you integrate proven tools instead of hand-rolling auth, metering, or billing: + +| Concern | Port | Available (v1) | Planned (drop-in) | +|---|---|---|---| +| Identity | `IdentityResolver` | API key | OIDC / OAuth | +| Metering + billing | `Ledger` | OpenMeter | Lago, Kill Bill | +| Funding | `FundingRail` | Grants (operator-issued) | Stripe, USDC, x402 | + +```mermaid +flowchart LR + GW[Gateway / SDK
API key header] --> SIG[Remote signer
mint PM tickets → ETH] + SIG -- gate webhook --> CORE + SIG -- usage events --> CORE + subgraph CORE[clearinghouse · lean core] + GATE[gate · allow/deny] + METER[usage ingest · metering] + KEYS[keys + admin/grant] + end + IDP[Identity adapter] -. resolve .-> CORE + CORE --> LEDGER[(Ledger adapter · OpenMeter)] + ONRAMP[Funding · grants → Stripe / USDC / x402] --> LEDGER +``` + +The core does two things: + +- **Gate** — answer the signer's auth webhook: resolve the customer from the header, check their balance, allow or deny. A rejection means no usable payment, so it's a **pre-spend** gate. +- **Meter** — consume `create_signed_ticket` events and draw the balance down. **Idempotent** (dedupe on `request_id`/`sequence_number`) and **gap-tolerant** (reconstruct missed events — exactly-once delivery isn't guaranteed). + +Keep **usage** and **balance** as separate ledgers with an explicit reconciliation step. Bill on minted-ticket EV (the signer reports fees in wei); use the wallet's on-chain ETH spend only as a later *aggregate* cross-check, not a per-customer match (PM is probabilistic). + +**Funding starts with grants** — an operator issues a customer a trial budget, the operator's wallet carries the ETH cost; real on-ramps (Stripe / USDC / x402) plug in later behind the `FundingRail` port. **Auth** is a personal API key for v1; OIDC/OAuth comes later via the `IdentityResolver` adapter. + +```bash +docker compose up # in build: signer + clearinghouse + OpenMeter +``` + +**Acceptance loop:** grant → run a job → balance draws down → signing hard-stops at $0. + +**Design principles:** minimal first · never fork the signer (external, stable contract) · ports & adapters · delegate accounting to a proven engine · accounting first, on-ramps later · no auth platform in the core. + +> **In build:** concrete API-key/grant/admin endpoints, config keys, and the compose file land here once `livepeer/clearinghouse` ships them. + +--- + +## Reference + +### Signer flags (go-livepeer) + +| Flag | Mode | Purpose | +|---|---|---| +| `-remoteSigner` | signer | run the remote signer service (requires an on-chain network) | +| `-remoteSignerUrl` | gateway | URL of the signer to attach to | +| `-remoteSignerHeaders` | gateway | headers sent gateway → signer, e.g. `header:val,header2:val2` (identity channel) | +| `-remoteSignerWebhookUrl` | signer | auth-webhook URL called during `/generate-live-payment` (the gate) | +| `-remoteSignerWebhookHeaders` | signer | headers sent on webhook requests | +| `-remoteDiscovery` | signer | enable orchestrator discovery via `/discover-orchestrators` | +| `-maxPricePerUnit` | signer | reject orchestrator prices above this (returns 481) | +| `-ethExternalSigner` | signer | delegate ETH signing to a Web3Signer sidecar (`external-signer`) | + +### Endpoints & status codes + +`POST /sign-orchestrator-info` · `POST /generate-live-payment` · `POST /sign-byoc-job` · `GET /discover-orchestrators` + +`480` refresh session · `481` price exceeded · `482` no tickets needed. + +### Auth webhook contract + +Fired after the payment state is updated but **just before it is signed** — a rejection means no usable payment, making it a pre-spend gate. + +```json +// request (signer → webhook) +{ "Headers": { "Authorization": ["Bearer "] }, + "State": { "...": "RemotePaymentState — StateID, PMSessionID, OrchestratorAddress, Balance, ..." } } + +// response (webhook → signer) +{ "Status": 200, "Reason": "", "Expiry": 1718630400 } +``` + +- `Status` — HTTP status returned to the caller; non-200 denies the payment. +- `Reason` — message when denied. +- `Expiry` — Unix seconds to cache this allow decision (signer skips the webhook while `AuthExpiry > now`); `0` = no caching. + +### `create_signed_ticket` Kafka event + +| Field | Meaning | +|---|---| +| `session_id`, `session_status` | opaque state ID; `new` / `continuing` | +| `pipeline` | e.g. `live-video-to-video` | +| `request_id` | per-request UUID (dedupe key) | +| `orch_address`, `orch_url` | orchestrator ETH address + service URL | +| `manifest_id`, `pm_session_id` | stream + PM session identifiers | +| `current_time(_unix)`, `previous_time(_unix)` | timing for usage measurement | +| `billable_secs`, `pixels` | usage measured | +| `computed_fee`, `cost_per_pixel`, `session_balance` | fee (wei), price ratio, balance after update | +| `sequence_number`, `num_tickets` | request count; tickets minted | + +--- + +## Appendix: source material + +- Vision: *Improve App Payment Experience* (Notion); Payments Clearinghouse / Livepeer: Weir. +- Signer code: `server/remote_signer.go`, `core/accounting.go`, `cmd/livepeer/starter/flags.go`; `doc/remote-signer.md`. +- Custody sidecar: [`livepeer/external-signer`](https://github.com/livepeer/external-signer). +- SDK: [`livepeer/livepeer-python-gateway`](https://github.com/livepeer/livepeer-python-gateway). +- Community hosted clearinghouse: [`eliteprox/pymthouse`](https://github.com/eliteprox/pymthouse). +- Forum: [Livepeer Payment Clearinghouse](https://forum.livepeer.org/t/livepeer-payment-clearinghouse/3264). diff --git a/docs/run-a-job.md b/docs/run-a-job.md new file mode 100644 index 0000000..64add3f --- /dev/null +++ b/docs/run-a-job.md @@ -0,0 +1,145 @@ +--- +title: "Run a job on the network" +description: "Find an app on the network, call it through the SDK, and point it at a signer to pay — no wallet, no crypto in your app." +--- + +> **Developer docs source — Tier 1.** Illustrative walkthrough of the target developer experience. Code reflects the current [`livepeer-python-gateway`](https://github.com/livepeer/livepeer-python-gateway) SDK; names may still change. + +Three moves: **find** an app, **call** it through the SDK, point it at a **signer** to pay. Your media flows straight to the orchestrator; the payment layer never sees it — it's just a URL and a key. + +## 1. Find an app to run + +An *app* is a container that runs on the network — a realtime video pipeline, an LLM, an image model, or anything you bring yourself. Orchestrators host your container and pass traffic straight through over **HTTP, SSE, WebSocket, or trickle** (Livepeer's segment-based transport for realtime media), so you can lift an app off your old compute provider with little change: the orchestrator pulls your image and runs it on its GPUs. + +The runtime is in **beta** — there's no self-serve onboarding yet, so to put *your own* app on the network you [contact the team](#bring-your-own-app). You can already run the **open-source example apps** live today. Find them two ways: + +- **Dashboard** — [livepeer.org/dashboard](https://livepeer.org/dashboard), a small hosted app for trying the network. +- **Discovery endpoint** — [`GET /v1/discovery/capabilities`](https://discovery-service-production-8955.up.railway.app/docs#tag/discovery/GET/v1/discovery/capabilities) lists the available apps and models. + +Each app has an **app id** (e.g. `livepeer-sample/hello-world`) — that's all the SDK needs to route to it. + +## 2. Install the SDK + +Not on PyPI yet — install from the `ja/live-runner` branch: + +```bash +pip install "git+https://github.com/livepeer/livepeer-python-gateway@ja/live-runner" +``` + +## 3. Run a job + +`reserve_session` handles discovery and orchestrator selection from an app id — the same call for every app. What you do *after* it depends on the app's transport. Start with `hello-world`, a plain HTTP request/response app: + +```python +from livepeer_gateway.selection import reserve_session +from livepeer_gateway.live_runner import call_runner, stop_runner_session + +SIGNER_URL, SIGNER_HEADERS = "https://api.pymthouse.com", {"Authorization": "Bearer sk_live_..."} # see §4 + +session = await reserve_session( + discovery_url="https://localhost:8935/discovery", + app="livepeer-sample/hello-world", + signer_url=SIGNER_URL, signer_headers=SIGNER_HEADERS, +) +result = await call_runner( + runner_url=session.app_url.rstrip("/") + "/hello", + payload={"name": "livepeer"}, + signer_url=SIGNER_URL, signer_headers=SIGNER_HEADERS, +) +print(result.data) # {'message': 'Hello, livepeer!'} +await stop_runner_session(session) +``` + +The other example apps keep the same `reserve_session` and swap the transport: + +| Example | Transport | App id | +| --- | --- | --- | +| [`hello-world`](https://github.com/livepeer/live-runner-example-apps/tree/main/hello-world) | HTTP request/response | `livepeer-sample/hello-world` | +| [`vllm`](https://github.com/livepeer/live-runner-example-apps/tree/main/vllm) | SSE (token streaming) | `vllm/qwen2.5-0.5b-instruct` | +| [`echo`](https://github.com/livepeer/live-runner-example-apps/tree/main/echo) | trickle (realtime video) | `livepeer-sample/echo` | +| [`streaming-asr`](https://github.com/livepeer/live-runner-example-apps/tree/main/streaming-asr) | WebSocket (speech-to-text) | `whisper/base.en` | + +### SSE — streaming tokens (`vllm`) + +`vllm` serves an OpenAI-compatible API, so you use the **stock `openai` client** with zero Livepeer code. Payment lives in a small local [`gateway.py`](https://github.com/livepeer/live-runner-example-apps/blob/main/vllm/gateway.py) that does discovery + the ticket handshake for you (forwarding the runner's `text/event-stream` straight through with `call_runner(..., stream=True)`) — point it at your signer once (see §4): + +```bash +uv run gateway.py --signer https://api.pymthouse.com # OpenAI endpoint on http://localhost:8080/v1 +``` + +Your client is then plain OpenAI against that `base_url`: + +```python +from openai import OpenAI + +client = OpenAI(base_url="http://localhost:8080/v1", api_key="unused") # gateway handles discovery + payment +stream = client.chat.completions.create( + model="Qwen/Qwen2.5-0.5B-Instruct", + messages=[{"role": "user", "content": "write a haiku about GPUs"}], + stream=True, +) +for chunk in stream: + print(chunk.choices[0].delta.content or "", end="", flush=True) # tokens arrive as generated +``` + +### Trickle — realtime video (`echo`) + +POST to the app's endpoint to open its trickle `in`/`out` channels, then publish frames to `in` and read transformed frames from `out`, keeping the long-lived session funded in the background: + +```python +from livepeer_gateway.http import post_json +from livepeer_gateway.media_publish import MediaPublish +from livepeer_gateway.media_output import MediaOutput + +session = await reserve_session(discovery_url="https://localhost:8935/discovery", + app="livepeer-sample/echo", + signer_url=SIGNER_URL, signer_headers=SIGNER_HEADERS) +session.start_payments() # tops up the session balance while it stays open + +channels = await post_json(session.app_url.rstrip("/") + "/echo", {"radius": 75}) # → {"in": ..., "out": ...} +publisher = MediaPublish(channels["in"]) # publish your frames here +async with MediaOutput(channels["out"], on_bytes=write): # read transformed frames back + ... # pump frames — see the echo client +await session.aclose() # stops payments + releases the session +``` + +### WebSocket — speech-to-text (`streaming-asr`) + +`open_websocket` opens the orchestrator-proxied socket from a reserved session and keeps it funded — stream audio up, get transcripts back on the same socket: + +```python +from livepeer_gateway.live_runner import open_websocket + +session = await reserve_session(discovery_url="https://localhost:8935/discovery", + app="whisper/base.en", + signer_url=SIGNER_URL, signer_headers=SIGNER_HEADERS) +async with open_websocket(session, "/transcribe") as ws: # SDK does the wss upgrade + session payments + await ws.send_bytes(pcm_16khz_mono) # stream 16 kHz mono PCM up (your audio bytes) + await ws.send_str("eos") # signal end of audio + async for msg in ws: # transcripts stream back + print(msg.json()) # {"text": "...", "final": false|true} +``` + +## 4. Pay for the job — the signer step + +Livepeer is a **decentralized network**: jobs are paid by sending the orchestrator a probabilistic-micropayment ticket that settles on-chain in ETH. Rather than hold a hot wallet in your app, you point it at a **signer** that mints tickets for you — and a **clearinghouse** can sit on top to handle accounts and billing in plain dollars. `signer_url` + `signer_headers` is the *only* payment config your app needs — no wallet, no crypto in your code. + +**Fastest path: a community-hosted signer.** [PymtHouse](https://pymthouse.com) hosts the signer and accounting — sign up, get a key, drop it in: + +```python +SIGNER_URL = "https://api.pymthouse.com" +SIGNER_KEY = "sk_live_..." +``` + +It meters usage and bills your account; when the balance runs out, jobs stop. Switching providers is just a different URL and key. + +**Run it yourself?** The [Payment Provider](./payment-provider.md) docs cover the whole stack: + +- **[Run a remote signer](./payment-provider.md#run-a-remote-signer)** — mint your own tickets for your own jobs, no accounts. +- **[Run the clearinghouse suite](./payment-provider.md#run-the-clearinghouse-suite)** — the accounting layer on top, to give customers a balance and bill them. + +The clearinghouse is a **suite**: a lean core with swappable ports, so you plug in established solutions instead of rolling your own — [OpenMeter](https://openmeter.io) for metering/billing, OAuth/OIDC for auth, Stripe / USDC for funding. + +## Bring your own app + +Start from [`live-runner-example-apps`](https://github.com/livepeer/live-runner-example-apps) — each example runs locally with `docker compose` against a local orchestrator, so you can build and test your app end to end on your own GPUs **for free**: offchain, omit the signer entirely (`reserve_session(discovery_url="https://localhost:8935/discovery", app=...)`) — it's only needed to pay real orchestrators on the network. Once it's running locally and you want it live, reach out at [partnerships@livepeer.foundation](mailto:partnerships@livepeer.foundation) and we'll onboard it.