> ## 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 Governance and Voting

> How Livepeer's on-chain governance works, what an LPT holder can vote on, and how to use the voting power that comes with bonded stake. Covers the Livepeer Improvement Proposal lifecycle, Governor contract mechanics, delegation pass-through, and how to override an orchestrator.

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 governed by the people who hold and stake its token. Any holder with at least 100 LPT can submit a proposal. Any holder with bonded LPT can vote. The protocol enforces the outcome on-chain, automatically, without an intermediary.
</Quote>

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

## Why on-chain governance

A protocol that secures money needs a way to change its own rules without breaking that security. Livepeer chose on-chain governance for three reasons.

First, the participants who bear the risk of a change should have authority over the change. Bonded LPT is at stake against the protocol's behaviour. The same stake is the unit of voting weight. Influence and exposure are the same number.

Second, an on-chain vote produces a single canonical outcome. There is no separate execution layer, no foundation that signs the upgrade, and no off-chain consensus that participants might disagree about. The Governor contract executes whatever passes.

Third, governance becomes legible. Every vote is on Arbitrum. Every proposal is in the [LIPs repository](https://github.com/livepeer/LIPs). Every parameter change is traceable to a specific LIP and a specific block. There is no closed-door process to audit.

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

## What governance can change

The protocol scopes governance to two domains: the protocol itself, and the treasury.

<DynamicTableV2
  headerList={["Domain", "Examples", "Executed by"]}
  itemsList={[
{ "Domain": "Protocol upgrades", "Examples": "Migration to a new chain (LIP-73), enabling partial unbonding (LIP-8), service registry (LIP-9)", "Executed by": "Governor contract calling protocol contract upgrade paths" },
{ "Domain": "Parameter changes", "Examples": "Round length (LIP-83), inflation calculation (LIP-34, LIP-35, LIP-40), treasury contribution (LIP-92)", "Executed by": "Governor contract calling parameter setter functions" },
{ "Domain": "Treasury allocations", "Examples": "Funding SPEs, public goods grants", "Executed by": "LivepeerGovernor contract releasing treasury balance" },
{ "Domain": "Governance framework", "Examples": "Polling system (LIP-15), stake-weighted voting (LIP-69), governance mechanism (LIP-19), upgrade foundation (LIP-25)", "Executed by": "Governor contract calling governance contract upgrade paths" },
]}
/>

The protocol does not give governance authority over private operator behaviour, off-chain network coordination, or the day-to-day operation of Orchestrators and Gateways. Governance acts on the on-chain rules and on the treasury balance. Everything else is the network's job.

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

## The Livepeer Improvement Proposal lifecycle

Every protocol or treasury change passes through the same staged process. Skipping a stage is not an option - the on-chain rules require it.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Idea phase" icon="comment-dots">
    A proposer raises an idea on the <LinkArrow label="Livepeer Forum" href="https://forum.livepeer.org/c/lips/18" newline={false} /> or in Discord. Most ideas are refined or abandoned at this stage. The forum is the primary venue for early feedback.
  </StyledStep>

  <StyledStep title="Draft phase" icon="comments">
    The proposer drafts a Livepeer Improvement Proposal (LIP) - a structured document with a header preamble (number, title, author, type, status) and a full specification. The draft goes into the <LinkArrow label="LIPs repository" href="https://github.com/livepeer/LIPs" newline={false} /> as a pull request for editorial review.
  </StyledStep>

  <StyledStep title="Last call" icon="bell">
    Once editors approve the draft, it enters a 10-day Last Call period. The community has a final opportunity to surface objections before the proposal moves to a vote.
  </StyledStep>

  <StyledStep title="On-chain submission" icon="arrow-up-right-from-square">
    Anyone with at least 100 LPT can submit the LIP on-chain to the Governor contract. The 100 LPT is locked while the proposal is voted on, and is returned if the proposal passes. The threshold is large by design - it filters serious proposals from frivolous ones.
  </StyledStep>

  <StyledStep title="Voting period" icon="ballot">
    A 30-round voting window opens (\~3.75 days on Arbitrum). Eligible voters - Orchestrators and the LPT bonded to them - cast votes on-chain. Anyone with at least 1 LPT bonded can vote.
  </StyledStep>

  <StyledStep title="Quorum and approval" icon="people-group">
    A proposal passes only if at least 33% of all bonded LPT participates (quorum) and more than 50% of cast votes are For. Both thresholds must be met. Either alone is not sufficient.
  </StyledStep>

  <StyledStep title="Execution" icon="check-to-slot">
    On a successful vote, the Governor contract executes the proposal automatically - changing a contract parameter, upgrading a contract implementation, or releasing treasury funds. There is no separate signer, no multisig, and no execution delay beyond the protocol-defined queueing period.
  </StyledStep>
</StyledSteps>

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

## How voting power is calculated

Voting power equals bonded LPT - including stake delegated to your Orchestrator.

If an Orchestrator self-bonds 1,000 LPT and has 9,000 LPT delegated to it, the Orchestrator's vote carries 10,000 LPT of weight by default. The Orchestrator votes on behalf of all 10,000 unless individual Delegators override.

This default has two consequences. First, Orchestrators function as elected representatives of their Delegators on every proposal. Choosing an Orchestrator is partly a choice of governance proxy. Second, Delegators retain final authority over their own stake. On any specific proposal, a Delegator can override their Orchestrator's vote and have their delegated LPT counted separately.

The override is per-proposal, not permanent. A Delegator who overrides on one proposal returns to the default on the next.

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

## How to vote on a proposal

The flow is the same whether you are voting on a protocol LIP or a treasury proposal.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Read the proposal" icon="book-open">
    Locate the LIP in the <LinkArrow label="LIPs repository" href="https://github.com/livepeer/LIPs" newline={false} /> and the active proposal on <LinkArrow label="Livepeer Explorer" href="https://explorer.livepeer.org/voting" newline={false} />. The Explorer shows the on-chain proposal text, current vote tally, and the time remaining.
  </StyledStep>

  <StyledStep title="Check your voting power" icon="scale-balanced">
    Connect your wallet on the Explorer. Your voting power is your bonded LPT. If you delegate, your power flows through your Orchestrator unless you override.
  </StyledStep>

  <StyledStep title="Cast your vote" icon="check-to-slot">
    Submit For, Against, or Abstain on-chain. The vote costs gas and is final once mined. Abstain counts toward quorum but not toward the For/Against majority.
  </StyledStep>

  <StyledStep title="Override your orchestrator if needed" icon="user-shield">
    To override your Orchestrator's vote, cast your own vote during the voting window. Your delegated stake is reassigned for that proposal only.
  </StyledStep>
</StyledSteps>

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

## Where to go next

<Columns cols={2}>
  <Card title={<CustomCardTitle icon="vault" title="Governance and Treasury Reference" />} href="/v2/about/protocol/governance-and-treasury" horizontal arrow>
    Canonical reference: governance functions, key LIPs, on-chain contracts.
  </Card>

  <Card title={<CustomCardTitle icon="bank" title="Treasury and Proposals" />} href="/v2/about/guides/treasury-and-proposals" horizontal arrow>
    How treasury proposals differ, what gets funded, and how to submit one.
  </Card>

  <Card title={<CustomCardTitle icon="ballot-check" title="Livepeer Explorer" />} href="https://explorer.livepeer.org/voting" horizontal arrow>
    Active proposals, on-chain votes, and historical governance outcomes.
  </Card>

  <Card title={<CustomCardTitle icon="github" title="LIPs Repository" />} href="https://github.com/livepeer/LIPs" horizontal arrow>
    Every LIP, draft and final, with full specifications and discussion history.
  </Card>
</Columns>
