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

# Gateway Middleware

> Add a middleware layer to a Livepeer Gateway for authentication, rate limiting, billing integration, custom routing logic, and BYOC pipeline support.

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

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

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

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

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

<Tip>
  the **request pipeline** - the layer between clients and the Gateway.
  <br />The **payment pipeline** (how the Gateway settles with Orchestrators) is covered in <LinkArrow href="/v2/gateways/guides/payments-and-pricing/clearinghouse-guide" label="Clearinghouses" newline={false} />.
</Tip>

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

The `go-livepeer` binary is a routing and payment engine. It does not authenticate users, meter usage, or route premium customers to different Orchestrators.
<br /> Those concerns belong to a middleware layer sitting between clients and the Gateway process.

## Responsibilities

<ScrollableDiagram title="Middleware Architecture" maxHeight="500px">
  ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
  %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#18794E', 'primaryTextColor': '#fff', 'primaryBorderColor': '#3CB540', 'lineColor': '#3CB540', 'mainBkg': '#18794E', 'nodeBorder': '#3CB540', 'clusterBkg': 'transparent', 'clusterBorder': '#3CB540', 'titleColor': '#3CB540', 'edgeLabelBackground': 'transparent', 'textColor': '#3CB540', 'nodeTextColor': '#fff'}}}%%
  flowchart TB
      C[Client App] --> MW
      subgraph MW["Middleware Layer"]
        A[Auth / API keys]
        RL[Rate limiting]
        M[Usage metering]
        R[Request routing]
      end
      MW --> GW
      subgraph GW["go-livepeer Gateway"]
        O[Orchestrator selection]
        P[Payment tickets]
      end
      GW --> ORCH[Orchestrator Network]
  ```
</ScrollableDiagram>

The middleware handles the customer-facing interface. The Gateway handles the network interface.

<StyledTable>
  <TableRow header>
    <TableCell header>Concern</TableCell>
    <TableCell header>Middleware</TableCell>
    <TableCell header>go-livepeer</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>API key validation</TableCell>
    <TableCell>Yes</TableCell>
    <TableCell>No</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>JWT / OAuth verification</TableCell>
    <TableCell>Yes</TableCell>
    <TableCell>No</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Per-customer rate limits</TableCell>
    <TableCell>Yes</TableCell>
    <TableCell>No</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Usage tracking and billing</TableCell>
    <TableCell>Yes</TableCell>
    <TableCell>No</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Customer-to-tier routing</TableCell>
    <TableCell>Yes</TableCell>
    <TableCell>No</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Orchestrator selection</TableCell>
    <TableCell>No</TableCell>
    <TableCell>Yes</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Probabilistic Micropayments</TableCell>
    <TableCell>No</TableCell>
    <TableCell>Yes</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>On-chain settlement</TableCell>
    <TableCell>No</TableCell>
    <TableCell>Yes</TableCell>
  </TableRow>
</StyledTable>

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

## Architecture

Three deployment patterns cover most middleware use cases.

<BorderedBox>
  <Tabs>
    <Tab title="Reverse Proxy" icon="shield-halved">
      The simplest and most maintainable pattern: an existing reverse proxy handles TLS, auth, and rate limiting before passing requests to the Gateway.

      **Caddy with API key auth** (simplest):

      ```caddyfile icon="terminal" Caddy File theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      ai-gateway.yourdomain.com {
          @authenticated {
              header Authorization "Bearer {$VALID_API_KEY}"
          }
          handle @authenticated {
              reverse_proxy localhost:8935
          }
          respond 401
      }
      ```

      **Nginx with external auth** (more flexible - delegates auth to a backend):

      ```nginx icon="terminal" NGINX theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      server {
          listen 443 ssl;
          server_name ai-gateway.yourdomain.com;

          location / {
              auth_request /auth;
              proxy_pass http://127.0.0.1:8935;
          }

          location = /auth {
              proxy_pass http://127.0.0.1:3000/verify-token;
              proxy_pass_request_body off;
              proxy_set_header Content-Length "";
              proxy_set_header X-Original-URI $request_uri;
          }
      }
      ```

      The `/verify-token` endpoint returns `200` to allow or `401` to deny. It can also return headers that nginx forwards to the Gateway, such as customer ID or tier.
    </Tab>

    <Tab title="API Gateway" icon="layer-group">
      For multi-tenant deployments where per-customer rate limits, quota enforcement, and usage analytics are needed, a dedicated API gateway layer fits well.

      **Kong** (open source, self-hosted):

      ```yaml icon="terminal" Kong theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      # kong.yml - declarative config
      services:
      - name: livepeer-gateway
          url: http://livepeer-gateway:8935
          routes:
          - name: ai-inference
              paths:
              - /
          plugins:
          - name: key-auth
          - name: rate-limiting
              config:
              minute: 60
              policy: local
          - name: request-termination
              config:
              status_code: 429
              message: "Rate limit exceeded"
      ```

      **Traefik** (Docker-native, automatic service discovery):

      ```yaml icon="terminal" Traefik theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      # docker-compose.yml
      services:
      traefik:
          image: traefik:v3
          command:
          - "--providers.docker=true"
          - "--entrypoints.websecure.address=:443"

      livepeer-gateway:
          image: livepeer/go-livepeer:master
          labels:
          - "traefik.http.routers.livepeer.rule=Host(`ai-gateway.yourdomain.com`)"
          - "traefik.http.routers.livepeer.middlewares=auth@docker"
          - "traefik.http.middlewares.auth.basicauth.users=user:$$apr1$$..."
      ```
    </Tab>

    <Tab title="Custom Proxy" icon="code">
      For operators who need full control over routing logic, a lightweight custom proxy in Node.js, Go, or Python sits in front of the Gateway.

      **Node.js example** (Express + http-proxy-middleware):

      ```javascript icon="code"  Custom Proxy theme={"theme":{"light":"github-light","dark":"dark-plus"}}
      const express = require('express');
      const { createProxyMiddleware } = require('http-proxy-middleware');
      const app = express();

      // Auth middleware
      app.use(async (req, res, next) => {
      const apiKey = req.headers['x-api-key'];
      const customer = await validateApiKey(apiKey);
      if (!customer) return res.status(401).json({ error: 'Invalid API key' });
      req.customer = customer;
      next();
      });

      // Usage metering
      app.use((req, res, next) => {
      const start = Date.now();
      res.on('finish', () => {
          recordUsage(req.customer.id, req.path, Date.now() - start);
      });
      next();
      });

      // Route to Gateway
      app.use('/', createProxyMiddleware({
      target: 'http://localhost:8935',
      changeOrigin: true,
      }));

      app.listen(3000);
      ```
    </Tab>
  </Tabs>
</BorderedBox>

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

## Authentication

<AccordionGroup>
  <Accordion title="API keys" icon="key">
    The simplest approach. Issue each customer a unique API key, validate it on every request, and store it hashed in a database.

    ```icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    Request: X-Api-Key: sk_live_abc123...
    Middleware: hash(sk_live_abc123...) -> look up in DB -> get customer record
    ```

    API keys are easy to rotate without requiring customers to re-authenticate. They are appropriate for server-to-server integrations where the key is stored securely on the client side.
  </Accordion>

  <Accordion title="JWT tokens" icon="id-badge">
    For consumer-facing applications where users authenticate via OAuth (Google, GitHub, etc.), issue short-lived JWT access tokens after authentication. Validate the JWT signature on every request without a database lookup.

    ```javascript icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    const jwt = require('jsonwebtoken');

    function verifyToken(req, res, next) {
      const token = req.headers.authorization?.replace('Bearer ', '');
      try {
        const payload = jwt.verify(token, process.env.JWT_SECRET);
        req.customer = { id: payload.sub, tier: payload.tier };
        next();
      } catch {
        res.status(401).json({ error: 'Invalid token' });
      }
    }
    ```

    JWTs are appropriate for Gateways that serve end-users directly through a web or mobile application.
  </Accordion>

  <Accordion title="Live AI webhook auth" icon="webhook">
    For Live AI (video-to-video) workloads, go-livepeer supports webhook-based RTMP authentication. The Gateway calls the webhook when an RTMP stream connects, and the webhook returns allow or deny.

    ```bash icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    -liveAIAuthWebhookUrl https://auth-service.example.com/rtmp-auth
    -liveAIAuthApiKey <internal-api-key-for-webhook>
    ```

    The webhook receives the stream key and can validate it against a customer database before allowing the stream through.
  </Accordion>
</AccordionGroup>

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

## Settings

<StyledSteps>
  <StyledStep icon="integral">
    ### Rate Limiting

    Rate limiting protects the Gateway from abuse and enforces fair usage across customers. Implement it in the middleware layer, not in go-livepeer.

    The appropriate limits depend on the Orchestrator pool capacity and customer agreements. A reasonable starting point for an AI inference Gateway:

    <StyledTable>
      <TableRow header>
        <TableCell header>Tier</TableCell>
        <TableCell header>Requests per minute</TableCell>
        <TableCell header>Concurrent sessions</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>Free</TableCell>
        <TableCell>10</TableCell>
        <TableCell>1</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>Standard</TableCell>
        <TableCell>60</TableCell>
        <TableCell>5</TableCell>
      </TableRow>

      <TableRow>
        <TableCell>Premium</TableCell>
        <TableCell>300</TableCell>
        <TableCell>20</TableCell>
      </TableRow>
    </StyledTable>

    Track limits in Redis for shared-memory rate limiting across multiple middleware instances:

    ```javascript icon="code" Redis theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    const rateLimit = require('express-rate-limit');
    const RedisStore = require('rate-limit-redis');

    const limiter = rateLimit({
    windowMs: 60 * 1000,
    max: (req) => req.customer.tier === 'premium' ? 300 : 60,
    store: new RedisStore({ client: redisClient }),
    keyGenerator: (req) => req.customer.id,
    });
    ```

    <CustomDivider style={{margin: "-2.5rem auto -2rem auto", width: "50%"}} />
  </StyledStep>

  <StyledStep icon="route">
    ### Custom Routing

    The most powerful use of middleware is routing different customers or job types to different Gateway instances, each configured with a distinct Orchestrator pool.

    ```javascript icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    // Route premium customers to high-performance Orchestrators
    app.use('/process', async (req, res) => {
    const target = req.customer.tier === 'premium'
        ? 'http://localhost:8935'   // Gateway A: premium orch list, high price cap
        : 'http://localhost:8936';  // Gateway B: standard orch list, lower price cap

    proxyRequest(req, res, target);
    });
    ```

    This pattern, combined with <LinkArrow href="/v2/gateways/guides/advanced-operations/orchestrator-selection#tiering-strategy" label="tiered Orchestrator selection" newline={false} />, allows delivering different SLA levels from one middleware deployment.

    <CustomDivider style={{margin: "-2.5rem auto -2rem auto", width: "50%"}} />
  </StyledStep>

  <StyledStep icon="gauge-high">
    ### Usage Metering

    go-livepeer does not track how much each customer has consumed - it only tracks what the Gateway owes Orchestrators. Customer usage must be metered in the middleware.

    What to track per request:

    * Customer ID
    * Pipeline type (text-to-image, video transcoding, etc.)
    * Input parameters (resolution, number of outputs, duration)
    * Response time
    * Success or failure

    Store events in a time-series database or event queue, then aggregate for invoicing.

    ```javascript icon="code" Usage Metering theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    res.on('finish', () => {
    usageQueue.push({
        customerId: req.customer.id,
        pipeline: extractPipeline(req.path),
        durationMs: Date.now() - startTime,
        statusCode: res.statusCode,
        timestamp: new Date().toISOString(),
    });
    });
    ```

    <CustomDivider style={{margin: "-2.5rem auto -2rem auto", width: "50%"}} />
  </StyledStep>
</StyledSteps>

## Middleware vs Clearinghouse

A clearinghouse combines middleware concerns with payment pipeline concerns into a single managed service:

<StyledTable>
  <TableRow header>
    <TableCell header>Concern</TableCell>
    <TableCell header>Custom middleware</TableCell>
    <TableCell header>Clearinghouse</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Customer auth</TableCell>
    <TableCell>Operator manages</TableCell>
    <TableCell>Managed</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Usage billing</TableCell>
    <TableCell>Operator manages</TableCell>
    <TableCell>Managed</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>ETH key custody</TableCell>
    <TableCell>Not in scope</TableCell>
    <TableCell>Managed</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Orchestrator payments</TableCell>
    <TableCell>go-livepeer handles</TableCell>
    <TableCell>Managed</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Multi-tenant isolation</TableCell>
    <TableCell>Operator builds</TableCell>
    <TableCell>Built in</TableCell>
  </TableRow>

  <TableRow>
    <TableCell>Fiat settlement</TableCell>
    <TableCell>Operator builds</TableCell>
    <TableCell>Built in</TableCell>
  </TableRow>
</StyledTable>

To avoid building the payment side, see <LinkArrow href="/v2/gateways/guides/payments-and-pricing/clearinghouse-guide" label="Clearinghouses" newline={false} /> for delegating ETH custody and payment accounting while keeping custom middleware for auth and routing.

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

## Related Pages

<CardGroup cols={2}>
  <Card title="Scaling" icon="arrow-up-right-dots" href="/v2/gateways/guides/advanced-operations/scaling">
    Scale middleware and Gateway instances together.
  </Card>

  <Card title="Publishing a Gateway" icon="globe" href="/v2/gateways/guides/advanced-operations/gateway-discoverability">
    Make the Gateway discoverable to external developers.
  </Card>

  <Card title="Clearinghouses" icon="building-columns" href="/v2/gateways/guides/payments-and-pricing/clearinghouse-guide">
    Delegate payment handling to a clearinghouse service.
  </Card>

  <Card title="Orchestrator Selection" icon="server" href="/v2/gateways/guides/advanced-operations/orchestrator-selection">
    Tiering, scoring, and failover for Orchestrator selection.
  </Card>
</CardGroup>
