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

# Builders Guide

> How to contribute to Livepeer at the protocol, network, or pipeline level. Covers the four canonical GitHub repositories, the three contributor paths, the build and test workflow for go-livepeer, and how new code reaches a production network upgrade.

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 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 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>;
};

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 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>;
};

<Quote>
  Livepeer is built in the open, by a community of contributors and Special Purpose Entities. The protocol contracts, the node software, the LIP repository, and the research output are all public. Anyone can read them; anyone can propose changes; only stake-weighted on-chain governance can ship them.
</Quote>

<CustomDivider style={{ margin: 0, marginBottom: "-2rem" }} />

## The three contributor paths

There is no single "Livepeer developer" role. Different parts of the system have different skill profiles and different review processes. Choosing the right path makes the contribution loop short.

<DynamicTableV2
  headerList={["Path", "Who it suits", "Where the work lives", "Review process"]}
  itemsList={[
{ "Path": "Core protocol contributor", "Who it suits": "Solidity engineers, mechanism designers, cryptoeconomic researchers", "Where the work lives": "livepeer/protocol, livepeer/LIPs", "Review process": "RFC on forum, LIP draft, on-chain vote, contract upgrade" },
{ "Path": "Node software contributor", "Who it suits": "Go engineers, video / AI infrastructure engineers, DevOps", "Where the work lives": "livepeer/go-livepeer", "Review process": "Issue or RFC, GitHub PR, code review by maintainers, release tag" },
{ "Path": "Network and pipeline contributor", "Who it suits": "AI / video pipeline developers, BYOC container authors, integration engineers", "Where the work lives": "BYOC containers, SPE-led pipeline repos, integration projects", "Review process": "Direct integration; no protocol change required for most work" },
]}
/>

A contribution to a contract requires a LIP. A contribution to the node binary requires a code review. A new BYOC pipeline does not require either - it ships when an Orchestrator decides to run it.

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

## The repositories

Four repositories cover almost everything a contributor needs to read or touch.

<DynamicTableV2
  headerList={["Repository", "Purpose"]}
  itemsList={[
{ "Repository": <LinkArrow label="livepeer/protocol" href="https://github.com/livepeer/protocol" newline={false} />, "Purpose": "Solidity contracts deployed on Arbitrum One. BondingManager, TicketBroker, RoundsManager, Minter, Governor, Treasury, LivepeerToken." },
{ "Repository": <LinkArrow label="livepeer/go-livepeer" href="https://github.com/livepeer/go-livepeer" newline={false} />, "Purpose": "Go implementation of the Orchestrator, Gateway, transcoder, and AI worker node software. The reference network client." },
{ "Repository": <LinkArrow label="livepeer/LIPs" href="https://github.com/livepeer/LIPs" newline={false} />, "Purpose": "Every Livepeer Improvement Proposal - draft, accepted, or final. The formal record of every protocol change." },
{ "Repository": <LinkArrow label="livepeer/research" href="https://github.com/livepeer/research" newline={false} />, "Purpose": "Mechanism design notes, simulations, public-facing analysis. Pre-LIP exploration." },
]}
/>

Two more repositories are useful but secondary: Livepeer/wiki for community documentation, and Livepeer/explorer-2 for the on-chain explorer UI.

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

## Building go-livepeer from source

The reference node implementation is Go-based with significant native FFmpeg and CUDA dependencies for video and AI workloads. A local build is the foundation of any node-software contribution.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Install dependencies" icon="screwdriver-wrench">
    Install Go (per the version pinned in go.mod), and run the FFmpeg setup script (`install_ffmpeg.sh`) which builds a custom FFmpeg with the codecs Livepeer requires (libx264, libfdk-aac, and the platform-appropriate hardware encoders). For GPU work, a matching CUDA installation is required.
  </StyledStep>

  <StyledStep title="Build the binary" icon="hammer">
    From the repository root, `make` produces the `livepeer` binary in the local build directory. Cross-compilation flags are documented in the Makefile; CI builds the official release artefacts for Linux (amd64/arm64), macOS (amd64/arm64), and Windows, in CPU and CUDA variants.
  </StyledStep>

  <StyledStep title="Run the test suites" icon="vial">
    Unit tests run with `go test ./...`. End-to-end tests live in `test/e2e/` and are executed via `test_e2e.sh`. Static analysis is run by golangci-lint, revive, misspell, and CodeQL on every PR.
  </StyledStep>

  <StyledStep title="Spin up a local devnet" icon="network-wired">
    The `devtool` utility brings up a private Ethereum and a small set of local Livepeer nodes for end-to-end testing without touching mainnet or testnet. Use it to validate any change that touches network behaviour.
  </StyledStep>
</StyledSteps>

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

## How a code change reaches production

The flow depends on which repository the change touches.

### Node software (go-livepeer)

A node-software change is reviewed through GitHub. The path is short: open an issue or post on the forum if the change is non-trivial, submit a pull request, address review feedback, and wait for merge. Significant changes are tagged into a release that Orchestrators and Gateways then upgrade to. There is no on-chain step, because the node software is run by individual operators - upgrades are voluntary unless the change is required by a protocol upgrade.

### Protocol contracts (Livepeer/protocol)

A contract change requires a Livepeer Improvement Proposal. The reason is structural: the contracts are upgraded by the Governor contract on a successful on-chain vote, so any change must travel through the LIP process. The path is longer: discuss on the forum, draft a LIP with a full specification, submit through the LIPs repository for editorial review, complete the 10-day Last Call period, submit on-chain to the Governor with the 100 LPT submission stake, win the 30-round vote at 33% quorum and majority approval, and then execute. The implementation PR can be reviewed in parallel; what cannot be skipped is the governance signal.

### Pipelines and BYOC

A new pipeline or BYOC container ships independently. The Orchestrator runs it; the Gateway selects it; the protocol settles payment as it would for any built-in pipeline. There is no LIP required, no node-software upgrade required, and no governance vote required. New workload classes can be added without touching the protocol.

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

## What to read before contributing

A contributor with deep context produces a contribution that gets merged. A contributor without context produces noise that maintainers have to triage.

<DynamicTableV2
  headerList={["If you intend to work on", "Read first"]}
  itemsList={[
{ "If you intend to work on": "Contracts (BondingManager, TicketBroker, etc.)", "Read first": "Blockchain Contracts reference, Mechanisms page, the LIPs that established the contract you're touching, and the contract's tests." },
{ "If you intend to work on": "Inflation, rewards, or rounds", "Read first": "Livepeer Token reference, Mechanisms (Rounds and Rewards tabs), the relevant monetary-policy LIPs (LIP-34, LIP-35, LIP-40, LIP-83, LIP-100)." },
{ "If you intend to work on": "Governance or treasury", "Read first": "Governance and Voting guide, Treasury and Proposals guide, LIP-1, LIP-15, LIP-19, LIP-25, LIP-69, LIP-89, LIP-91, LIP-92." },
{ "If you intend to work on": "Node software (transcoding path)", "Read first": "go-livepeer overview, the Network Architecture page, Job Pipelines page, Marketplace Model page." },
{ "If you intend to work on": "AI worker or BYOC", "Read first": "Job Pipelines (Real-time AI and BYOC sections), Network Interfaces, the orchestrator capability advertisement format." },
]}
/>

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

## Where to participate beyond code

Code is one contribution. Several others matter as much.

* **Forum participation.** Almost every LIP starts as a forum thread. Engaged commenters shape proposals before they ever reach the chain.
* **Vote.** Bonded LPT carries a vote. Voting on proposals is a form of contribution.
* **Run an Orchestrator or Gateway.** The network needs operators who care about quality. Operating a node is the most direct way to learn the system.
* **Author a LIP.** Even without writing code, drafting a LIP that the community adopts is a high-leverage contribution.
* **Publish research.** Mechanism design, performance benchmarks, public analysis - all directly useful to the protocol's evolution.

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

## Where to go next

<Columns cols={2}>
  <Card title={<CustomCardTitle icon="github" title="livepeer/go-livepeer" />} href="https://github.com/livepeer/go-livepeer" horizontal arrow>
    The reference node implementation. Build, test, and contribute.
  </Card>

  <Card title={<CustomCardTitle icon="github" title="livepeer/protocol" />} href="https://github.com/livepeer/protocol" horizontal arrow>
    Solidity contracts deployed on Arbitrum One.
  </Card>

  <Card title={<CustomCardTitle icon="github" title="livepeer/LIPs" />} href="https://github.com/livepeer/LIPs" horizontal arrow>
    The full record of protocol proposals - draft, accepted, and final.
  </Card>

  <Card title={<CustomCardTitle icon="comments" title="Livepeer Forum" />} href="https://forum.livepeer.org/" horizontal arrow>
    Where every non-trivial change starts. Read, post, and engage.
  </Card>
</Columns>
