> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Zero to First Reward

> End-to-end tutorial: install go-livepeer, configure video transcoding, fund and stake LPT, activate on the Livepeer network, and claim a first LPT reward.

export const StyledStep = ({title, icon, titleSize = 'h3', iconColor = null, titleColor = null, children, className = '', style = {}, ...rest}) => {
  const styledTitle = titleColor ? <span style={{
    color: titleColor
  }}>{title}</span> : title;
  return <Step title={styledTitle} icon={icon} iconColor={iconColor || undefined} titleSize={titleSize} className={className} style={style} {...rest}>
      {children}
    </Step>;
};

export const StyledSteps = ({children, iconColor, titleColor, lineColor, iconSize = '24px', className = '', style = {}, ...rest}) => {
  const resolvedIconColor = iconColor || 'var(--accent-dark, #18794E)';
  const resolvedTitleColor = titleColor || 'var(--lp-color-accent)';
  const resolvedLineColor = lineColor || 'var(--lp-color-accent)';
  return <div className={['docs-styled-steps', className].filter(Boolean).join(' ')} style={style} {...rest}>
      <style>{`
        .docs-styled-steps .steps > div > div.absolute > div {
          background-color: ${resolvedIconColor};
        }
        .docs-styled-steps .steps > div > div.w-full > p {
          color: ${resolvedTitleColor};
        }
        .docs-styled-steps .steps > div > div.absolute.w-px {
          background-color: ${resolvedLineColor};
        }
        .docs-styled-steps .steps > div:last-child > div.absolute.w-px::after {
          content: '';
          position: absolute;
          bottom: 0;
          left: 50%;
          transform: translateX(-50%);
          width: 6px;
          height: 6px;
          background-color: ${resolvedLineColor};
          transform: translateX(-50%) rotate(45deg);
        }
      `}</style>
      <div>
        <Steps>{children}</Steps>
      </div>
    </div>;
};

export const TableCell = ({children, align = "left", header = false, style = {}, className = "", ...rest}) => {
  const Component = header ? "th" : "td";
  return <Component className={className} style={{
    padding: "0.75rem 1rem",
    textAlign: align,
    border: header ? "none" : "1px solid var(--lp-color-border-default)",
    ...style
  }} {...rest}>
      {children}
    </Component>;
};

export const TableRow = ({children, header = false, hover = false, style = {}, className = "", ...rest}) => {
  const rowId = `table-row-${Math.random().toString(36).substr(2, 9)}`;
  return <>
      {hover && <style>{`
          #${rowId}:hover {
            background-color: var(--lp-color-bg-card);
          }
        `}</style>}
      <tr id={rowId} className={className} style={{
    ...header && ({
      backgroundColor: "var(--lp-color-accent-strong)",
      color: "var(--lp-color-on-accent)",
      fontWeight: "bold"
    }),
    ...style
  }} {...rest}>
        {children}
      </tr>
    </>;
};

export const StyledTable = ({children, variant = "default", style = {}, className = "", ...rest}) => {
  const wrapperVariants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)",
      overflow: "hidden"
    },
    bordered: {
      border: "2px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-page)",
      overflow: "hidden"
    },
    minimal: {
      border: "none",
      backgroundColor: "transparent",
      overflow: "visible"
    }
  };
  return <div data-docs-styled-table-shell className={className} style={{
    width: "100%",
    padding: 0,
    margin: 0,
    ...wrapperVariants[variant],
    ...style
  }} {...rest}>
      <table data-docs-styled-table style={{
    width: "100%",
    borderCollapse: "collapse",
    borderSpacing: 0,
    margin: 0,
    backgroundColor: "transparent"
  }}>
        {children}
      </table>
    </div>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

<Tip>
  Follow this path when the goal is straightforward: start a production video Orchestrator, enter the Active Set, and claim a first LPT reward. AI pipelines, dual mode, and pool joining build on this baseline.
</Tip>

***

By the end, the Orchestrator node is running on Arbitrum One mainnet, processing video transcoding jobs, and has successfully called `Reward()` to claim its first LPT inflation allocation. Plan for **4 to 8 hours**. LPT acquisition and bridge confirmation usually take the longest.

**Success criteria:**

* The node appears on [Livepeer Explorer](https://explorer.livepeer.org/orchestrators) in the Active Set
* At least one reward round has completed and ETH + LPT balances reflect earnings

<CustomDivider />

## Prerequisites

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Requirement</TableCell>
      <TableCell header>Notes</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>Linux server (Ubuntu 22.04 or 24.04 recommended)</TableCell>
      <TableCell>Production GPU transcoding runs on Linux. Use macOS for CLI reference only.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>NVIDIA GPU with CUDA 12.0+ drivers</TableCell>
      <TableCell>Verify: `nvidia-smi` shows the GPU. Minimum driver: `525.60.13`.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>Docker Engine + NVIDIA Container Toolkit</TableCell>
      <TableCell>Verify: `docker run --rm --gpus all nvidia/cuda:12.0.0-base-ubuntu20.04 nvidia-smi`</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>Public static IP or hostname</TableCell>
      <TableCell>Port 8935 must be reachable from the internet. Test after node starts.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>Arbitrum One RPC endpoint</TableCell>
      <TableCell>Use an Arbitrum One endpoint from Alchemy (`arb-mainnet.g.alchemy.com`) or Infura.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>ETH on Arbitrum One</TableCell>
      <TableCell>\~0.01 ETH minimum for gas. Bridge from L1 at [bridge.Arbitrum.io](https://bridge.arbitrum.io) or buy directly on a CEX with Arbitrum withdrawal.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>LPT on Arbitrum One</TableCell>
      <TableCell>Enough to stake. The Active Set is the top 100 Orchestrators by total stake. Check the current threshold at [Livepeer Explorer](https://explorer.livepeer.org/orchestrators). Plan for a multi-hour acquisition window before starting.</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<Warning>
  Acquiring LPT on Arbitrum One usually takes hours because exchange support and bridge confirmation timing vary. Start once LPT is already in the wallet, or reserve that waiting time up front.
</Warning>

<CustomDivider />

## Step 1: Install go-livepeer

<StyledSteps>
  <StyledStep title="Pull the Docker image">
    ```bash icon="terminal" filename="install" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker pull livepeer/go-livepeer:latest
    ```

    Verify:

    ```bash icon="terminal" filename="verify-install" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker run --rm livepeer/go-livepeer:latest livepeer --version
    ```

    Expected output includes the version string, e.g. `Livepeer Node Version: v0.8.9`.
  </StyledStep>

  <StyledStep title="Create persistent data volume">
    ```bash icon="terminal" filename="create-volume" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker volume create livepeer-data
    ```

    All node data (keystore, chain data, config) persists in this volume across container restarts.
  </StyledStep>
</StyledSteps>

<CustomDivider />

## Step 2: Configure the node

<StyledSteps>
  <StyledStep title="Identify your GPU device ID">
    ```bash icon="terminal" filename="check-gpu" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    nvidia-smi -L
    ```

    Expected output:

    ```text icon="terminal" title="Example GPU output" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    GPU 0: NVIDIA GeForce RTX 4090 (UUID: GPU-...)
    ```

    Note the GPU index (usually `0` for a single GPU). Use `0,1` for two GPUs, or `all` for all available.
  </StyledStep>

  <StyledStep title="Start the node for the first time (key generation)">
    Start the node once to generate the Ethereum keystore. Replace all placeholders:

    ```bash icon="terminal" filename="first-start" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker run -d \
      --name livepeer-orchestrator \
      -v livepeer-data:/root/.lpData \
      --network host \
      --gpus all \
      livepeer/go-livepeer:latest \
      -network arbitrum-one-mainnet \
      -ethUrl https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY \
      -orchestrator \
      -transcoder \
      -nvidia 0 \
      -maxSessions 10 \
      -pricePerUnit 1000 \
      -serviceAddr YOUR_PUBLIC_IP:8935
    ```

    Replace:

    * `YOUR_API_KEY` with your Alchemy or Infura Arbitrum One API key
    * `YOUR_PUBLIC_IP` with the machine's public IP address or domain name
    * `-nvidia 0` with your GPU device ID(s)

    On first start, go-livepeer creates a new Ethereum keystore and prompts for a passphrase via the container logs:

    ```bash icon="terminal" filename="get-passphrase-prompt" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker logs livepeer-orchestrator 2>&1 | grep -i "passphrase\|Enter"
    ```

    Set a strong passphrase and record it securely. The keystore is at `/root/.lpData/arbitrum-one-mainnet/keystore` inside the volume.

    <Warning>
      The private key controls the Orchestrator identity and all staked LPT. Back up the keystore file to offline storage. Loss of the keystore means permanent loss of access to the account and all bonded LPT.
    </Warning>
  </StyledStep>

  <StyledStep title="Note your orchestrator Ethereum address">
    ```bash icon="terminal" filename="get-address" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker logs livepeer-orchestrator 2>&1 | grep -i "account\|address" | head -5
    ```

    The address appears in the startup logs. You will use it to send ETH and LPT.
  </StyledStep>
</StyledSteps>

<CustomDivider />

## Step 3: Fund the wallet

**Two assets are needed on Arbitrum One:**

1. **ETH** - for gas (reward calls, ticket redemption, activation). Keep at least 0.01 ETH.
2. **LPT** - for staking. The Active Set is the top 100 Orchestrators by total stake. Check the current threshold at [Livepeer Explorer](https://explorer.livepeer.org/orchestrators) before acquiring.

<Note>
  This is the waiting step. Bridge ETH from Ethereum L1 at [bridge.Arbitrum.io](https://bridge.arbitrum.io) with a typical 10 to 15 minute confirmation, or buy directly from a centralised exchange with Arbitrum One withdrawal support. LPT acquisition follows the timeline of the exchange path you choose and usually takes longer.
</Note>

Verify balances are available:

```bash icon="terminal" filename="check-balances" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
docker exec livepeer-orchestrator livepeer_cli
# Select: "Get node status" to view ETH and LPT balances
```

<CustomDivider />

## Step 4: Stake LPT and activate

Activation registers the Orchestrator on-chain, sets commission rates, and makes the node eligible for the Active Set.

<StyledSteps>
  <StyledStep title="Open livepeer_cli">
    ```bash icon="terminal" filename="open-cli" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker exec -it livepeer-orchestrator livepeer_cli
    ```
  </StyledStep>

  <StyledStep title="Activate ">
    Select **"Invoke multi-step become an Orchestrator"** from the menu.

    The multi-step process prompts for:

    * **Reward Cut** - the percentage of LPT inflation you keep (remainder goes to Delegators). Setting 0% attracts more delegation; setting 100% keeps all inflation yourself.
    * **Fee Cut** - the percentage of ETH job fees you keep. Higher Fee Cut means less passed to Delegators.
    * **Service URI** - your public address (e.g. `https://YOUR_PUBLIC_IP:8935`). Must match `-serviceAddr` exactly.
    * **LPT to self-delegate** - the amount to bond to your own node. This is your stake.

    Confirm each prompt. The activation transaction submits to Arbitrum. Confirmation takes 1-3 minutes.
  </StyledStep>

  <StyledStep title="Verify activation in logs">
    ```bash icon="terminal" filename="check-activation" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker logs livepeer-orchestrator 2>&1 | grep -i "activate\|active\|registered" | tail -10
    ```

    Expected:

    ```text icon="terminal" title="Expected activation log" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    Registered orchestrator on chain
    Transcoding on Nvidia GPU 0
    Listening for RPC on :8935
    Received Ping request
    ```

    The `Received Ping request` line confirms the node is publicly reachable.
  </StyledStep>
</StyledSteps>

<CustomDivider />

## Step 5: Verify on Livepeer Explorer

Open [explorer.livepeer.org/Orchestrators](https://explorer.livepeer.org/orchestrators) and search for your Ethereum address.

Confirm:

* The node appears in the Orchestrator list
* **Status** shows as Active after the next round begins, roughly 22 hours after activation
* **Service URI** matches the address you set in `-serviceAddr`
* **Price per pixel** reflects your `-pricePerUnit` setting (after conversion from wei)

<Note>
  Active-set entry starts at the next round boundary. The node becomes active when its total stake is already above the current 100th Orchestrator at that boundary. Use Explorer to watch your ranking and add more delegated LPT whenever the node stays below the cutoff.
</Note>

Also verify the port is reachable from outside the network. Test from a machine different from your server:

```bash icon="terminal" filename="port-check" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
curl -k https://YOUR_PUBLIC_IP:8935/status
```

Any response (including an error JSON) confirms the port is open. A connection timeout means the firewall is blocking port 8935.

<CustomDivider />

## Step 6: Claim the first reward

Livepeer distributes LPT inflation to active Orchestrators once per round (approximately every 22 hours). Go-livepeer automatically calls `Reward()` at round initialisation by default.

**Verify automatic reward calling is enabled:**

```bash icon="terminal" filename="check-reward-flag" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
docker inspect livepeer-orchestrator | grep -A 5 "Cmd"
# Confirm -reward=false is NOT in the command - if absent, auto-reward is on
```

**Wait for the next round to complete.** Round timing is visible on [Explorer](https://explorer.livepeer.org) under the current round number and time remaining.

**Verify the reward call succeeded:**

```bash icon="terminal" filename="check-reward-log" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
docker logs livepeer-orchestrator 2>&1 | grep -i "reward" | tail -5
```

Expected:

```text icon="terminal" title="Expected reward log" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
Called Reward() for round XXXXX
```

**Verify on Explorer:**

On your Orchestrator page on Explorer, the **Rewards** section updates after a successful call, showing the LPT received for the round.

<Note>
  Rewards accrue only in rounds where the node entered the Active Set before the round started. A zero first round usually means the node crossed the threshold after round initialisation. Confirm active-set membership on Explorer and check again next round.
</Note>

<CustomDivider />

## What happened

The node completed the full video Orchestrator lifecycle:

1. **Installed** go-livepeer and generated an Ethereum identity (keystore)
2. **Configured** for video transcoding with GPU selection, pricing, and public address
3. **Funded** the wallet with ETH (for gas) and LPT (for staking)
4. **Activated** on-chain via `livepeer_cli`, setting commission rates and self-delegating LPT to enter the Active Set
5. **Verified** public reachability and Explorer visibility
6. **Earned** the first LPT inflation reward by calling `Reward()` at round initialisation

**Reward income formula:**

```text icon="terminal" title="Reward income formula" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
LPT earned per round = (your total stake / network active stake) × round_issuance × 0.90 × your_reward_cut
```

ETH fee income is separate and arrives as Gateways route transcoding jobs to the node. Fee income scales with job volume, while reward size scales with stake.

<CustomDivider />

## Related pages

<CardGroup cols={2}>
  <Card title="Add AI to a Video Node" icon="microchip" href="/v2/orchestrators/guides/tutorials/add-ai-to-video-node" arrow horizontal>
    Add AI inference to this running node without changing video configuration.
  </Card>

  <Card title="AI Earning Quickstart" icon="bolt" href="/v2/orchestrators/guides/tutorials/ai-earning-quickstart" arrow horizontal>
    Start earning from AI inference with minimal LPT - the alternative first path.
  </Card>

  <Card title="Earning Model" icon="coins" href="/v2/orchestrators/guides/staking-and-rewards/earning-model" arrow horizontal>
    How LPT rewards and ETH fees combine into total Orchestrator income.
  </Card>

  <Card title="Reward Call Tuning" icon="sliders" href="/v2/orchestrators/guides/config-and-optimisation/reward-call-tuning" arrow horizontal>
    Calculate reward call profitability and optimise timing at your stake level.
  </Card>
</CardGroup>
