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

# Network Tools and Metrics

> How to read the Livepeer Network from the outside. Covers the five public observability surfaces - Explorer, subgraph, dashboards, leaderboard, and capabilities API - plus how to embed live network data in an external site.

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>
  The Livepeer Network is observable from five public surfaces. Each surface answers a different class of question - on-chain state, historical activity, real-time performance, capability availability, or governance outcomes. Knowing which surface to read is half the work.
</Quote>

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

## Why a guide on observability

The protocol stores its rules and balances on Arbitrum One. The network's actual work happens off-chain across Orchestrators, Gateways, and pipelines that the chain never sees. Reading the network well requires combining on-chain state with off-chain signal - and knowing which signal each surface gives you.

A founder evaluating Livepeer needs different data than an Orchestrator monitoring its own performance, or a developer integrating a metrics widget into their site. This guide maps the available surfaces to the questions they answer.

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

## The five public surfaces

<DynamicTableV2
  headerList={["Surface", "What it shows", "Update cadence", "Best for"]}
  itemsList={[
{ "Surface": "Livepeer Explorer", "What it shows": "On-chain state - active set, bonded supply, inflation, votes, treasury balance", "Update cadence": "Per Arbitrum block (~1 second)", "Best for": "Holders, voters, evaluators" },
{ "Surface": "Subgraph", "What it shows": "Indexed historical on-chain events with structured queries", "Update cadence": "Per block, with indexer lag (seconds to minutes)", "Best for": "Analytics, dashboards, custom integrations" },
{ "Surface": "Network dashboards (Grafana)", "What it shows": "Off-chain network performance - throughput, latency, success rates per pipeline", "Update cadence": "Real-time", "Best for": "Network operators, SPE leads, performance-sensitive integrators" },
{ "Surface": "Performance leaderboard", "What it shows": "Per-orchestrator quality metrics - score, success rate, regional latency", "Update cadence": "Periodic, off-chain", "Best for": "Gateways selecting orchestrators, delegators choosing where to bond" },
{ "Surface": "Capabilities API", "What it shows": "Live orchestrator advertisements - which capabilities are available, at what price, with what availability", "Update cadence": "Real-time, polled per gateway", "Best for": "Gateways, integrators, BYOC pipeline developers" },
]}
/>

Each surface has a strict scope. The Explorer cannot tell you whether an Orchestrator is currently online. The Grafana dashboards cannot tell you who voted on a LIP. The capabilities API cannot tell you historical revenue. Use the surface that matches the question.

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

## Livepeer Explorer

The Explorer is the canonical view of on-chain protocol state. Everything the protocol contracts hold is queryable here.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Active set and bonded supply" icon="layer-group">
    The full active Orchestrator set, sorted by total bonded stake, with self-bond, delegated stake, and round status visible per Orchestrator.
  </StyledStep>

  <StyledStep title="Inflation and rewards" icon="chart-line">
    Per-round inflation rate, total LPT supply, treasury balance, and reward distribution. The bonded share is shown against the protocol's target.
  </StyledStep>

  <StyledStep title="Governance" icon="ballot-check">
    Active and historical proposals, on-chain vote tallies, voter participation, and time remaining on open votes.
  </StyledStep>

  <StyledStep title="Per-account view" icon="user">
    Bonded balance, unbonding locks, claimable earnings, voting history. Connect a wallet to see your own state and to act on it.
  </StyledStep>
</StyledSteps>

<Tip>
  The Explorer is the surface to use for any answer that depends on what is actually committed on-chain - not what an Orchestrator says it can do, not what historical analytics report. If a question can be answered by reading a contract, use the Explorer.
</Tip>

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

## Subgraph and historical analytics

The Livepeer subgraph indexes every on-chain event into a queryable schema. It is the right surface for time-series questions, custom dashboards, and any analysis that needs more than current state.

Common analytics patterns:

* **Network usage** - segments transcoded, AI inference calls, total minutes per period.
* **Fee revenue** - ETH redeemed by Orchestrators per period, demand-side fees per Gateway.
* **Orchestrator performance** - per-Orchestrator round-by-round reward calls, fee earnings, stake changes.
* **Governance participation** - votes cast per LIP, quorum trajectories, Orchestrator vs Delegator participation rates.
* **Treasury activity** - treasury inflows from inflation, outflows from passed proposals, balance trajectories.

The subgraph is queried directly with GraphQL, and is the data source behind most public dashboards. For one-off analysis, query it directly. For embedded data products, use it as the backend.

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

## Network dashboards and the leaderboard

Off-chain network performance is published through Grafana dashboards and a performance leaderboard. These surfaces answer questions the chain cannot - latency, throughput, success rate per pipeline, geographical distribution of work.

<DynamicTableV2
  headerList={["Surface", "Source", "What it surfaces"]}
  itemsList={[
{ "Surface": "Grafana - network operations", "Source": "Off-chain telemetry from orchestrators and gateways", "What it surfaces": "Live throughput per workload class, segment success rate, pipeline latency, error trends" },
{ "Surface": "Performance leaderboard", "Source": "Periodic public benchmarks against orchestrators", "What it surfaces": "Per-orchestrator score, regional latency, recent success rate, capability mix" },
{ "Surface": "Per-orchestrator transparency reports", "Source": "Self-published by SPEs and operators", "What it surfaces": "Operational changes, incident reports, upgrade plans" },
]}
/>

These surfaces have a structural blind spot: they only see what their telemetry covers. An Orchestrator that does not publish telemetry is invisible to the dashboards even when it is serving production traffic. Combine off-chain dashboards with on-chain data to see the full picture.

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

## Capabilities API

Each Orchestrator publishes its current capabilities as a real-time advertisement: video transcoding profiles, AI inference pipelines, BYOC containers, real-time AI sessions, and the prices for each. Gateways read this advertisement to assemble a working set per job.

For an external observer or integrator, the capabilities API is the surface to use to answer:

* Which Orchestrators currently advertise a given AI pipeline.
* What prices Orchestrators are quoting for transcoding profiles.
* Where the network has spare capacity for a specific workload.
* Whether a BYOC container is being run by anyone in the Active Set.

Capability advertisements are bitstrings, not free-form text. The list is governed - new capabilities require a registration that propagates to Gateways. The advertisement structure is stable enough to query programmatically.

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

## Embedding live network data

Several of the surfaces above are designed to be embedded in external sites - a portfolio tracker, an Orchestrator's transparency page, a public ecosystem dashboard.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Embed a metrics widget" icon="chart-line">
    The repository provides components that render live Livepeer data - bonded supply, inflation rate, active Orchestrator count, treasury balance - as standalone widgets backed by the subgraph.
  </StyledStep>

  <StyledStep title="Embed a Grafana panel" icon="gauge-high">
    Grafana panels can be exposed as iframes for read-only embedding. Use this for off-chain throughput, success rate, and pipeline-specific dashboards.
  </StyledStep>

  <StyledStep title="Render an Explorer link" icon="link">
    For state that updates per block, a contextual link to the Explorer is more current than any cached widget. Live state should resolve to the Explorer.
  </StyledStep>

  <StyledStep title="Build a custom integration" icon="code">
    For analytics that no public surface answers directly - custom segmentation, Orchestrator cohort comparison, treasury allocation analysis - query the subgraph and render in your own UI.
  </StyledStep>
</StyledSteps>

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

## Where to go next

<Columns cols={2}>
  <Card title={<CustomCardTitle icon="chart-line" title="Network Metrics Reference" />} href="/v2/about/guides/network-metrics" horizontal arrow>
    Every Explorer metric explained, with formulas for institutional analysis.
  </Card>

  <Card title={<CustomCardTitle icon="eye" title="Network Observability" />} href="/v2/about/network/observability" horizontal arrow>
    Read paths, surface trade-offs, and off-chain blind spots in detail.
  </Card>

  <Card title={<CustomCardTitle icon="globe" title="Livepeer Explorer" />} href="https://explorer.livepeer.org/" horizontal arrow>
    Live on-chain state, governance proposals, and per-Orchestrator data.
  </Card>

  <Card title={<CustomCardTitle icon="diagram-project" title="Subgraph" />} href="https://thegraph.com/explorer/subgraphs?search=livepeer" horizontal arrow>
    Indexed historical events for custom analytics and embedded dashboards.
  </Card>
</Columns>
