> ## 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.

# Livepeer Network Actors and Nodes

> Overview of the key actors and participants in the Livepeer network, including their roles, interactions, and incentives.

export const CenteredContainer = ({children, maxWidth = "800px", padding = "0", preset = "default", width = "", minWidth = "", marginRight = "", marginBottom = "", textAlign = "", style = {}, className = "", ...rest}) => {
  const presets = {
    default: {},
    fitContent: {
      width: "fit-content",
      minWidth: "fit-content"
    },
    readable70: {
      width: "70%",
      minWidth: "fit-content"
    },
    readable80: {
      width: "80%",
      minWidth: "fit-content"
    },
    readable90: {
      width: "90%"
    },
    wide900: {
      maxWidth: "900px"
    }
  };
  const presetStyle = presets[preset] || presets.default;
  return <div className={className} style={{
    maxWidth: presetStyle.maxWidth || maxWidth,
    margin: "0 auto",
    padding: padding,
    ...presetStyle.width ? {
      width: presetStyle.width
    } : {},
    ...presetStyle.minWidth ? {
      minWidth: presetStyle.minWidth
    } : {},
    ...width ? {
      width
    } : {},
    ...minWidth ? {
      minWidth
    } : {},
    ...marginRight ? {
      marginRight
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...textAlign ? {
      textAlign
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const BorderedBox = ({children, variant = "default", padding = "var(--lp-spacing-4)", borderRadius = "var(--lp-spacing-px-8)", margin = "", accentBar = "", style = {}, className = "", ...rest}) => {
  const variants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)"
    },
    accent: {
      border: "1px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-card)"
    },
    muted: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "transparent"
    }
  };
  const accentBarColors = {
    accent: "var(--lp-color-accent)",
    positive: "var(--green-9)"
  };
  return <div data-docs-bordered-box="" data-accent-bar={accentBarColors[accentBar] ? "" : undefined} className={className} style={{
    ...variants[variant],
    padding: padding,
    borderRadius: borderRadius,
    ...margin ? {
      margin
    } : {},
    ...accentBarColors[accentBar] ? {
      position: "relative",
      '--accent-bar-color': accentBarColors[accentBar]
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const Quote = ({children, className = "", style = {}, ...rest}) => {
  const quoteStyle = {
    fontSize: "1rem",
    textAlign: 'center',
    opacity: 1,
    fontStyle: 'italic',
    color: 'var(--lp-color-accent)',
    border: '1px solid var(--lp-color-border-default)',
    borderRadius: "8px",
    padding: "var(--lp-spacing-4)",
    margin: '1rem 0',
    ...style
  };
  return <blockquote className={className} style={quoteStyle} {...rest}>{children}</blockquote>;
};

export const DynamicTableV2 = ({tableTitle = null, headerList = [], itemsList = [], monospaceColumns = [], columnWidths = {}, columnConfig = {}, showSeparators = false, margin, className = '', style = {}, ...rest}) => {
  if (!headerList.length) {
    return <div>No headers provided</div>;
  }
  const tableRef = useRef(null);
  const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
  const measureFitColumns = () => {
    const tableElement = tableRef.current;
    if (!tableElement) {
      return;
    }
    const nextWidths = headerList.reduce((accumulator, header, index) => {
      const config = columnConfig?.[header] || ({});
      if (!config.fitContent) {
        return accumulator;
      }
      const contentNodes = tableElement.querySelectorAll(`[data-docs-column-key="${index}"] [data-docs-fit-content]`);
      let maxContentWidth = 0;
      contentNodes.forEach(node => {
        const width = Math.ceil(node.getBoundingClientRect().width);
        if (width > maxContentWidth) {
          maxContentWidth = width;
        }
      });
      if (maxContentWidth > 0) {
        accumulator[header] = `${maxContentWidth + 16}px`;
      }
      return accumulator;
    }, {});
    setMeasuredColumnWidths(currentWidths => {
      const currentEntries = Object.entries(currentWidths);
      const nextEntries = Object.entries(nextWidths);
      if (currentEntries.length === nextEntries.length && nextEntries.every(([header, width]) => currentWidths[header] === width)) {
        return currentWidths;
      }
      return nextWidths;
    });
  };
  useLayoutEffect(() => {
    measureFitColumns();
  }, [headerList, itemsList, columnConfig]);
  useEffect(() => {
    const tableElement = tableRef.current;
    if (!tableElement || typeof ResizeObserver === 'undefined') {
      return undefined;
    }
    const resizeObserver = new ResizeObserver(() => {
      measureFitColumns();
    });
    resizeObserver.observe(tableElement);
    if (tableElement.parentElement) {
      resizeObserver.observe(tableElement.parentElement);
    }
    return () => {
      resizeObserver.disconnect();
    };
  }, [headerList, itemsList, columnConfig]);
  const fitHeaders = headerList.filter(header => columnConfig?.[header]?.fitContent);
  const hasMeasuredFitColumns = fitHeaders.length === 0 || fitHeaders.every(header => Boolean(measuredColumnWidths[header]));
  const getColumnStyle = (header, isMonospace = false) => {
    const config = columnConfig?.[header] || ({});
    const fitContent = Boolean(config.fitContent);
    const fluid = Boolean(config.fluid);
    const nowrap = Boolean(config.nowrap) || fitContent || isMonospace;
    const preferredWidth = columnWidths[header];
    const measuredWidth = measuredColumnWidths[header];
    return {
      ...fitContent && measuredWidth ? {
        width: measuredWidth,
        minWidth: measuredWidth,
        maxWidth: measuredWidth
      } : {},
      ...!fitContent && !fluid && preferredWidth ? {
        minWidth: preferredWidth
      } : {},
      ...nowrap ? {
        whiteSpace: 'nowrap'
      } : {
        wordWrap: 'break-word',
        overflowWrap: 'break-word'
      }
    };
  };
  const getColumnTrackStyle = header => {
    const config = columnConfig?.[header] || ({});
    const fitContent = Boolean(config.fitContent);
    const fluid = Boolean(config.fluid);
    const preferredWidth = columnWidths[header];
    const measuredWidth = measuredColumnWidths[header];
    if (fitContent && measuredWidth) {
      return {
        width: measuredWidth,
        minWidth: measuredWidth,
        maxWidth: measuredWidth
      };
    }
    if (fluid) {
      return {};
    }
    if (preferredWidth) {
      return {
        width: preferredWidth
      };
    }
    return {};
  };
  const renderCellContent = (header, content) => {
    const config = columnConfig?.[header] || ({});
    if (!config.fitContent) {
      return content;
    }
    return <div data-docs-fit-content style={{
      display: 'inline-flex',
      alignItems: 'center',
      whiteSpace: 'nowrap',
      width: 'max-content',
      maxWidth: 'none'
    }}>
        {content}
      </div>;
  };
  return <div className={className} style={style} {...rest}>
      {tableTitle && <div style={{
    fontStyle: 'italic',
    margin: 0
  }}>
          <strong>{tableTitle}</strong>
        </div>}
      <div style={{
    overflowX: 'auto',
    ...margin != null && ({
      margin
    })
  }} role="region" tabIndex={0} aria-label={tableTitle ? `Scrollable table: ${tableTitle}` : 'Scrollable table'}>
        <table ref={tableRef} data-docs-dynamic-table-v2 style={{
    width: '100%',
    tableLayout: hasMeasuredFitColumns ? 'fixed' : 'auto',
    borderCollapse: 'collapse',
    fontSize: '0.9rem',
    marginTop: 0
  }}>
          <colgroup>
            {headerList.map((header, index) => <col key={index} style={getColumnTrackStyle(header)} />)}
          </colgroup>
          <thead>
            <tr style={{
    backgroundColor: 'var(--lp-color-accent)',
    color: 'var(--lp-color-on-accent)',
    borderBottom: '1px solid var(--lp-color-border-default)'
  }}>
              {headerList.map((header, index) => <th key={index} data-docs-column-key={index} style={{
    padding: '10px 8px',
    textAlign: 'left',
    fontWeight: '600',
    color: 'var(--lp-color-on-accent)',
    verticalAlign: 'top',
    ...getColumnStyle(header)
  }}>
                  {renderCellContent(header, header)}
                </th>)}
            </tr>
          </thead>
          <tbody>
            {itemsList.filter(item => showSeparators || !item?.__separator).map((item, rowIndex) => item?.__separator ? <tr key={rowIndex} style={{
    backgroundColor: 'var(--lp-color-accent)',
    color: 'var(--lp-color-on-accent)',
    borderBottom: '1px solid var(--lp-color-accent)'
  }}>
                    <td colSpan={headerList.length} style={{
    padding: '6px 8px',
    fontWeight: '700',
    color: 'var(--lp-color-on-accent)',
    letterSpacing: '0.01em'
  }}>
                      {(item[headerList[0]] ?? item.Category) ?? 'Category'}
                    </td>
                  </tr> : <tr key={rowIndex} style={{
    borderBottom: '1px solid var(--lp-color-border-default)'
  }}>
                    {headerList.map((header, colIndex) => {
    const value = (item[header] ?? item[header.toLowerCase()]) ?? '-';
    const isMonospace = monospaceColumns.includes(colIndex);
    return <td key={colIndex} data-docs-column-key={colIndex} style={{
      padding: '8px 8px',
      fontFamily: isMonospace ? 'monospace' : 'inherit',
      verticalAlign: 'top',
      ...getColumnStyle(header, isMonospace)
    }}>
                          {renderCellContent(header, isMonospace ? <code>{value}</code> : value)}
                        </td>;
  })}
                  </tr>)}
          </tbody>
        </table>
      </div>
    </div>;
};

export const QuadGrid = ({children, icon = "arrows-spin", iconSize = 50, iconColor = "var(--lp-color-accent)", iconBackgroundColor = "transparent", gap, spinDuration = "10s", className = "", style = {}, ...rest}) => {
  if (children == null) {
    console.warn("[QuadGrid] Missing children");
    return null;
  }
  return <div className={className} style={{
    position: "relative",
    ...style
  }} {...rest}>
      <style>{`
        @keyframes quadGridSpin {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }
        .lp-quad-grid-layout {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          grid-auto-rows: 1fr;
          gap: var(--lp-quad-grid-gap, 0);
        }
        @media (max-width: 768px) {
          .lp-quad-grid-layout {
            grid-template-columns: 1fr;
          }
        }
        @media (prefers-reduced-motion: reduce) {
          .lp-quad-grid-icon {
            animation: none !important;
          }
        }
      `}</style>
      <div className="lp-quad-grid-layout" style={{
    "--lp-quad-grid-gap": gap
  }}>
        {children}
      </div>
      <div style={{
    position: "absolute",
    top: "50%",
    left: "50%",
    transform: "translate(-50%, -50%)",
    zIndex: 10
  }}>
        <div className="lp-quad-grid-icon" style={{
    backgroundColor: iconBackgroundColor,
    borderRadius: "50%",
    padding: "var(--lp-spacing-2)",
    animation: `quadGridSpin ${spinDuration} linear infinite`
  }}>
          <Icon icon={icon} size={iconSize} color={iconColor} />
        </div>
      </div>
    </div>;
};

export const ScrollableDiagram = ({children, title = '', maxHeight = '500px', minWidth = '100%', showControls = false, className = '', style = {}, ...rest}) => {
  const buildDiagramKey = (currentTitle = '', currentClassName = '') => {
    const source = `${currentTitle}|${currentClassName}|scrollable-diagram`;
    let hash = 0;
    for (let index = 0; index < source.length; index += 1) {
      hash = hash * 31 + source.charCodeAt(index) >>> 0;
    }
    return `docs-diagram-${hash.toString(36)}`;
  };
  const diagramKey = buildDiagramKey(title, className);
  const zoomName = `${diagramKey}-zoom`;
  const zoomLevels = [{
    label: '75%',
    value: 0.75
  }, {
    label: '100%',
    value: 1
  }, {
    label: '125%',
    value: 1.25
  }, {
    label: '150%',
    value: 1.5
  }];
  const containerStyle = {
    overflow: 'auto',
    maxHeight,
    border: '1px solid var(--lp-color-border-default)',
    borderRadius: "8px",
    padding: "var(--lp-spacing-4)",
    background: 'var(--lp-color-bg-card)',
    position: 'relative'
  };
  return <div className={className} style={{
    position: 'relative',
    marginBottom: "var(--lp-spacing-4)",
    ...style
  }} {...rest}>
      {title && <p style={{
    textAlign: 'center',
    fontStyle: 'italic',
    color: 'var(--lp-color-text-secondary)',
    marginBottom: "var(--lp-spacing-2)",
    fontSize: '0.875rem'
  }}>
          {title}
        </p>}

      {showControls ? <style>{`
          [data-docs-diagram-key="${diagramKey}"] [data-docs-diagram-content] {
            transform: scale(1);
            transform-origin: top left;
            width: max-content;
          }
          ${zoomLevels.map(zoomLevel => `
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-shell] [data-docs-diagram-content] {
            transform: scale(${zoomLevel.value});
          }
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-controls] label[for="${diagramKey}-${zoomLevel.label.replace('%', '')}"] {
            background: var(--lp-color-accent);
            color: var(--lp-color-on-accent);
            border-color: var(--lp-color-accent);
          }`).join('\n')}
        `}</style> : null}

      {showControls ? zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <input key={inputId} id={inputId} type="radio" name={zoomName} defaultChecked={zoomLevel.value === 1} style={{
      position: 'absolute',
      opacity: 0,
      pointerEvents: 'none'
    }} />;
  }) : null}

      <div data-docs-diagram-key={diagramKey} data-docs-diagram-shell style={containerStyle}>
        <div data-docs-diagram-content style={{
    minWidth,
    transformOrigin: 'top left',
    width: 'max-content'
  }}>
          {children}
        </div>
      </div>

      {showControls ? <div data-docs-diagram-controls style={{
    display: 'flex',
    justifyContent: 'flex-end',
    alignItems: 'center',
    gap: "var(--lp-spacing-2)",
    marginTop: "var(--lp-spacing-2)",
    flexWrap: 'wrap'
  }}>
          <span style={{
    fontSize: "0.75rem",
    color: 'var(--lp-color-text-muted)',
    marginRight: 'auto'
  }}>
            Scroll to pan
          </span>
          {zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <label key={inputId} htmlFor={inputId} style={{
      background: 'transparent',
      color: 'var(--lp-color-text-secondary)',
      border: '1px solid var(--lp-color-border-default)',
      borderRadius: "4px",
      padding: '4px 10px',
      cursor: 'pointer',
      fontSize: "0.75rem",
      fontWeight: '600'
    }}>
                {zoomLevel.label}
              </label>;
  })}
        </div> : null}
    </div>;
};

export const Subtitle = ({style = {}, text, children, variant = 'default', fontSize = '', fontWeight = '', fontStyle = '', marginTop = '', marginBottom = '', color = '', className = '', ...rest}) => {
  const renderInlineCode = (value, keyPrefix) => {
    return value.split(/(`[^`]+`)/g).map((segment, index) => {
      if (segment.startsWith('`') && segment.endsWith('`')) {
        return <code key={`${keyPrefix}-code-${index}`}>{segment.slice(1, -1)}</code>;
      }
      return segment;
    });
  };
  const renderInlineMarkup = (value, keyPrefix = 'subtitle') => {
    if (typeof value !== 'string') {
      return value;
    }
    return value.split(/(\*\*[\s\S]+?\*\*)/g).map((segment, index) => {
      if (segment.startsWith('**') && segment.endsWith('**')) {
        const inner = segment.slice(2, -2);
        return <strong key={`${keyPrefix}-strong-${index}`}>
            {renderInlineCode(inner, `${keyPrefix}-strong-${index}`)}
          </strong>;
      }
      return renderInlineCode(segment, `${keyPrefix}-${index}`);
    });
  };
  const renderContent = (value, keyPrefix) => {
    if (Array.isArray(value)) {
      return value.map((item, index) => renderContent(item, `${keyPrefix}-${index}`));
    }
    return renderInlineMarkup(value, keyPrefix);
  };
  const variants = {
    default: {
      fontSize: '1rem',
      fontStyle: 'italic',
      color: 'var(--lp-color-accent)',
      marginBottom: 0
    },
    changelog: {
      fontSize: '0.8rem',
      fontStyle: 'normal',
      fontWeight: 700,
      color: 'var(--lp-color-text-primary)',
      marginBottom: 0
    }
  };
  const base = variants[variant] || variants.default;
  return <span className={className} style={{
    ...base,
    ...fontSize ? {
      fontSize
    } : {},
    ...fontWeight ? {
      fontWeight
    } : {},
    ...fontStyle ? {
      fontStyle
    } : {},
    ...marginTop ? {
      marginTop
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...color ? {
      color
    } : {},
    ...style
  }} {...rest}>
      {renderContent(text, 'text')}
      {renderContent(children, 'children')}
    </span>;
};

export const CustomCardTitle = ({icon, title, variant = "card", iconSize, style = {}, className = "", ...rest}) => {
  const variants = {
    card: {
      display: 'flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)",
      marginBottom: "var(--lp-spacing-3)",
      color: 'var(--lp-color-text-primary)',
      fontSize: '1rem',
      fontWeight: 600
    },
    accordion: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)"
    },
    tab: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.4rem',
      fontSize: '0.875rem'
    }
  };
  const sizes = {
    card: 20,
    accordion: 18,
    tab: 14
  };
  const size = iconSize || sizes[variant] || 20;
  const baseStyle = variants[variant] || variants.card;
  return variant === 'card' ? <div className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </div> : <span className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </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>;
};

export const stream_0 = undefined

<CenteredContainer width="60%" minWidth="400px">
  <Card icon="github" title="Go-Livepeer Codebase" arrow href="https://github.com/livepeer/go-livepeer" horizontal />
</CenteredContainer>

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Network Actors

<Tip>
  The actors are the participants that run the off-chain layer and interact with the on-chain Protocol to coordinate, settle, and earn.
</Tip>

The protocol defines three core actors, and implements mechanisms to coordinate their interactions and align incentives for the desired network outcomes:

<AccordionGroup>
  <Accordion title="Gateways" icon="route">
    **Function:** Ingests source video or AI inference requests from end users and routes the work to Orchestrators.

    **Purpose:** Connects demand-side applications and users to the Livepeer Network.

    **Economics:** Pays for transcoding services using ETH through the ticket system.

    **Technical role.** Probably the most demanding integration work of the three. Run Gateway software, ingest video or AI requests from end users, handle Orchestrator discovery and selection, manage payment channels and ticket issuance, deal with failover, and bridge the protocol to whatever application is actually consuming the work. This is where most of the engineering surface area lives.

    **Economic role.** They're the demand side and the only source of external revenue. They fund deposits and reserves to back probabilistic tickets, set their own quality/price tradeoffs by selecting Orchestrators, and ultimately decide whether the network's pricing is competitive against centralised alternatives. If Gateways aren't economically satisfied, no one else gets paid.

    **Community role.** Historically the quietest of the three, but increasingly important. Gateway operators are often building products on top – streaming platforms, AI apps, developer tools – and their feedback drives what capabilities Orchestrators add and what the protocol prioritises. The Livepeer AI subnet exists in large part because Gateway-side demand pulled it into existence.
  </Accordion>

  <Accordion title="Delegators" icon="user-check">
    **Function:** Bonds LPT to Orchestrators without running infrastructure directly.

    **Purpose:** Secures the network by delegating stake to Orchestrators.

    **Economics:** Earns rewards by sharing in the fees and inflationary rewards of the Orchestrator they delegate to.

    **Technical role.** Light but real – they need to evaluate Orchestrators (uptime, performance, fee history, reliability), manage their own wallet security, and periodically claim earnings or move stake. No infrastructure, but non-trivial diligence if they're doing it well.

    **Economic role.** Capital allocators. They route LPT toward Orchestrators they believe will earn the most fees and rewards relative to risk, and away from underperformers. In aggregate this is the price signal that shapes the Active Set. They also bear the opportunity cost of locked stake and unbonding periods.

    **Community role.** Lower visibility than Orchestrators but real influence – they're the constituency Orchestrators are courting. Active Delegators participate in governance polls, weigh in on Orchestrator behaviour, and (especially the larger ones) shape sentiment about which Orchestrators are trustworthy. They're the network's accountability layer.
  </Accordion>

  <Accordion title="Orchestrators" icon="microchip">
    **Function:** Runs the hardware that performs transcoding and AI inference work.

    **Purpose:** Registers on-chain, stakes LPT, and bids for work. The top-ranked subset by total stake forms the Active Set, which is eligible to receive jobs in a given round.

    **Economics:** Earns rewards and fees for performing work, after the Orchestrator's configured fee share and Reward Cut are applied.

    **Technical role.** Operate the infrastructure. Run go-livepeer, manage transcoders and GPUs, maintain uptime, keep model containers current for AI work, advertise capabilities, and actually perform jobs. This is the only role in the protocol that requires running hardware.

    **Economic role.** Price the work (`pricePerUnit`), set the split with Delegators (`feeShare`, `rewardCut`), bond self-stake as collateral, redeem winning tickets, and call reward each round. They're a small business inside the network – capex on hardware, opex on power and bandwidth, revenue in ETH and LPT.

    **Community role.** They're the most visible public face of the network. Reputation matters because Delegators are choosing them – so Orchestrators run websites, publish stats, show up on Discord, write guides, help newer operators, and participate in governance discussions. The serious ones are effectively brands.
  </Accordion>
</AccordionGroup>

A fourth ***worker*** actor also contributes to the network's overall Video and AI capabilities:

* **Transcoders** and **AI Workers** are specialised nodes that execute video transcoding and AI inference tasks in Docker containers or as external endpoints, and are often co-located on the same hardware as Orchestrators.

<QuadGrid icon="arrows-spin" iconSize={50} gap="0.5rem">
  <Card title="Orchestrators" icon="microchip" cta="Go to Orchestrators" href="/v2/orchestrators/portal">
    GPU nodes that perform transcoding and AI inference work, earn fees and staking rewards, and participate in governance

    **Roles**: Perform Compute Operations, participate in governance
  </Card>

  <Card title="Gateways" icon="torii-gate" cta="Go to Gateways" href="/v2/gateways/portal">
    Also known as Broadcasters, Gateways submit video and AI jobs to the network, route work, manage payment flows, and connect to Orchestrators

    **Roles**: Submit jobs, route work, manage payment flows, connect to orchestrators
  </Card>

  <Card title="Delegators" icon="coins" cta="Go to Delegators" href="/v2/delegators/portal">
    LPT token holders who stake to an orchestrator to secure the network and earn rewards, and participate in governance and voting

    **Roles**: Stake LPT, earn rewards, participate in governance and voting
  </Card>

  <Card title="Transcoders & AI Workers" icon="laptop-code" cta="Go to Developers" href="/v2/developers/portal">
    Specialised nodes that execute AI inference or video transcoding tasks

    **Roles**: Execute AI inference or video transcodingtasks, ensure correctness & veracity
  </Card>
</QuadGrid>

## Node Types and Network Architecture

The Network nodes that perform work and interact with the on-chain protocol are implemented by `go-livepeer` as different Node Types, which conditionally enable different functionality and components within the same codebase. A node can operate in multiple roles simultaneously by having multiple flags set during startup.

<ScrollableDiagram title="go-livepeer Node Type Architecture" maxHeight="900px">
  ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
  %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': "Inter, 'Inter Fallback', -apple-system, system-ui" }}}%%
  flowchart TB
      classDef source fill:#1a1a1a,color:#E0E4E0,stroke:#71717a,stroke-width:2px
      classDef node fill:#111827,color:#E0E4E0,stroke:#2b9a66,stroke-width:2px
      classDef process fill:#0d0d0d,color:#E0E4E0,stroke:#2b9a66,stroke-width:2px
      classDef runtime fill:#0d0d0d,color:#E0E4E0,stroke:#60a5fa,stroke-width:2px
      classDef payment fill:#0d0d0d,color:#E0E4E0,stroke:#f59e0b,stroke-width:2px

      subgraph External["External Clients"]
          RTMP["RTMP/HTTP<br/>Stream Source"]:::source
          HLS["HLS Player"]:::source
          API["AI API Client"]:::source
      end

      subgraph BroadcasterNode["BroadcasterNode"]
          Server["LivepeerServer<br/>server/mediaserver.go"]:::node
          Segmenter["RTMPSegmenter<br/>lpmscore"]:::process
          Connections["rtmpConnections map<br/>ManifestID to rtmpConnection"]:::process
          Sessions["BroadcastSessionsManager"]:::process
      end

      subgraph GatewayNode["Gateway"]
          AIHandlers["AI HTTP Handlers<br/>server/ai_mediaserver.go"]:::node
          AISession["AISessionManager<br/>server/ai_session.go"]:::process
      end

      subgraph OrchestratorNode["OrchestratorNode"]
          ServeSegment["ServeSegment()<br/>server/segment_rpc.go"]:::node
          Orchestrator["orchestrator struct<br/>core/orchestrator.go"]:::process
          SegmentChans["SegmentChans<br/>map ManifestID to SegmentChan"]:::runtime
          Recipient["pm.Recipient<br/>Payment Processing"]:::payment
      end

      subgraph AIWorkerNode["AIWorkerNode"]
          RemoteAI["RemoteAIWorkerManager<br/>core/ai_worker.go"]:::runtime
          Docker["Docker Containers<br/>ai-runner images"]:::runtime
      end

      subgraph TranscoderNode["TranscoderNode"]
          RemoteTranscoder["RemoteTranscoderManager<br/>core/orchestrator.go"]:::runtime
          LocalTranscoder["LocalTranscoder<br/>core/transcoder.go"]:::runtime
          Ffmpeg["ffmpeg.Transcode()"]:::runtime
      end

      RTMP --> Server
      HLS --> Server
      API --> AIHandlers
      Server --> Segmenter --> Connections --> Sessions
      AIHandlers --> AISession
      Sessions --> ServeSegment
      AISession --> ServeSegment
      ServeSegment --> Orchestrator
      Orchestrator --> SegmentChans
      Orchestrator --> Recipient
      SegmentChans --> RemoteAI
      SegmentChans --> RemoteTranscoder
      RemoteAI --> Docker
      RemoteTranscoder --> LocalTranscoder --> Ffmpeg
  ```
</ScrollableDiagram>

<Card title={<CustomCardTitle icon="diagram-project" title="Network Architecture" />} href="/v2/about/network/architecture" horizontal arrow> The Network's three-layer topology in detail. </Card>

<CustomDivider style={{ margin: '0 0 -2rem 0' }} />

## Node Type Details

A node can operate in multiple roles simultaneously by having multiple flags set during startup.

<BorderedBox variant="accent">
  The codebase defines node types as an enumerated integer type with corresponding string representations in [core/livepeernode.go](https://github.com/livepeer/go-livepeer/blob/main/core/livepeernode.go).

  ```
  DefaultNode      = 0  "default"
  BroadcasterNode  = 1  "broadcaster"
  OrchestratorNode = 2  "orchestrator"
  TranscoderNode   = 3  "transcoder"
  RedeemerNode     = 4  "redeemer"
  AIWorkerNode     = 5  "aiworker"
  ```
</BorderedBox>

### Node Type Details

<AccordionGroup>
  <Accordion title="Orchestrator Node" icon="microchip">
    **Purpose**:
    The OrchestratorNode coordinates transcoding work, manages payments, and serves as the intermediary between broadcasters and transcoders.

    **Key Components**

    | Component             | Location                | Purpose                                  |
    | --------------------- | ----------------------- | ---------------------------------------- |
    | `orchestrator struct` | `core/orchestrator.go`  | Main orchestrator implementation         |
    | `Recipient`           | `core/livepeernode.go`  | Probabilistic micropayment recipient     |
    | `SegmentChans`        | `core/livepeernode.go`  | Map of segment channels per manifest     |
    | `Balances`            | `core/livepeernode.go`  | Track broadcaster balances per session   |
    | `lphttp`              | `server/segment_rpc.go` | HTTP/RPC handler for segment transcoding |

    The Orchestrator node type is responsible for performing transcoding and AI inference work. Orchestrators claim jobs, execute them, and earn fees and rewards.

    **Payment Processing**
    Orchestrators validate and process probabilistic micropayments using the ticket system, which involves:

    * Receiving payment information via Livepeer-Payment header
    * Validating tickets with ProcessPayment
    * Crediting balances for the broadcaster session
    * Redeeming winning tickets with the TicketBroker on-chain
    * Managing deposits and reserves to ensure solvency

    **Capacity Management**
    Orchestrators track capacity and reject work when at maximum sessions:

    * Maximum sessions controlled by MaxSessions variable
    * CheckCapacity verifies if manifest already has a channel or if capacity is available
    * Sessions tracked in SegmentChans map keyed by ManifestID

    **Segment Transcoding RPC**
    The HTTP RPC endpoint for segment transcoding is implemented in ServeSegment:

    * Parse Headers: Extract payment and segment data from Livepeer-Payment and Livepeer-Segment headers
    * Process Payment: Call ProcessPayment to validate and credit tickets
    * Submit to Transcoder: Route segment to transcoder via TranscodeSeg
    * Return Results: Encode transcoded data in response
  </Accordion>

  <Accordion title="Broadcaster Node" icon="video-camera">
    **Purpose**:
    The `BroadcasterNode` is responsible for ingesting live video streams and serving content to viewers.
    It acts as the entry point for content producers and coordinates with orchestrators for transcoding.

    **Key Components**

    | Component                  | Location                | Purpose                                                         |
    | -------------------------- | ----------------------- | --------------------------------------------------------------- |
    | `LivepeerServer`           | `server/mediaserver.go` | Main media server handling RTMP/HTTP ingestion and HLS playback |
    | `RTMPSegmenter`            | `server/mediaserver.go` | Segments RTMP streams into HLS                                  |
    | `rtmpConnections`          | `server/mediaserver.go` | Map of active stream connections                                |
    | `BroadcastSessionsManager` | `server/broadcast.go`   | Manages sessions with orchestrators                             |
    | `SessionPool`              | `server/broadcast.go`   | Pool of available orchestrator sessions                         |

    **Initialisation Flow**

    <ScrollableDiagram title="Gateway Node Initialisation and Stream Processing Pipeline" maxHeight="400px">
      ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': "Inter, 'Inter Fallback', -apple-system, system-ui" }}}%%
      flowchart TD
          classDef init fill:#1a1a1a,color:#E0E4E0,stroke:#71717a,stroke-width:2px
          classDef stage fill:#1a1a1a,color:#E0E4E0,stroke:#2b9a66,stroke-width:2px

          subgraph Init["Initialisation"]
              direction TB
              I1["Set NodeType = BroadcasterNode"]:::init
              I2["Configure LivepeerServer<br/>RtmpDisabled = false"]:::init
              I3["Register HTTPMux handlers<br/>/live/ ingest, /stream/ playback"]:::init
              I1 --> I2 --> I3
          end

          subgraph Pipeline["Stream Processing Pipeline"]
              direction TB
              S1["1. Stream Authentication<br/>createRTMPStreamIDHandler<br/>validates via AuthWebhookURL"]:::stage
              S2["2. Connection Registration<br/>registerConnection creates rtmpConnection<br/>and initialises BroadcastSessionsManager"]:::stage
              S3["3. Segmentation<br/>SegmentRTMPToHLS<br/>converts stream to HLS segments"]:::stage
              S4["4. Processing<br/>processSegment sends each segment<br/>to orchestrators for transcoding"]:::stage
              S5["5. Playlist Management<br/>results saved to storage<br/>and playlists updated"]:::stage
              S1 --> S2 --> S3 --> S4 --> S5
          end

          Init -.->|Ready| S1
      ```
    </ScrollableDiagram>

    **HTTP Push Support**
    Broadcasters can also accept HTTP push ingestion when started with `-httpIngest` flag:

    * Handler: HandlePush at /live/ endpoint in `server/segment_rpc.go`
    * Supports multipart responses for transcoded renditions
    * Implements watchdog timer to clean up inactive sessions after httpPushTimeout (60s)
  </Accordion>

  <Accordion title="Transcoder Node" icon="laptop-code">
    **Purpose**:
    The Transcoder node type is responsible for executing video transcoding tasks.

    **Key Components**

    | Component                 | Location               | Purpose                                 |
    | ------------------------- | ---------------------- | --------------------------------------- |
    | `RemoteTranscoderManager` | `core/orchestrator.go` | Manages the pool of remote transcoders  |
    | `LocalTranscoder`         | `core/transcoder.go`   | Performs local ffmpeg-based transcoding |
    | `Transcoder` interface    | `core/transcoder.go`   | Abstracts transcoding operations        |
    | `liveTranscoder`          | `core/orchestrator.go` | Wraps remote transcoder RPC calls       |

    **Transcoder Registration**
    Remote transcoders register with an orchestrator using the RegisterTranscoder RPC:

    The RemoteTranscoderManager maintains two data structures:

    * liveTranscoders (map for lookup) and
    * transcoders (slice for round-robin selection).
      Each liveTranscoder wraps a gRPC stream with channels for segment distribution (SegmentChan) and result collection (ResultChan).

    **Transcoding Workflow**

    1. Orchestrator receives segment transcoding request via ServeSegment
    2. Orchestrator selects a liveTranscoder from the pool
    3. Segment sent to liveTranscoder's SegmentChan
    4. Transcoder executes ffmpeg transcoding (local or remote)
    5. Transcoded result sent back via ResultChan
    6. Orchestrator returns transcoded segment to broadcaster

    **Hardware Acceleration**
    Transcoders support hardware-accelerated transcoding through:

    * Nvidia GPUs: Enabled via -nvidia flag with device IDs
    * Netint VPUs: Enabled via -netint flag

    The capabilities are reported during registration and used for session compatibility checking.
  </Accordion>

  <Accordion title="Gateway Node" icon="user-robot">
    **Purpose**:

    The Gateway role (enabled via -gateway flag) adds AI inference capabilities to a broadcaster node, allowing it to accept AI requests and coordinate with AI-capable orchestrators.

    **Key Components**

    | Component                | Location                          | Purpose                                              |
    | ------------------------ | --------------------------------- | ---------------------------------------------------- |
    | `AISessionManager`       | `server/mediaserver.go:117`       | Manages AI orchestrator sessions                     |
    | `AIMediaServer` handlers | `server/ai_mediaserver.go:63-132` | Exposes HTTP endpoints for AI requests               |
    | `AISession`              | `server/rpc.go`                   | Represents a session with an AI-capable orchestrator |

    The Gateway node type extends the Broadcaster functionality to support AI inference workloads. It manages sessions with AI-capable orchestrators and exposes HTTP endpoints for AI requests, allowing it to route both video and AI work through the same infrastructure.

    **AI Session Selection**

    The AISessionManager selects orchestrators for AI jobs using capability-based filtering:

    <ScrollableDiagram title="AI Session Selection" maxHeight="360px">
      ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': "Inter, 'Inter Fallback', -apple-system, system-ui" }}}%%
      flowchart LR
          classDef request fill:#1a1a1a,color:#E0E4E0,stroke:#71717a,stroke-width:2px
          classDef selector fill:#111827,color:#E0E4E0,stroke:#2b9a66,stroke-width:2px
          classDef result fill:#0d0d0d,color:#E0E4E0,stroke:#60a5fa,stroke-width:2px
          classDef chosen fill:#0d0d0d,color:#E0E4E0,stroke:#2b9a66,stroke-width:2px
          classDef candidate fill:#0d0d0d,color:#E0E4E0,stroke:#71717a,stroke-width:2px,stroke-dasharray:4 4

          Request["AI Request<br/>model_id, pipeline"]:::request
          Manager["AISessionManager"]:::selector
          Pool["Orchestrator Pool"]:::selector
          Filter["Capability Filter<br/>model, pipeline"]:::selector
          MinLS["MinLSSelector<br/>latency-based"]:::selector
          Session["AISession"]:::result
          OrchA["Orchestrator A<br/>LatencyScore: 0.05"]:::chosen
          OrchB["Orchestrator B<br/>LatencyScore: 0.12"]:::candidate

          Request --> Manager --> Pool --> Filter --> MinLS --> Session
          Session --> OrchA
          Session -.-> OrchB
      ```
    </ScrollableDiagram>

    The selection algorithm prioritizes orchestrators with:

    1. Compatible capabilities (correct pipeline and model)
    2. Available capacity
    3. Lowest latency score from previous requests

    **Live Video AI Processing**

    Gateway nodes can process live video streams through AI pipelines using the MediaMTX integration:

    1. Stream arrives at MediaMTX (via RTMP/WebRTC)
    2. MediaMTX calls gateway's /live/video-to-video/{stream_0}/start endpoint
    3. Gateway selects orchestrator with live AI capabilities
    4. Segments published to orchestrator via trickle protocol
    5. Processed segments written to output destination
  </Accordion>

  <Accordion title="AI Worker Node" icon="laptop-code">
    **Purpose**:
    The AI Worker node type executes AI inference tasks, either in Docker containers or by routing to external endpoints.

    **Key Components**

    | Component               | Location               | Purpose                               |
    | ----------------------- | ---------------------- | ------------------------------------- |
    | `RemoteAIWorkerManager` | `core/livepeernode.go` | Manages the pool of remote AI workers |
    | `AIWorker` interface    | `core/livepeernode.go` | Abstracts AI inference operations     |
    | Docker containers       | External               | Model-specific inference runners      |

    **Worker Registration**

    AI workers register with an orchestrator similar to transcoders:

    * The RemoteAIWorkerManager tracks workers in liveAIWorkers (map by worker ID) and aiWorkers (slice for selection).
    * Each liveAIWorker maintains a taskChans map keyed by request ID, allowing concurrent requests to the same worker.

    **Docker Container Management**

    Orchestrators with AI worker capability manage Docker containers for model inference:

    1. Container Lifecycle: Containers started on-demand when jobs arrive
    2. GPU Allocation: Orchestrator assigns GPU devices to containers
    3. Health Monitoring: Containers checked for responsiveness
    4. Capacity Tracking: Available/busy container slots tracked per model

    The container stop timeout is configurable via aiWorkerContainerStopTimeout (default 5s) to allow in-flight requests to complete before forcefully terminating.
  </Accordion>
</AccordionGroup>

<CustomDivider />

## Related Pages
