> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# core.numericInvariant

> Assert a numeric comparison against an op's output at simulation time

export const ComposeItemDetail = ({kind, id}) => {
  const COMPOSE_ENVS = [{
    id: 'production',
    label: 'Production',
    base: 'https://composer.li.quest',
    host: 'composer.li.quest'
  }, {
    id: 'ethglobal',
    label: 'ETHGlobal preview',
    base: 'https://ethglobal-composer.li.quest',
    host: 'ethglobal-composer.li.quest'
  }];
  const ENV_STORAGE_KEY = 'lifi-composer-docs-env';
  const [envId, setEnvId] = useState(() => {
    if (typeof window === 'undefined') return 'production';
    try {
      const stored = window.localStorage?.getItem(ENV_STORAGE_KEY);
      return stored && COMPOSE_ENVS.some(e => e.id === stored) ? stored : 'production';
    } catch {
      return 'production';
    }
  });
  const env = COMPOSE_ENVS.find(e => e.id === envId) ?? COMPOSE_ENVS[0];
  const selectEnv = next => {
    if (!COMPOSE_ENVS.some(e => e.id === next)) return;
    setEnvId(next);
    try {
      window.localStorage?.setItem(ENV_STORAGE_KEY, next);
    } catch {}
  };
  const [state, setState] = useState({
    data: null,
    error: null
  });
  const [copied, setCopied] = useState(null);
  useEffect(() => {
    let cancelled = false;
    setState({
      data: null,
      error: null
    });
    const path = kind === 'edge' ? '/compose/zap-packs' : '/compose/manifest';
    const run = async () => {
      try {
        const response = await fetch(`${env.base}${path}`);
        if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
        const body = await response.json();
        if (!body || body.success !== true) {
          throw new Error(body?.error?.message ?? 'Unexpected response shape');
        }
        if (!cancelled) setState({
          data: body.data,
          error: null
        });
      } catch (err) {
        if (!cancelled) setState({
          data: null,
          error: err.message ?? String(err)
        });
      }
    };
    run();
    return () => {
      cancelled = true;
    };
  }, [kind, id, env.base]);
  const envTabs = <div style={{
    marginBottom: '1rem'
  }}>
      <div role="tablist" aria-label="Compose backend environment" style={{
    display: 'inline-flex',
    gap: '0.25rem',
    padding: '0.25rem',
    borderRadius: '8px',
    border: '1px solid rgba(127,127,127,0.3)',
    background: 'rgba(127,127,127,0.08)'
  }}>
        {COMPOSE_ENVS.map(e => {
    const active = e.id === env.id;
    return <button key={e.id} type="button" role="tab" aria-selected={active} title={`Fetch live data from ${e.base}`} onClick={() => selectEnv(e.id)} style={{
      cursor: 'pointer',
      border: 'none',
      borderRadius: '6px',
      padding: '0.35rem 0.9rem',
      fontSize: '0.9em',
      fontWeight: active ? 600 : 500,
      background: active ? '#3b82f6' : 'transparent',
      color: active ? '#fff' : 'inherit',
      transition: 'background 0.12s ease'
    }}>{e.label}</button>;
  })}
      </div>
      <div style={{
    marginTop: '0.35rem',
    fontSize: '0.8em',
    opacity: 0.65
  }}>
        Live data from <code>{env.host}</code>{env.id !== 'production' ? ' · preview of unreleased features, may be unstable' : ''}
      </div>
    </div>;
  if (state.error) {
    return <>
      {envTabs}
      <div style={{
      padding: '0.75rem 1rem',
      border: '1px solid #f5c6cb',
      background: '#f8d7da',
      color: '#721c24',
      borderRadius: '6px'
    }}>
        <strong>Failed to load {kind} <code>{id}</code>.</strong>
        <div style={{
      fontFamily: 'monospace',
      marginTop: '0.25rem',
      fontSize: '0.9em'
    }}>{state.error}</div>
      </div>
    </>;
  }
  if (!state.data) return <>{envTabs}<div>Loading {kind} details…</div></>;
  const notFound = label => <div style={{
    padding: '0.75rem 1rem',
    border: '1px solid #ffeaa7',
    background: '#fff3cd',
    color: '#856404',
    borderRadius: '6px'
  }}>
      <strong>No {kind} found for id <code>{id}</code>.</strong>
      <div style={{
    marginTop: '0.25rem',
    fontSize: '0.9em'
  }}>
        It may have been renamed or removed. See the <a href={`/composer/${label}`}>{label} catalog</a> for the current list.
      </div>
    </div>;
  const renderSchema = schema => {
    if (!schema) return <div style={{
      opacity: 0.6,
      fontSize: '0.9em'
    }}>No config schema.</div>;
    return <pre style={{
      background: '#0d1117',
      color: '#e6edf3',
      padding: '0.75rem',
      borderRadius: '6px',
      overflow: 'auto',
      fontSize: '0.85em',
      border: '1px solid #30363d'
    }}><code style={{
      color: 'inherit',
      background: 'transparent'
    }}>{JSON.stringify(schema, null, 2)}</code></pre>;
  };
  const renderPortRows = ports => {
    if (!ports || ports.length === 0) return <div style={{
      opacity: 0.6,
      fontSize: '0.9em'
    }}>None.</div>;
    return <table>
      <thead>
        <tr>
          <th className="text-left"><strong>Name</strong></th>
          <th className="text-left"><strong>Kind</strong></th>
          <th className="text-left"><strong>Details</strong></th>
        </tr>
      </thead>
      <tbody>
        {ports.map((port, idx) => {
      const {name, kind: portKind, ...rest} = port;
      const entries = Object.entries(rest);
      return <tr key={`${name ?? 'port'}-${idx}`}>
            <td><code>{name ?? '—'}</code></td>
            <td><code>{portKind ?? '—'}</code></td>
            <td>
              {entries.length === 0 ? <span style={{
        opacity: 0.5
      }}>—</span> : entries.map(([k, v]) => <span key={k} style={{
        display: 'inline-block',
        marginRight: '0.5rem'
      }}>
                      <span style={{
        opacity: 0.7
      }}>{k}:</span> <code>{typeof v === 'string' ? v : JSON.stringify(v)}</code>
                    </span>)}
            </td>
          </tr>;
    })}
      </tbody>
    </table>;
  };
  const renderSelectors = selectors => {
    if (!selectors || selectors.length === 0) return <div style={{
      opacity: 0.6,
      fontSize: '0.9em'
    }}>No selectors.</div>;
    return <table>
      <thead>
        <tr>
          <th className="text-left"><strong>Binding</strong></th>
          <th className="text-left"><strong>Source</strong></th>
          <th className="text-left"><strong>Match</strong></th>
          <th className="text-left"><strong>Selection</strong></th>
        </tr>
      </thead>
      <tbody>
        {selectors.map((sel, idx) => <tr key={`${sel.binding ?? 'sel'}-${idx}`}>
            <td><code>{sel.binding ?? '—'}</code></td>
            <td><code>{sel.source ?? '—'}</code></td>
            <td><code>{sel.match ? JSON.stringify(sel.match) : '—'}</code></td>
            <td><code>{sel.selection ? JSON.stringify(sel.selection) : '—'}</code></td>
          </tr>)}
      </tbody>
    </table>;
  };
  const shortAddress = addr => addr && addr.length > 10 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr;
  const copyAddress = async (address, key) => {
    if (!address) return;
    try {
      if (navigator?.clipboard?.writeText) {
        await navigator.clipboard.writeText(address);
      } else {
        const el = document.createElement('textarea');
        el.value = address;
        el.setAttribute('readonly', '');
        el.style.position = 'absolute';
        el.style.left = '-9999px';
        document.body.appendChild(el);
        el.select();
        document.execCommand('copy');
        document.body.removeChild(el);
      }
      setCopied(key);
      setTimeout(() => setCopied(current => current === key ? null : current), 1200);
    } catch {}
  };
  const addressCell = (address, chainId, rowIdx, side) => {
    const key = `${rowIdx}-${side}`;
    const isCopied = copied === key;
    return <>
      <code role="button" tabIndex={0} title={isCopied ? 'Copied!' : `Click to copy ${address}`} onClick={() => copyAddress(address, key)} onKeyDown={e => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        copyAddress(address, key);
      }
    }} style={{
      cursor: 'pointer',
      background: isCopied ? '#d4edda' : undefined,
      color: isCopied ? '#155724' : undefined,
      textDecoration: 'underline dotted',
      textDecorationColor: '#999',
      borderRadius: '3px',
      padding: isCopied ? '0 2px' : undefined
    }}>
        {isCopied ? 'Copied!' : shortAddress(address)}
      </code>
      <span style={{
      opacity: 0.6
    }}> · chain {chainId}</span>
    </>;
  };
  if (kind === 'edge') {
    const packs = state.data;
    const pack = packs.find(p => p.protocol === id);
    if (!pack) return <>{envTabs}{notFound('routing-edges')}</>;
    const edges = pack.edges ?? [];
    return <>
      {envTabs}
      <div style={{
      marginBottom: '0.5rem',
      fontSize: '0.9em',
      opacity: 0.75
    }}>
        Protocol <code>{pack.protocol}</code> · <code>{edges.length}</code> edges
      </div>
      <h3>Edges</h3>
      {edges.length === 0 ? <div style={{
      opacity: 0.6,
      fontSize: '0.9em'
    }}>No edges registered.</div> : <table>
            <thead>
              <tr>
                <th className="text-left"><strong>Type</strong></th>
                <th className="text-left"><strong>In</strong></th>
                <th className="text-left"><strong>Out</strong></th>
              </tr>
            </thead>
            <tbody>
              {edges.map((edge, idx) => <tr key={`${edge.type}-${edge.in?.address}-${edge.out?.address}-${idx}`}>
                  <td><code>{edge.type}</code></td>
                  <td>{addressCell(edge.in?.address ?? '', edge.in?.chainId ?? '', idx, 'in')}</td>
                  <td>{addressCell(edge.out?.address ?? '', edge.out?.chainId ?? '', idx, 'out')}</td>
                </tr>)}
            </tbody>
          </table>}
    </>;
  }
  const manifest = state.data;
  let item = null;
  if (kind === 'op') item = (manifest.operations ?? []).find(op => op.id === id); else if (kind === 'materialiser') item = (manifest.materialisers ?? []).find(m => m.kind === id); else if (kind === 'guard') item = (manifest.guards ?? []).find(g => g.kind === id);
  if (!item) return <>{envTabs}{notFound(`${kind}s`)}</>;
  return <>
    {envTabs}
    <div style={{
    marginBottom: '0.5rem',
    fontSize: '0.9em',
    opacity: 0.75
  }}>
      Manifest version <code>{manifest.manifestVersion}</code>
      {item.description ? <> · {item.description}</> : null}
    </div>
    <h3>Signature</h3>
    <ul>
      <li><strong>Kind:</strong> <code>{kind}</code></li>
      <li><strong>Id:</strong> <code>{id}</code></li>
      {item.accepts ? <li><strong>Accepts:</strong> <code>{item.accepts}</code></li> : null}
    </ul>
    {kind === 'op' ? <>
      <h3>Input ports</h3>
      {renderPortRows(item.inputs)}
      <h3>Output ports</h3>
      {renderPortRows(item.outputs)}
    </> : null}
    {kind === 'guard' ? <>
      <h3>Selectors</h3>
      {renderSelectors(item.compatibility?.selectors)}
    </> : null}
    <h3>Config schema</h3>
    {renderSchema(item.configSchema)}
  </>;
};

The `core.numericInvariant` guard asserts a numeric comparison against an op's output port at simulation time, turning the observed value into an on-chain invariant. Use it when you need an explicit bound on a produced amount — for example, requiring that a deposit returns at least some minimum number of shares, or that a leftover balance is exactly zero.

Unlike [`slippage`](/composer/composer-api/guards/slippage), which expresses tolerance relative to the expected amount in basis points, `core.numericInvariant` compares against an **absolute** `threshold` you supply. It targets `handle` outputs only.

<ComposeItemDetail kind="guard" id="core.numericInvariant" />

## Config

| Field       | Type                                              | Description                                                                         |
| ----------- | ------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `port`      | string                                            | The op output port (a `handle`) to assert against.                                  |
| `op`        | `'gte' \| 'gt' \| 'lte' \| 'lt' \| 'eq' \| 'neq'` | The comparison operator applied as `value <op> threshold`.                          |
| `threshold` | string                                            | The bound to compare against, as a decimal-integer string (base-unit, no decimals). |

## Example

Require that a zap's `amountOut` port produces at least `1000000` base units:

```ts theme={"system"}
builder.lifi.zap('zap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: {
    resourceOut: resources.erc20(A_ETH_USDC, 1),
  },
  guards: [
    guards.numericInvariant({
      port: 'amountOut',
      op: 'gte',
      threshold: '1000000',
    }),
  ],
});
```

See [Build a Flow](/composer/composer-api/guides/build-a-flow#guards-invariants-during-execution) for attaching guards, interpreting failures, and choosing tolerances.
