Skip to main content
This tutorial walks through a complete end-to-end test of the Livepeer gateway + orchestrator pipeline using BYOC (Bring Your Own Container) with a CPU-only Docker container. No GPU is required. By the end you will have:
  • A go-livepeer orchestrator running locally accepting BYOC jobs
  • A go-livepeer gateway running in off-chain mode pointed at a community remote signer
  • A simple CPU Docker container registered as a BYOC pipeline
  • A verified end-to-end job sent from your gateway through your orchestrator and back
  • A clear path to taking this setup to production on-chain
This tutorial uses the off-chain gateway mode (remote signer) for simplicity. Off-chain mode was introduced in Q4 2025 via PRs #3791 and #3822. It is the recommended starting point for all new AI and BYOC gateway deployments.
BYOC vs LV2V: BYOC uses a different job flow from LV2V (live-video-to-video). BYOC does not use gRPC to fetch OrchestratorInfo — discovery is done by HTTP capability query. The gateway’s start-stream request can explicitly set an allow-list or block-list of orchestrators. This makes BYOC well-suited to browser and CPU-only deployments where gRPC dependencies are undesirable.

Prerequisites

Docker

Docker Engine 24+ installed and running. Verify with docker --version.

go-livepeer binary

Latest tagged release from github.com/livepeer/go-livepeer/releases. Linux amd64 recommended.

Python 3.10+

Required if you want to use the Python gateway SDK for sending test jobs. Optional for the go-livepeer-only path.

Free ports

The following ports must be free on your machine: 7935 (orchestrator HTTP), 8935 (gateway AI API), 9090 (orchestrator metrics — optional).
You do not need:
  • A GPU
  • An Ethereum wallet or ETH
  • An Arbitrum RPC endpoint
  • On-chain registration
The community remote signer at https://signer.eliteencoder.net/ handles all Ethereum operations for you during testing.

Architecture overview

Your machine
┌─────────────────────────────────────────────────────────────────┐
│                                                                   │
│  ┌──────────────┐    job request     ┌────────────────────────┐  │
│  │   Gateway    │ ─────────────────> │     Orchestrator       │  │
│  │  (port 8935) │ <───────────────── │     (port 7935)        │  │
│  │  off-chain   │    job result      │                        │  │
│  └──────┬───────┘                   │  ┌──────────────────┐  │  │
│         │                           │  │  BYOC Container  │  │  │
│         │ signTicket                │  │  (CPU pipeline)  │  │  │
│         ▼                           │  └──────────────────┘  │  │
│  ┌──────────────┐                   └────────────────────────┘  │
│  │Remote Signer │                                                 │
│  │(external)    │                                                 │
│  └──────────────┘                                                 │
└─────────────────────────────────────────────────────────────────┘
The gateway handles no Ethereum operations itself. All payment signing is delegated to the community remote signer. The orchestrator is registered locally only — no on-chain registration needed for this smoke test.

Part 1: Build the CPU BYOC container

We will use a minimal Python image that echoes video frames back — a CPU-only passthrough pipeline useful for testing the full job flow without any GPU or model inference.
1

Create the pipeline directory

mkdir byoc-cpu-test
cd byoc-cpu-test
2

Write the pipeline code

Create pipeline.py:
import asyncio
import logging

from runner.live.pipelines import Pipeline, BaseParams
from runner.live.trickle import VideoFrame, VideoOutput


class PassthroughParams(BaseParams):
    """No custom parameters needed for passthrough."""
    pass


class PassthroughPipeline(Pipeline):
    """
    CPU-only passthrough pipeline for BYOC smoke testing.
    Echoes every incoming video frame directly to the output queue.
    No model loading, no GPU required.
    """

    def __init__(self):
        self.frame_queue: asyncio.Queue[VideoOutput] = asyncio.Queue()
        self._running = True

    async def initialize(self, **params):
        logging.info("PassthroughPipeline: initialized (CPU, no model loading)")

    async def put_video_frame(self, frame: VideoFrame, request_id: str):
        if self._running:
            # Pass the frame through unchanged
            await self.frame_queue.put(VideoOutput(frame, request_id))

    async def get_processed_video_frame(self) -> VideoOutput:
        return await self.frame_queue.get()

    async def update_params(self, **params):
        logging.info(f"PassthroughPipeline: params update received: {params}")

    async def stop(self):
        self._running = False
        logging.info("PassthroughPipeline: stopped")

    @classmethod
    def prepare_models(cls):
        logging.info("PassthroughPipeline: no models to prepare (CPU passthrough)")
3

Write the entrypoint

Create main.py:
import os
from runner.app import start_app
from runner.live.pipelines import PipelineSpec

pipeline_spec = PipelineSpec(
    name="passthrough-cpu",          # This is your model_id in go-livepeer
    pipeline_cls="pipeline:PassthroughPipeline",
    params_cls="pipeline:PassthroughParams",
    initial_params={},
)

if __name__ == "__main__":
    start_app(pipeline=pipeline_spec)
4

Write the Dockerfile

Create Dockerfile:
# Pin to a specific ai-runner live-base version
ARG BASE_IMAGE=livepeer/ai-runner:live-base
FROM ${BASE_IMAGE}

WORKDIR /app

# Copy pipeline files
COPY pipeline.py ./pipeline.py
COPY main.py ./main.py

# No additional dependencies needed for CPU passthrough

ENV HF_HUB_OFFLINE=1

ENTRYPOINT ["python", "main.py"]
5

Build the Docker image

docker build -t byoc-cpu-passthrough:latest .
Expected output ends with:
Successfully tagged byoc-cpu-passthrough:latest
Verify:
docker images | grep byoc-cpu-passthrough

Part 2: Run the orchestrator

The orchestrator accepts jobs from the gateway and routes them to the BYOC container.
1

Download the go-livepeer binary

# Replace vX.Y.Z with the latest release from github.com/livepeer/go-livepeer/releases
curl -LO https://github.com/livepeer/go-livepeer/releases/download/vX.Y.Z/livepeer-linux-amd64.tar.gz
tar -xzf livepeer-linux-amd64.tar.gz
chmod +x livepeer livepeer_cli
2

Start the BYOC container

Start your pipeline container before the orchestrator so it is ready when the orchestrator registers capabilities:
docker run -d \
  --name byoc-cpu-passthrough \
  --network host \
  -p 8000:8000 \
  byoc-cpu-passthrough:latest
Verify the container is running:
docker logs byoc-cpu-passthrough
# Should see: PassthroughPipeline: initialized (CPU, no model loading)
3

Start the orchestrator

Run the orchestrator in -orchestrator mode, advertising the BYOC capability and pointing at the running Docker container:
./livepeer \
  -orchestrator \
  -serviceAddr 0.0.0.0:8935 \
  -cliAddr 127.0.0.1:7935 \
  -byoc \
  -byocContainerURL http://localhost:8000 \
  -byocModelID passthrough-cpu \
  -pricePerUnit 1 \
  -network offchain \
  -datadir ./data-orchestrator
Flag explanation:
  • -orchestrator — run in orchestrator mode
  • -serviceAddr — the address the gateway will connect to
  • -cliAddr — local CLI management port
  • -byoc — enable BYOC mode
  • -byocContainerURL — URL of the running BYOC Docker container
  • -byocModelID — must match the name in your pipeline’s PipelineSpec
  • -pricePerUnit 1 — set a nominal price (1 wei equivalent) for testing
  • -network offchain — no Ethereum dependency, local registration only
  • -datadir — separate data directory from the gateway
Successful startup logs include:
Orchestrator registered with service address 0.0.0.0:8935
BYOC capability registered: passthrough-cpu

Part 3: Run the gateway (off-chain mode)

1

Start the gateway with remote signer

In a new terminal, run the gateway in off-chain mode pointed at the community remote signer and your local orchestrator:
./livepeer \
  -gateway \
  -cliAddr 127.0.0.1:7936 \
  -httpAddr 0.0.0.0:8935 \
  -orchAddr http://localhost:8935 \
  -remoteSignerAddr https://signer.eliteencoder.net \
  -network offchain \
  -datadir ./data-gateway
Flag explanation:
  • -gateway — run in gateway mode
  • -cliAddr — separate CLI port from the orchestrator (must differ)
  • -httpAddr — AI API port that your applications will call
  • -orchAddr — point directly at your local orchestrator
  • -remoteSignerAddr — the community-hosted remote signer (provides free ETH for testing)
  • -network offchain — no Arbitrum RPC required
  • -datadir — separate data directory
Successful startup logs include:
Gateway started on :8935
Connected to remote signer at https://signer.eliteencoder.net
Registered orchestrator: localhost:8935
The remote signer at signer.eliteencoder.net is a community-hosted service maintained by John (Elite Encoder). It provides free ETH for testing off-chain gateway setups. Confirm availability in #local-gateways on Discord if you encounter connection errors.
2

Verify both processes are running

Check the gateway API is responding:
curl http://localhost:8935/health
# Expected: {"status":"ok"}
Check the orchestrator is reachable from the gateway:
curl http://localhost:7935/registeredOrchestrators
# Expected: JSON array with your orchestrator entry

Part 4: Send a test job

1

Install the Python gateway SDK (optional but recommended)

The Python SDK makes it easy to send a structured BYOC job and observe payments:
pip install git+https://github.com/j0sh/livepeer-python-gateway.git
2

Send a BYOC job via the SDK

Create test_job.py:
import asyncio
from livepeer_gateway import GatewayClient

async def main():
    client = GatewayClient(
        gateway_url="http://localhost:8935",
        model_id="passthrough-cpu",
    )

    print("Starting BYOC job...")
    async with client.start_byoc_job() as session:
        # Send a small test payload (simulated video frame as bytes)
        test_frame = b"\x00" * 1024  # 1KB of zeros — minimal CPU test payload
        await session.send(test_frame)
        print("Frame sent. Waiting for response...")

        result = await session.receive()
        print(f"Job complete. Response size: {len(result)} bytes")
        assert len(result) > 0, "Expected non-empty response from passthrough pipeline"

    print("BYOC test passed.")

asyncio.run(main())
Run it:
python test_job.py
Successful output:
Starting BYOC job...
Frame sent. Waiting for response...
Job complete. Response size: 1024 bytes
BYOC test passed.
3

Alternative: Test via curl (gateway HTTP API)

If you prefer not to install the SDK, you can test via the gateway’s HTTP endpoint directly:
curl -X POST http://localhost:8935/live/video-to-video \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/dev/urandom \
  --max-time 10
A non-error response confirms the pipeline is reachable and the session was established.
4

Inspect the logs

In the gateway terminal, you should see payment-related log lines confirming the remote signer was used:
Calling remote signer: getOrchInfoSig
Calling remote signer: signTicket
Ticket sent to orchestrator localhost:8935
In the orchestrator terminal:
BYOC job received: model_id=passthrough-cpu
Forwarding to container at http://localhost:8000
Job complete

Part 5: Troubleshooting

Confirm the community remote signer is online:
curl https://signer.eliteencoder.net/health
If this fails, post in #local-gateways on Discord — the signer may be down or the URL may have changed. As a fallback, you can run your own remote signer from the go-livepeer source using the -remoteSigner flag with your own Ethereum key.
Check that your Docker container is running and reachable before starting the orchestrator:
docker ps | grep byoc-cpu-passthrough
curl http://localhost:8000/health
If the container is not reachable, the orchestrator will start but will not advertise the BYOC capability to the gateway.
If either port 7935, 7936, or 8935 is already in use:
lsof -i :8935
lsof -i :7935
Adjust the -serviceAddr, -cliAddr, and -httpAddr flags accordingly. Make sure -orchAddr on the gateway matches the orchestrator’s -serviceAddr.
Check that the orchestrator’s -serviceAddr is reachable from the gateway process:
curl http://localhost:8935/getOrchestrators
If the orchestrator is on a different interface, update -orchAddr to use the correct IP. For a same-machine setup, localhost or 127.0.0.1 should always work.
Check Docker logs:
docker logs byoc-cpu-passthrough
Common causes: missing ai-runner base image (pull it first with docker pull livepeer/ai-runner:live-base), or a Python import error in your pipeline code.

Part 6: Graduate to on-chain production

Once your off-chain smoke test passes, follow this path to move to a production on-chain deployment.
1

Acquire ETH on Arbitrum One

On-chain gateways require a PM deposit (approximately 0.065 ETH) and a PM reserve (approximately 0.03 ETH) on Arbitrum One.Options for acquiring Arbitrum ETH:
Confirm current ETH deposit requirements in #lounge on Discord or the on-chain requirements page before depositing — amounts can change as ETH/USD rates shift.
2

Create an Ethereum wallet for your gateway

Your production gateway needs a dedicated Ethereum wallet. Do not reuse a personal wallet.
./livepeer_cli \
  -network arbitrum-one-mainnet \
  -datadir ./data-gateway \
  # Follow the CLI prompts to create a new keystore
Note your wallet address. You will need it for the PM deposit and reserve transactions.
3

Set up an Arbitrum RPC endpoint

Your on-chain gateway needs a reliable Arbitrum One RPC. Options:Test your RPC:
curl -X POST YOUR_RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
4

Deposit PM funds on-chain

Use livepeer_cli to make your PM deposit and reserve:
./livepeer_cli \
  -network arbitrum-one-mainnet \
  -ethUrl YOUR_ARBITRUM_RPC \
  -datadir ./data-gateway
From the CLI menu, select:
  1. Deposit broadcasting funds (ETH) — deposit approximately 0.065 ETH
  2. Request unlock broadcasting funds is not needed yet; this is for withdrawal
After depositing, confirm your balance:
./livepeer_cli \
  -network arbitrum-one-mainnet \
  -ethUrl YOUR_ARBITRUM_RPC \
  -datadir ./data-gateway \
  # Select "Get node status" to see PM deposit balance
5

Register your orchestrator on-chain

Your production orchestrator needs to activate and set pricing on-chain:
./livepeer \
  -orchestrator \
  -serviceAddr YOUR_PUBLIC_IP:8935 \
  -network arbitrum-one-mainnet \
  -ethUrl YOUR_ARBITRUM_RPC \
  -datadir ./data-orchestrator \
  -byoc \
  -byocContainerURL http://localhost:8000 \
  -byocModelID passthrough-cpu \
  -pricePerUnit 1000
Then activate via the CLI:
./livepeer_cli \
  -network arbitrum-one-mainnet \
  -ethUrl YOUR_ARBITRUM_RPC \
  -datadir ./data-orchestrator
# Select: "Activate orchestrator"
# Set your service fee, block reward cut, and pricing
6

Switch the gateway to on-chain mode

Remove -network offchain and -remoteSignerAddr, and add your Arbitrum RPC and on-chain flags:
./livepeer \
  -gateway \
  -cliAddr 127.0.0.1:7936 \
  -httpAddr 0.0.0.0:8935 \
  -network arbitrum-one-mainnet \
  -ethUrl YOUR_ARBITRUM_RPC \
  -datadir ./data-gateway
The gateway will now discover orchestrators from the on-chain registry and sign PM tickets directly using your wallet’s signing key.
Once on-chain, you no longer need the community remote signer. Your gateway holds the PM signing key and handles all Ethereum operations directly. This is the standard mode for video gateways and is also supported for AI gateways that prefer full on-chain custody.
7

Verify on-chain operation

Confirm your gateway is visible on the network:
  1. Visit explorer.livepeer.org and search for your gateway address
  2. Check that PM tickets are being sent via the gateway logs: look for Ticket sent lines
  3. Monitor your ETH balance — it should decrease slowly as PM deposits fund tickets
  4. Use tools.livepeer.cloud to check your orchestrator’s visibility and performance

What next

BYOC and off-chain gateway support is actively developed. For the latest status of BYOC payments, remote signer compatibility, and SDK updates, follow the #local-gateways channel on Discord and the livepeer-python-gateway PR queue.
Last modified on March 16, 2026