# Introduction

Metric is a decentralized exchange for **active pools**: pools with custom pricing and liquidity that proactively change with new information.

**Active pools** split pricing and liquidity into two parts. The [**Price Provider**](/how-pools-work/price-providers) sets prices: a bid, an ask and a **reference price**, from oracles, onchain data or custom rules. The **pool** holds the liquidity in [percentage ranges](/how-pools-work/liquidity-and-bins) around the provider's quote and executes trades. Unlike most AMMs, the pool isn't its own price oracle: trades move price only relative to the reference. When the reference moves, the ranges move with it.

**Curators** choose the Price Provider and the pool's settings. **Liquidity providers** choose which ranges to fund.

Pools can be **public** and accept outside liquidity or run as **private pools** using the curator's own capital.

Pools can be **update-dependent**, relying on price updates pushed onchain, or **update-independent**, deriving quotes from onchain state and logic without price updates.

Where the chain allows it, an update-dependent pool's latest quote update can be placed ahead of the trades that use it, protecting the market maker. See [Freshness and ordering](/how-pools-work/freshness-and-ordering).

## What can Metric pools do?

* **Follow offchain markets.** Use oracle feeds to price tokenized stocks, forex, gold or crypto majors.

With a custom Price Provider or extension, a pool could also:

* **Follow onchain markets.** Use another pool's prices as a reference. The provider must guard against that price being moved.
* **Price yield-bearing assets.** Use a token's conversion rate into its underlying asset as a pricing input.
* **Price custom instruments.** Use custom pricing models for options, prediction-market outcomes or other synthetic assets.
* **Run custom trading strategies.** Grid orders, limit orders, deep OTC orders and volatility-adjusted pricing.

**PropAMMs** and **oracle-guided pools** are the two best-known kinds of active pools.

## What it means for you

| You are a…                          | Start here                                                                                                   |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Trader                              | [Trade](/use-metric/trade)                                                                                   |
| Liquidity provider                  | [Provide liquidity](/use-metric/provide-and-manage-liquidity)                                                |
| Public-pool curator or token issuer | [Curators](/use-metric/provide-and-manage-liquidity/public-pools-and-curation)                               |
| Market maker                        | [Run a private pool](/run-a-pool/getting-started)                                                            |
| Aggregator, solver or app developer | [Integrate liquidity](/integrate/integrate-metric-liquidity)                                                 |
| Pool developer                      | [Build custom pool logic](/run-a-pool/build-a-price-provider) · [Extensions](/run-a-pool/build-an-extension) |

Learn [how Metric works](/start-here/how-metric-works), check [supported chains and contract addresses](/reference/supported-chains-and-deployments) or look up a term in the [Glossary](/reference/glossary).


# How Metric works

An active pool separates pricing logic from liquidity allocation.

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2FcLOr8P1wsJFTy223TJ8l%2Fmetric_active_bin_deviation_v3.png?alt=media&#x26;token=296c18a8-f5d3-4c6f-8c28-a158bd266bbd" alt=""><figcaption></figcaption></figure>

## Pricing

Each pool has a [Price Provider](/how-pools-work/price-providers), a smart contract that supplies a quote containing bid, ask and reference price.

It can follow oracle feeds, use other onchain data, or run custom pricing logic. Most of an active pool's flexibility lives in its provider.

The pool doesn't form its own view of price. It relies on the price provider and applies its own execution logic on top. Halting trading is an accepted safe state for a pool.

## Liquidity and deviation

Liquidity sits in **bins**, each covering a percentage range around the reference. The **active bin and the position within it determine the current deviation**.

A position halfway through a **+1% to +2% bin** means **+1.5% deviation**. At a $100 reference, the pool's current price before the spread is $101.50.

If the reference rises to $105 and nobody trades, the pool's price becomes $106.58 before the spread, still 1.5% above the reference. **The deviation and token quantities stay unchanged.** Unlike in concentrated-liquidity AMMs, LPs don't need to withdraw and redeposit to follow the price.

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2FJ2ONnHfy5EP8cpS11AwW%2Fimage.png?alt=media&#x26;token=2d73b4c4-253e-46d8-9fe2-2c1bc1d8d39d" alt=""><figcaption></figcaption></figure>

## Swaps

A trade fills in the active bin first and continues into the next bin once that one runs out. Buying pushes the pool's deviation up and selling pushes it down, so the larger the trade, the worse its average price. That's **price impact**.

## Extensions

Extensions add rules around swaps, deposits and withdrawals. They can restrict who trades or deposits, reject trades that breach configured limits, block all trades when something goes wrong, or record fills for a pricing strategy. They **can't change a trade's price**: the pool reads the Price Provider's quote before any extension runs, and an extension can only let the trade through or reject it.

See [Risks and safeguards](/use-metric/risks-and-safeguards).

More detail: [Liquidity and bins](/how-pools-work/liquidity-and-bins) · [Price Providers](/how-pools-work/price-providers) · [Extensions](/how-pools-work/extensions-and-permissions).


# Contact us

Metric is happy to talk to anyone who trades, makes markets or builds onchain.

<table data-header-hidden><thead><tr><th width="300"></th><th></th></tr></thead><tbody><tr><td><strong>Traders and LPs</strong></td><td><a href="https://discord.gg/metricxyz">https://discord.gg/metricxyz</a></td></tr><tr><td><strong>Market makers, institutions and builders</strong></td><td><a href="mailto:contact@metric.xyz">contact@metric.xyz</a> or Discord</td></tr><tr><td><strong>Security researchers</strong></td><td><a href="mailto:security@metric.xyz">security@metric.xyz</a></td></tr><tr><td><strong>Everything else</strong></td><td><a href="mailto:contact@metric.xyz">contact@metric.xyz</a></td></tr></tbody></table>

If you're **talented** and **ambitious** and looking for a job, reach us at <contact@metric.xyz>.


# Liquidity and bins

## Definitions

Every Metric pool reads a reference price from its price provider. Liquidity is held in **bins: percentage ranges above and below that reference** (for example +1% to +2%). LPs allocate tokens to these bins.

**Deviation** is how far the pool's current price sits from the reference, before the spread. The reference itself is 0% deviation. In a Metric pool, **deviation** works like the **price in a classic AMM**, except that it is measured relative to the reference.

**Pool price before the spread = reference price × (1 + deviation)**

The position inside the **active bin** gives the exact deviation.

#### Where deviation comes from

At any moment, trading happens in the active bin. The exact deviation depends on how far through that bin the pool currently is:

**Deviation = bin's lower deviation + position fraction × bin width**

For example, halfway through a **+1% to +2% bin** is **+1.5%**, not +0.5%.

## Trading discovers the deviation

Trading moves the position inside the bin and discovers its deviation from the reference in the [provider's quote](/how-pools-work/price-providers).

At a given reference, buying the priced asset moves deviation up; selling moves it down. Trades move through bins as liquidity is consumed. More liquidity means less movement for the same trade size.

#### A new reference, the same deviation

At **+1.5% deviation**, a $100 reference gives a $101.50 pool price. If only the reference changes to $110, that price becomes $111.65. **Nobody has to trade for that to happen.** The active bin, the position within it, token balances and LP ownership all stay unchange&#x64;**.**

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2F0Ddzpm6NC0sGDZEQSKhs%2Fmetric_bin_position_deviation.png?alt=media&#x26;token=993d8d22-781c-4722-be38-ab61798a6eeb" alt=""><figcaption></figcaption></figure>

## Execution price

The provider's quote is an input. The pool price before the spread is the price at the current position, not the average price a trade gets. A trade moves deviation as it fills, so its average price depends on trade size, liquidity across the bins it crosses, the spread, and any per-bin or notional fee, all applied on top of the provider's reference. Inside a bin, the price moves linearly with the output the bin pays, so the start of a bin fills better than its average. See [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth).

LPs own shares of the bins they fund and earn a share of the spread only where their liquidity is used.

[Provide and manage liquidity](/use-metric/provide-and-manage-liquidity)


# Price Providers

Each pool uses one **Price Provider**: a contract that returns a quote containing bid, ask and reference price.

Pools of the same pair can share one provider contract while keeping separate liquidity.

By design, a Price Provider is small and disposable: quick to write and cheap to deploy. When a strategy changes, deploy a new provider rather than patching the old one.

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2FhcSK7cfDrAim6zC6K21z%2Fmetric_provider_and_bins_into_pool.png?alt=media&#x26;token=727d8c72-1a25-417b-8994-ea1818b5bb31" alt=""><figcaption></figcaption></figure>

## Three pricing inputs

Bid and ask are from the **pool's perspective**, for the asset being priced.

| Input           | Role                                                      |
| --------------- | --------------------------------------------------------- |
| Bid             | Price at which the pool buys the asset                    |
| Ask             | Price at which the pool sells the asset                   |
| Reference price | Sets 0% deviation; fees and markouts are measured from it |

The reference must lie between bid and ask. Widening a side increases the spread LPs earn on that side, and both sides can widen together without limit. What the pool's configuration bounds is the skew: how much further one side may sit from the reference than the other. Metric can also turn this check off for a pool. A quote that breaks these rules reverts, and the pool doesn't trade.

See [Liquidity and bins](/how-pools-work/liquidity-and-bins#execution-price) for how the pool uses the provider's quote.

#### If the provider can't quote

If the provider reverts or reports no valid price, the pool rejects every swap until a valid quote returns, so nothing trades at a missing or invalid price. Deposits and withdrawals still work, subject to the pool's extensions. Providers use this on purpose, for example to halt during a feed outage or outside trading hours.

## Different markets, different rules

A provider can:

* Follow an oracle feed or another onchain market.
* Adjust spreads using price uncertainty, a stablecoin's distance from its peg or pool balances.
* Discover prices using trading activity and inventory.
* Use a market maker's proprietary quotes.
* Set its own trading hours. A private pool can price a stock whenever its curator chooses, including while the stock's market is closed.

**An external oracle is optional.** Offchain models can use Metric's [shared oracle infrastructure](/how-pools-work/oracles) to publish data; the pool still reads prices from its provider contract.

## Why the reference matters

A quote can pass the pool's checks and still trade at a bad price, losing LP funds. A misleading reference can also inflate reported markouts without improving the value of the assets held.

Always report a correct reference price; see [Build a Price Provider](/run-a-pool/build-a-price-provider#reference-price).

[Anchored pricing](/how-pools-work/anchored-pricing#anchored-pricing) uses an independent feed to constrain custom quotes. Unanchored providers rely on their own logic, inputs and permissions.

For replacement and configuration permissions, see [Risks and safeguards](/use-metric/risks-and-safeguards).

[Oracles](/how-pools-work/oracles) · [Build a Price Provider](/run-a-pool/build-a-price-provider)


# Oracles

Metric provides **shared oracle infrastructure** on each chain for Chainlink, Pyth and gas optimized propAMM updates.

Pool curators can reuse it instead of deploying and maintaining their own oracle systems.

{% hint style="info" %}
**Oracle and contract addresses:** see [Contract addresses](/reference/supported-chains-and-deployments#oracle-and-price-provider-layer).
{% endhint %}

All of them share an interface for **price, spreads and timestamp**. A [Price Provider](/how-pools-work/price-providers) reads these to build its bid and ask.

## Chainlink and Pyth: a commodity for every Metric pool

Metric integrates **Chainlink Ultra** and **Pyth Pro** through shared contracts. Each verified feed exists once per chain, and one update serves every Metric pool that uses it.

Anyone can submit a newer signed update. The contract checks the **Chainlink or Pyth** signature, so whoever relays the update can't change the price. A pool priced from one of these feeds trusts Chainlink or Pyth, not the relayer.

Only Metric pools and approved integrators can read these feeds.

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2F5xo92jwf6xrDJZ9HgVMs%2Fmetric_shared_feed_many_pools.png?alt=media&#x26;token=b1688da9-f5ef-4b01-b02e-b59919641e23" alt=""><figcaption></figcaption></figure>

## Multiplier Oracle: tokenized stocks

A tokenized stock doesn't always track one share. Its issuer keeps a **multiplier**: the number of shares one token redeems for. It moves on corporate actions: a reinvested dividend nudges it up, and an N:1 split multiplies it by N. Pyth and Chainlink price the share, not the token, so a pool reading them directly drifts with every dividend and is off by a factor of N after every split.

The **Multiplier Oracle** prices these tokens as the **Pyth share price × the issuer's multiplier**. It reads the multiplier from the token through an adapter for each token standard:

| Chain           | Tokenized stock                  | Adapter                     |
| --------------- | -------------------------------- | --------------------------- |
| Base            | B20 Asset                        | `B20AssetParams`            |
| Robinhood Chain | Robinhood Stock Token (ERC-8056) | `RobinhoodStockTokenParams` |

Each Multiplier Oracle feed binds one Pyth equity feed to one token and one adapter, and the binding can't change once created. A feed stalls while the issuer's pause flag is set, for example around a split, and resumes by itself when the flag clears. While it is stalled, pools halt swaps; withdrawals stay open.

Anyone can create a feed. The contract only checks that the adapter is approved, so a feed can be bound to the wrong token or the wrong Pyth feed. Metric checks every binding when it classifies a pool; see [Pool tiers](/use-metric/pool-tiers).

## Compressed Oracle: built for propAMMs

Market makers publish their own prices through a **shared Compressed Oracle contract**.

The Compressed Oracle is gas-optimized: up to **four feed values** fit in one 32-byte storage word, at about 8,000 gas per feed.

Multiple feeds can be batched in one transaction. Packed data and fewer storage writes reduce the gas cost of frequent updates.

A feed is controlled by its creator or an updater they authorize. Its price depends on that publisher, not on any other independent/verifiable sources.

## Freshness and access

Each Price Provider has a staleness limit, capped at creation at up to 7 days. Some providers let the curator change it later, but never past that cap. Until the limit is reached, the pool keeps trading on the latest stored update. Once the update is older than the limit, or the feed reports itself stalled, the provider halts and the swap reverts. It never trades on a price older than the limit.

Providers also halt when a feed reports too much price uncertainty, when the price leaves the feed's configured minimum and maximum, or when the computed bid/ask range collapses.

Anyone can read compressed feeds, onchain or offchain. Chainlink and Pyth feeds can be read onchain only by a registered pool during a swap, and by approved integrators. Offchain reads work for everyone, so wallets, explorers and the Metric API can always show the price. **Permissionless updates do not mean unrestricted reads.**

[Contact us](/start-here/contact-us) for shared oracle access if you believe you can contribute to the ecosystem.

External oracles remain optional for custom pricing strategies. Available oracle sources vary by chain; see [Contract addresses](/reference/supported-chains-and-deployments).

[Anchored pricing](/how-pools-work/anchored-pricing) · [Operate a pool](/run-a-pool/price-updates-and-monitoring)


# Extensions

Extensions add rules before or after pool actions: they can control access, check a trade's effect on liquidity, record trades for future pricing, block all trading and more.

Curators can reuse existing extensions or build their own.

**Extensions can't change the pool's quote; they can only reject a trade.** A custom extension therefore can't manipulate the quote of the current trade.

## Where extensions run

| Pool action | Extension points                   |
| ----------- | ---------------------------------- |
| Swap        | Before or after the swap           |
| Deposit     | Before or after adding liquidity   |
| Withdrawal  | Before or after removing liquidity |

For swaps for example, quote validation comes **before** the before-swap extensions; extensions cannot replace that quote for the current swap. After-swap extensions can inspect token amounts and pool state.

A rejection at either stage reverts the pool action, including token transfers.

<figure><img src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2FLxPBKG9Yt5rZ0zu6suvf%2Fmetric_swap_lifecycle_extensions.png?alt=media&#x26;token=a720ccc3-577b-405d-9f77-69249d3bd77e" alt=""><figcaption></figcaption></figure>

## Standard extensions

| Extension         | What it controls                                                                                              |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| StopLoss          | Rejects swaps when a bin's value per LP share, in tokens rather than USD, drops too far within a time window. |
| Velocity Guard    | Rejects swaps when the reference price moves too fast.                                                        |
| Deposit allowlist | Limits who can deposit into a pool.                                                                           |

## Settings matter

**StopLoss** values each bin at the Price Provider's bid and ask and tracks its peak: the highest value per LP share it has recorded. The drawdown setting decides how far below the peak a swap may take the bin; decay lowers the peak over time, so an old peak doesn't block the bin forever.

If a bin has no baseline, its first monitored trade initializes one without this drawdown check.

StopLoss rejects trades; it does not sell an LP's position or cap all losses. Ordinary market movements can trigger it.

**Velocity Guard** checks changes in the supplied reference, not ordinary price impact from crossing bins. Its allowance and comparison anchor can be changed by the pool admin.

Attaching a guard does not prove it is enabled. Check its settings and who can change them.

## Fixed addresses, changeable behavior

Extension addresses and execution order are fixed at pool creation. Settings or code may remain changeable unless restricted.

A custom withdrawal extension can block LP exits. Before depositing, review its code and who can change it.

[Risks and safeguards](/use-metric/risks-and-safeguards) · [Pool tiers](/use-metric/pool-tiers) · [Build an extension](/run-a-pool/build-an-extension)


# Anchored standard

The **Anchored standard** is how Metric protects LPs in public pools, even from a malicious pool curator. It combines two parts:

* **Anchored pricing** keeps every quote within limits set by an independent oracle.
* The **StopLoss** extension limits how fast a bin can lose value.

Pools that meet it can qualify for 🟢 Best Practices or 🟡 Neutral; see [Pool tiers](/use-metric/pool-tiers).

## How it protects

Anchoring limits how far a pool's quotes can differ from a trusted source. That limits the loss a single misquoted transaction can cause.

To keep losses from adding up too fast, the Anchored standard includes the **StopLoss** extension, which limits LP losses per bin per day. StopLoss values one LP share of each bin in the pool's tokens, not in USD, using the Price Provider's bid and ask, and compares that with the highest value it has recorded. In an anchored pool the oracle bounds that bid and ask, so an curator can't hide a loss from StopLoss.

Even with a malicious pool curator, StopLoss slows losses enough that LPs have time to react and withdraw, provided everything is initialized correctly.

StopLoss rejects trades. It doesn't sell positions or cap all losses, and it protects only when attached and initialized. See [Extensions](/how-pools-work/extensions-and-permissions).

## Anchored pricing

An Anchored Price Provider (APP) takes its reference price from independent Chainlink or Pyth data and uses it to constrain bid and ask. For a tokenized stock with an issuer multiplier, the reference comes from the [Multiplier Oracle](/how-pools-work/oracles#multiplier-oracle-tokenized-stocks): the Pyth share price scaled by that multiplier.

It sets a maximum bid and a minimum ask. The provider can quote a bid lower or an ask higher, but cannot cross those limits. The pool's own [skew check](/how-pools-work/price-providers#three-pricing-inputs) still applies, so widening only one side too far reverts.

### Two ways to use it

* **Custom source:** a strategy supplies candidate bid/ask quotes and its own reference price, which must sit between them or the provider halts; the anchored provider applies the reference derived limits.
* **Source of truth:** bid and ask are derived from the oracle feed itself: the oracle's own reported uncertainty, scaled by a configured multiplier, plus a fixed minimum margin.

In both cases, the reference price comes from the anchor feed, not the custom strategy.

An APP can also derive a cross-pair reference from two feeds in the same oracle: **gold/USD ÷ EUR/USD = gold/EUR**. A separate gold/EUR feed is not required.

If its feed stops updating, an anchored pool keeps trading on the last price until that price is older than the provider's staleness limit, then stops. The limit is set per provider and can be up to 7 days; 🟢 pools must use 15 seconds or less. For tokenized stocks and other assets whose market closes, a Chainlink market-closed update stops the pool as soon as it lands. The pool resumes when the feed resumes.

### Custom source flexibility

With a custom source, the provider's owner can change the pricing logic at any time without delay. That doesn't change the feed the pool is anchored to or its Price Provider. This is a strength of the APP: any market maker's pricing logic works, as long as it is anchored.

### Example

Suppose the reference is $100, with a buying limit of $99 and a selling limit of $101.

| Custom strategy proposes       | Anchored buying quote | Anchored selling quote |
| ------------------------------ | --------------------- | ---------------------- |
| Buy at $99.50; sell at $100.50 | $99                   | $101                   |
| Buy at $95; sell at $105       | $95                   | $105                   |

These are illustrative limits, not default settings. For execution pricing, see [Liquidity and bins](/how-pools-work/liquidity-and-bins#execution-price).

## Limits of protection

The Anchored standard limits losses; it doesn't prevent them, and LPs in an anchored pool can still lose capital.

Anchoring does not guarantee competitive quotes or an accurate, up-to-date reference. The anchor feed can lag or drift from the fair market price.

The protection depends on the correct feed, its freshness and the provider's configuration. Anchoring does not replace [pool safeguards](/use-metric/risks-and-safeguards) or control over [extension permissions](/how-pools-work/extensions-and-permissions).


# Freshness and ordering

How a trade meets the maker's latest price update on each chain.

A trade can **pick off** an outdated quote before the maker's new quote reaches the oracle.

**Freshness limits** reject old feed data. **Ordering** decides whether the new update happens before the trade that uses it.

Update-independent pools don't need the update pipeline, though ordering can still affect their onchain sources.

## Staleness bound

Off-the-shelf Price Providers revert when a required update is older than their limit, which halts trading until a fresh update lands. Custom providers set their own checks.

A freshness limit bounds how old the feed data behind a quote can be. See [Pool tiers](/use-metric/pool-tiers) for required limits.

**Keeping prices updated is the pool curator's job**, which in a public pool means the curator: running the updater, or making sure a shared one keeps the feed fresh. Metric doesn't guarantee that updates are published or delivered on time, even when they're sent through Metric.

## How each chain orders an update against a trade

Metric works closely with block builders and the foundations of every chain it operates on. The builder commitments below are the builders' own service policies, not onchain rules.

| Regime                       | Where                                                                   | How it works                                                                                                                                                                                                                          | What you can rely on                                                                                                                    |
| ---------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Conditional updates**      | Ethereum, participating builders                                        | Market makers stream updates to Metric's endpoint, which forwards them to participating builders. A participating builder includes your update only in a block that contains a trade against your pool. Priority tips change nothing. | In blocks it builds, the builder commits to placing the latest update it has received from you before the trade.                        |
| **Protected execution lane** | BSC, participating builders                                             | You submit updates to Metric's endpoint, which forwards them to 48Club and, when the update carries a `maxTimestamp`, to BlockRazor.                                                                                                  | In blocks these builders build, they commit to placing your update before trades against your pool.                                     |
| **Builder auction**          | Ethereum outside participating builders; BSC outside the protected lane | Builders order transactions for block profit.                                                                                                                                                                                         | No ordering commitment. An ordinary update may land before the trade or after it, and a bundle that pays the builder more can go first. |
| **Arrival order**            | Robinhood Chain                                                         | The sequencer orders by arrival, not priority fee.                                                                                                                                                                                    | Nothing beyond arrival time.                                                                                                            |

Chain setup: [Ethereum](/run-a-pool/chain-specific-guides/ethereum) · [BSC](/run-a-pool/chain-specific-guides/bsc) · [Robinhood](/run-a-pool/chain-specific-guides/robinhood). Where Metric is live: [Contract addresses](/reference/supported-chains-and-deployments). Pipeline engineering: [Best practices](/run-a-pool/chain-specific-guides).

## What ordering doesn't solve

* **Your latency.** At best, ordering puts your latest update first; it can't make it newer. A taker who knows something your latest update doesn't reflect can still trade against it.
* **Trades without an ordering commitment.** A trade in another builder's block or on an arrival-order chain meets whichever update is onchain when it executes. It is priced from that update if the update is within the staleness limits, and reverts if it isn't.
* Metric's oracles use **monotonic timestamps**: an older update cannot overwrite a newer one.

## For integrators

For conditional updates:\
\
For takers, quote from Metric's API, which serves the latest streamed quotes; onchain reads are unreliable because of lag.\
\
For makers, submit quote updates through Metric's API, which forwards them to participating builders, or directly to those builders.

Metric also follows Titan's [propAMM interface](https://docs.titanbuilder.xyz/propamms/takers), so integrations built on it reach Metric pools as well. See [Integrate Metric liquidity](/integrate/integrate-metric-liquidity).


# Trade

1. Open [Metric](https://app.metric.xyz), connect your wallet and choose a network.
2. Select the tokens and amount.
3. Compare Metric and aggregator routes. Review the quoted output, price impact and slippage tolerance.
4. Approve token spending if requested, then confirm the swap in your wallet.

Keep enough of the network's gas token to pay for gas.

## Understand the quote

**Zero fees.** Your quote is the full price. You pay the spread built into it, which is shared by all other parties. Nothing is added on top: no trading fee, no frontend fee. The optional notional fee, off by default, is built into the quote too. See [Fees](/use-metric/fees).

**Price impact** comes from using liquidity at progressively less favorable prices. Large trades can cross several bins, which affects the price.

**Slippage tolerance** limits how far below the quoted output your execution may fall.

Pools with **freshness checks** reject swaps when their price data is too old. Other safeguards can also reject a swap; see [Risks and safeguards](/use-metric/risks-and-safeguards).

## Other ways to trade

Integrated aggregators can access Metric liquidity alongside other venues; however, choosing an aggregator does not guarantee a Metric fill.

## Programmatic trading

You can trade against Metric pools in two ways: call a pool directly, or go through the SimpleRouter.

Calling a pool directly. Pass the amount you want to trade and a price limit. The swap runs until it hits your price limit or the pool runs out of liquidity, whichever comes first. **If either happens before your full amount is used, the swap still succeeds but fills only part of your amount.** Check the returned amounts, not just whether the transaction succeeded. Set the price limit to the worst price you're willing to accept. When you call a pool directly, your contract pays the pool through the swap callback.

Using the SimpleRouter. Every router swap function takes a deadline, and the swap reverts after that time. The router only works with pools registered with the Metric factory. **It doesn't check that your route connects.** If the tokens, pools and swap directions don't line up, the swap can revert, deliver a different token than you meant, or leave tokens in the router, where anyone can take them. Take pool and token addresses from the [metadata endpoint](/reference/api/metadata).

See [Integrate Metric liquidity](/integrate/integrate-metric-liquidity) for routing and contract details.


# Fees

**Traders pay zero fees on Metric.** Every cost of a trade is already built into the price you're quoted. Nothing is added at execution: no trading fee and no frontend fee.

The quoted price can include up to three components:

| Component          | Set by                                      | Always applies?             |
| ------------------ | ------------------------------------------- | --------------------------- |
| Half spread        | Price provider (chosen by the pool curator) | Yes                         |
| Additional bin fee | Pool curator                                | Only on bins where it's set |
| Notional fee       | Metric                                      | No, off by default          |

## Half spread

Each pool's price provider publishes three prices: a **bid**, an **ask** and a **reference price** between them. You pay the gap between the reference price and the side you trade on:

* **Buying:** the difference between the reference price and the ask.
* **Selling:** the difference between the reference price and the bid.

The spread moves as the provider updates its quote. In anchored pools, the oracle sets the minimum half spread, and the curator can quote wider but not narrower.

## Additional bin fee

Pool liquidity sits in price ranges called **bins**. A curator can add an extra fee to any bin, with separate rates for buying and selling. If your trade crosses several bins, each bin's rate applies only to the part of the trade filled in that bin. Curators use this to make some price ranges cheaper or more expensive to trade through.

## Notional fee

A notional fee is a fixed percentage of trade size. It's **off by default**. Metric can turn it on for specific pools, up to a maximum of 1%. Curators can't set a notional fee for now.

## Who earns what

**Half spread and additional bin fees** are split between LPs, the pool curator (called **admin** in the contracts) and Metric. The curator and Metric take their configured shares; LPs keep the rest.

**Notional fees** go to the protocol, not LPs.

* Metric's default share is 7.5% of the overall spread.
* Raising the **curator's share** reduces LPs' markout without changing the trader's quote.
* The curator and Metric can each change their share of the spread at any time and with no timelock, even when the Price Provider is immutable. Check the pool's current rates before and after depositing.
* Raising the **spread, bin fee or notional fee** increases the cost of a trade for traders.

## How fees are paid out

* **LPs:** Earnings stay in the bins and compound into your position. There's nothing to claim.
* **Pool curator and Metric:** Their shares build up in the pool until someone collects them. Anyone can trigger collection. The curator's share goes to the curator's fee address and Metric's share goes to Metric's fee receiver.
* **Rate changes:** When fee rates or shares change, what has already been earned is split at the old rates before the new rates apply.

## Before you trade or deposit

* **Traders:** Check the quote. It's the full price.
* **LPs:** Check the pool's current fee shares and bin fees before depositing.

Related: [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns).


# Pool tiers

A pool's tier describes its safeguards and permissions for public LPs, not its profitability or execution risk. It is recalculated from the pool's onchain configuration and Metric's curator and feed records, and can change.

{% hint style="warning" %}
**Verified doesn't mean endorsed.** Neither verification nor the 🟢 Best Practices tier is an endorsement or warranty of any curator or strategy.
{% endhint %}

## What the tier means

Anchoring constrains custom quotes against an oracle. A verified curator identifies who runs the pool but does not impose the same price constraint.

High Risk does not prove malicious behavior. It means the pool does not meet the qualifying configurations. A malicious or compromised pricing strategy can drain LP funds; **no tier guarantees safety**. 🟢 Best Practices and 🟡 Neutral pools have safeguards that restrict specific actions and limit the risk.

Pools in the same tier can carry very different risks. In 🟢 and 🟡 pools, StopLoss rejects any single swap taking a bin more than 15% below its peak, except the bin's first trade. In a 🔴 pool, one transaction may drain it.

Before depositing, check the feed, safeguard settings and remaining admin permissions. See [Risks and safeguards](/use-metric/risks-and-safeguards).

| Badge             | Baseline check       | Pricing                                                               | Curator  |     Extensions     |
| ----------------- | -------------------- | --------------------------------------------------------------------- | -------- | :----------------: |
| 🟢 Best Practices | :white\_check\_mark: | [Anchored pricing](/how-pools-work/anchored-pricing#anchored-pricing) | Verified | Immutable StopLoss |
| 🟡 Neutral        | :white\_check\_mark: | [Anchored pricing](/how-pools-work/anchored-pricing#anchored-pricing) | Any      | Immutable StopLoss |
| 🟡 Neutral        | :white\_check\_mark: | Any Price Provider                                                    | Verified | Immutable StopLoss |
| 🔴 High Risk      | :x:                  | Any other configuration, or a failed baseline check                   | Any      |         Any        |

A single failed baseline check makes the pool High Risk, however well the rest is configured.

If an anchored pool's price feeds don't match its tokens, the pool drops one tier: Best Practices becomes Neutral and Neutral becomes High Risk. A token whose feed mapping Metric hasn't confirmed yet counts as a mismatch.

Pools with a deposit allowlist are shown as 🟣 Private instead of receiving a tier. A pool Metric has excluded is hidden from pool lists and search; it only appears, as Blacklisted, in the positions of LPs already in it. A pool that hasn't been classified yet, for example because it was just created, is shown as Unknown.

### Tokenized stocks

B20 Assets on Base and Robinhood Stock Tokens on Robinhood Chain carry an issuer multiplier, so they must be priced through the [Multiplier Oracle](/how-pools-work/oracles#multiplier-oracle-tokenized-stocks). A correctly wired Multiplier Oracle pool is tiered like any other anchored pool and can reach 🟢 Best Practices.

A pool is 🔴 High Risk, however well the rest is configured, when:

* it prices one of these tokens from Pyth, Chainlink or a compressed feed directly, which ignores the multiplier, or
* one of its Multiplier Oracle feeds is miswired: bound to a token other than the one it prices, over a feed that isn't a share price, on a token that isn't one of these tokenized stocks, through an adapter that can't read the token's multiplier, or pricing one of these tokens without its multiplier.

A Multiplier Oracle feed Metric can't check yet, for example one that hasn't been created or whose share price feed isn't in Metric's catalog, counts as a mismatch and drops the pool one tier.

## Definitions

**Anchored pricing** means the pool uses an [Anchored Price Provider](/how-pools-work/anchored-pricing#anchored-pricing). Its parameters must be immutable, and its price feeds must be no more than 15 seconds stale. The provider clamps the curator's quotes against an oracle price, so the curator can quote wider than that price but never through it.

**Verified curator** means the current pool admin has passed KYB, is in Metric's registry of trusted or known admins and is the pool creator. Verification identifies the curator; it doesn't vouch for the strategy. The admin role can't be renounced, only transferred, and a transfer to anyone other than the creator loses verification. The app shows an unverified admin as an operator. See [Curators](/use-metric/provide-and-manage-liquidity/public-pools-and-curation) for verification and onboarding.

**Withdrawal extension** means any extension registered on `beforeRemoveLiquidity` or `afterRemoveLiquidity`, including Metric's standard ones. It can block LP exits, so it makes a pool High Risk. Adding extensions on swaps or deposits doesn't lower the tier.

**Baseline checks** means the pool must have:

* An **immutable** Price Provider address: the provider can never be replaced.
* A maximum bin width of **1%**.
* An attached and initialized **StopLoss** with **drawdown ≤ 15%** and **daily decay ≤ 15%**, whose settings can never change. That requires a timelock of `type(uint40).max`, which makes every proposed change revert, and no change proposed earlier still queued. Any shorter timelock, however long, only delays a change and fails this check.
* **No withdrawal extensions**.


# Risks and safeguards

{% hint style="danger" %}
**Providing liquidity on Metric is high risk. You can lose your entire deposit.** Read this page and check the pool's [tier](/use-metric/pool-tiers) before you deposit.
{% endhint %}

A malicious or compromised pool curator with unrestricted pricing can drain a pool by selling its assets too cheaply or buying them at inflated prices.

Safeguards and permissions differ across pools. Check what actually constrains the curator before depositing.

## What to check before depositing

* **Tier:** check which safeguards and permissions the tier has.
* **Pricing:** inspect the Price Provider's logic, sources and update requirements.
* **Curator:** identify who curates and controls the pool.
* **Control:** check which pricing settings can change, who can change them, and whether the provider can be replaced or upgraded.
* **Extensions:** custom withdrawal extensions can block your exit, and token restrictions can prevent transfers. Whitelisted extensions provide known controls, but their settings matter.
* **Safeguards:** check that guards are enabled, initialized and configured, not just attached. Review their limits and who can change them. [Pool tiers](/use-metric/pool-tiers) lists qualifying configurations, not guarantees of safety.

## Safeguards and their limits

| Risk                                | Protection                                                                                          | Limitations                                                                                                                                                                                                                                  |
| ----------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bad or malicious quotes             | **Anchored pricing** limits the provider's buying and selling quotes against an independent oracle. | Protection is only as good as the oracle: it depends on the feed's accuracy and freshness.                                                                                                                                                   |
| Stale price data                    | **Price freshness checks** reject trades when a required feed exceeds its allowed age.              | Custom providers define their own checks.                                                                                                                                                                                                    |
| Per-bin drawdown                    | **StopLoss** rejects swaps that breach the configured drop in a bin's value per LP share.           | A bin's first monitored StopLoss trade establishes its baseline and is not drawdown-protected. The peak it compares against decays over time, so losses can keep adding up.                                                                  |
| Reference jumps                     | **Velocity Guard** rejects swaps when the reference-price change exceeds the configured allowance.  | Reference moves within the allowance pass, so a loose allowance lets large jumps through, and the reference can drift any distance in small steps. The guard trusts the pool admin, who can change the allowance or turn it off at any time. |
| Provider replaced after you deposit | An **immutable Price Provider** is fixed when the pool is created and can't be replaced.            | A review of the provider's code stays valid, but what it reads can change: upgradeable feeds or admin-set parameters can still move its prices. Review those too, and recheck them while you're deposited.                                   |

The safeguards reduce these risks but can't eliminate them, and none of them caps how much you can lose.

Mechanisms and settings: [Anchored standard](/how-pools-work/anchored-pricing) · [Extensions](/how-pools-work/extensions-and-permissions).

## Market and technical risks

* **Inventory exposure.** Trades change your token ratio. A reference-price update moves quoted ranges but does not rebalance your holdings. Asset price losses can outweigh markout.
* **Execution across bins.** Trades can cross multiple bins. The &#x31;**% maximum bin width** used for tiers is not a cap on total slippage or LP losses.
* **Contracts and infrastructure.** Contract bugs, oracle failures, chain disruption and underlying token risks can cause losses or prevent access to funds.
* **Pick-offs.** A trade that executes before the maker's newest oracle update uses old data. When that quote is better for the taker, the difference comes out of the pool and shows up as a negative markout. Preventing this is the curator's job; see [Best practices](/run-a-pool/chain-specific-guides).


# Provide and manage liquidity

{% hint style="danger" %}
**Providing liquidity on Metric is high risk. You can lose your entire deposit.** Read [Risks and safeguards](/use-metric/risks-and-safeguards) before you deposit.
{% endhint %}

Your liquidity sits in bins: price ranges relative to the pool's reference price. When the reference moves, your ranges move with it, so there is no range to manage and no rebalancing script.

You earn a share of the spread traders pay **when trades use your bins' liquidity**, after the curator's and Metric's fees. Earnings compound into your position, with nothing to claim. Unused liquidity earns nothing, even if other bins in the same pool are active.

## Choose a pool

Start with the pool's [tier](/use-metric/pool-tiers), then check:

* **Pricing:** What sets the quotes, and is it anchored to a trusted oracle?
* **Control:** Who is the curator, are they verified, and what can they still change: the provider, its settings, fees, safeguards?
* **Logic:** Is the Price Provider's logic public and simple, or undisclosed?
* **Extensions:** Can a custom withdrawal extension block your withdrawal? Do you understand what each swap extension does?
* **Earnings:** What share of the spread do the curator and Metric take as fees, and is the pool's markout positive for LPs? See [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns).

See [Risks and safeguards](/use-metric/risks-and-safeguards).

## Deposit

1. Open the pool in [Metric](https://app.metric.xyz) and connect your wallet on the correct network.
2. Select the bins where you want to provide liquidity.
3. Enter the amounts and review your allocation.
4. Approve token spending if requested, then confirm the deposit.

You can choose any available bins. Some allocations require **one token**; others require **both**, depending on the bins' current composition.

{% hint style="info" %}

#### Example: choosing bins

An ETH/USDC pool, reference $2,000, active bin just below the reference. You select bins from −1% to +1%.

* **What the deposit asks for.** Bins above the active price hold ETH (they sell ETH to buyers). Bins below hold USDC (they buy ETH from sellers). The active bin holds both. Your allocation decides which token, or both, the deposit requires.
* **The reference suddenly moves to $2,100.** Every bin reprices to its new percentage range. Nothing is bought or sold: you keep the same tokens, and your ETH is worth more. You earn nothing from the move itself. A drop works the same way, except your ETH is worth less.
* **Traders buy ETH.** The active position climbs through your upper bins. In each bin your ETH is sold at that bin's price plus the spread, and what you earn stays in that bin. Bins the trade never reached earn nothing.
* **You withdraw.** You receive less ETH and more USDC than you deposited, plus your share of what those bins earned. Whether that leaves you ahead of simply holding depends on where ETH goes next; see the [examples](/use-metric/provide-and-manage-liquidity/understanding-your-returns#trading-pnl).
  {% endhint %}

## Manage or withdraw

Open your position, choose how much liquidity to remove, review the tokens you will receive, then confirm in your wallet.

Your earnings stay in the bins and compound into your position; there's nothing to claim. Trades change your token ratio, so you may withdraw a different ratio of tokens than you deposited.

Moving ranges does not remove market exposure. See [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns).

Related: [Fees](/use-metric/fees).


# Understanding your returns

There are two metrics we track in our UI.

* **Markout:** how much your bins made or lost on each swap, measured against the pool's reference price a few minutes after the trade. Positive means you traded at a good price; negative means traders got ahead of the market at your expense.
* **Trading PnL:** how much more or less you have than if you had simply held the tokens you deposited, with both valued at today's prices. The same trades drive both.

## Markout

Markout values each trade a fixed time after it executes, at the pool's reference price at that moment. For every swap that crossed your bins, it takes the tokens the swap moved in and out of them and values them at the reference price that long after the trade. The time is the **horizon**: 1, 5 or 30 minutes. The app shows 5 minutes unless you pick another.

$$
\text{Markout}*h = \sum*{\text{swaps}} \left( \Delta\text{token0} \times \text{ref}\_{,t+h} + \Delta\text{token1} \right) \times \text{price}\_1^{,t+h}
$$

where $$t$$ is when the swap executed, $$h$$ the horizon, $$\text{ref}$$ the pool's reference price in token1 per token0, and $$\text{price}\_1$$ token1's USD price.

**Example.** You deposit 10 ETH and 20,000 USDC, worth $40,000 with ETH at $2,000. Traders buy 5 ETH from your bins at $2,010 while the reference is $2,000: your bins give up 5 ETH and receive 10,050 USDC, leaving you 5 ETH and 30,050 USDC. The extra $50 is the spread. Five minutes later:

| Reference after 5 minutes | Markout 5m | What happened                                                  |
| ------------------------- | ---------- | -------------------------------------------------------------- |
| $2,000 (unchanged)        | +$50       | You kept the spread.                                           |
| $2,006                    | +$20       | The price moved the trader's way, but by less than the spread. |
| $2,020                    | −$50       | The trader bought ahead of a move: you were picked off.        |

With USDC as token1, the last row is $$-5 \times 2{,}020 + 10{,}050 = -50$$.

So markout counts the spread you earned and also whether the trades were worth taking. Fees the curator and Metric take never reach your bins, so markout is already after their shares; see [Fees](/use-metric/fees).

Markout isn't realized profit. Nothing is sold at the later reference price: markout values each trade as if your bins had closed it out then. Added up over a pool's past trades, it estimates what that trading flow is worth to LPs, which makes it a guide to what providing liquidity there might earn, not a promise that it will.

{% hint style="warning" %}
**Markout is only as reliable as the pool's price source.** It's measured against the pool's own reference price. If that price is wrong, every markout figure is wrong with it: a positive markout can hide losses to traders who know the real price.
{% endhint %}

Markout APR divides markout by the liquidity that earned it and annualizes it. The APR on the pools list is the pool's **Markout APR** over the period and at the horizon chosen in Settings: 24 hours, 7 days, 30 days or all time, at 1, 5 or 30 minutes. The default is 7 days at 5 minutes. It divides by the liquidity of the bins that traded in the period, so liquidity parked in bins no swap reached doesn't dilute it (the API's `markoutByWindow.<period>.byHorizon.<horizon>.aprBps`). On a pool's My performance tab, Markout and its APR cover your position over the selected period, divided by your position value. These are past figures, not a forecast: future trades can be worth more or less to LPs. They also leave out price moves after the horizon, so they aren't your total return.

A swap is counted once the pool has a reference price and token prices for its horizon. A figure shows as unavailable when less than 70% of the period's volume can be valued. The pools-list APR is also unavailable when no bin traded at all (markout is then $0). It has no minimum liquidity, so a pool where only a small bin traded can show a very large APR.

## Trading PnL

Every trade changes your token mix. When traders buy ETH from your bins, you're left with less ETH and more USDC. Trading PnL is the value of what you hold now minus the value of what you deposited, both at today's prices:

$$
\text{Trading PnL} = \Delta\text{token0} \times \text{price}\_0^{,\text{now}} + \Delta\text{token1} \times \text{price}\_1^{,\text{now}}
$$

where $$\Delta$$ is how much of each token trades added to or took from your bins, including the spread they leave there. It can be positive or negative.

**Example.** Take the same trade: after it you hold 5 ETH and 30,050 USDC instead of the 10 ETH and 20,000 USDC you deposited. Then the price moves:

| ETH price          | Your position | If you had held | Trading PnL | Vs. your deposit |
| ------------------ | ------------- | --------------- | ----------- | ---------------- |
| $2,200 (+10%)      | $41,050       | $42,000         | −$950       | +$1,050          |
| $2,000 (unchanged) | $40,050       | $40,000         | +$50        | +$50             |
| $1,800 (−10%)      | $39,050       | $38,000         | +$1,050     | −$950            |

In the first row, $$-5 \times 2{,}200 + 10{,}050 = -950$$.

If the price doesn't move, trading PnL is just that spread. Had nobody traded, your position would match holding in every row and trading PnL would be $0. Trading PnL compares you with holding, not with your deposit: in the last row you're $1,050 ahead of holding but still down $950.

Your own deposits and withdrawals aren't gains or losses. Each one starts a new comparison from what you hold at that point, and the changes before it are valued at the prices when you made it.

## Why price moves can outweigh markout

Markout stops counting at its horizon, but the tokens you received keep moving with the market. If the token bought for $999 is worth $1,000 five minutes later, the markout is $1; if it's later worth $1,010, you're $11 above its purchase cost; at $990, $9 below, despite the $1 of markout. In the example above, a +$50 markout becomes −$950 of trading PnL after a 10% rise.

That's why a good strategy with positive markout can still lose money for a while: every trade leaves you holding more of one token, and nobody knows where prices go next. In the short run, price moves dominate. Over many trades, markout adds up while price moves partly cancel out, so in a correctly set-up pool, markout should dominate.

{% hint style="warning" %}
**Positive markout doesn't mean you're making money.** You can still lose money providing liquidity, including your entire deposit. See [Risks and safeguards](/use-metric/risks-and-safeguards).
{% endhint %}

Related: [Fees](/use-metric/fees) · [Risks and safeguards](/use-metric/risks-and-safeguards).


# Curators

A **curator** is whoever runs a Metric pool: it chooses the pricing strategy and the pool's settings. This page covers public pools, which take deposits from other users.

In the Metric app, only a pool admin verified by Metric is shown as a curator ("curated by"). An unverified one is shown as an **operator** ("operated by"), and the tier checklist reads "Operator, not a verified curator."

{% hint style="warning" %}
**Verified doesn't mean endorsed.** Neither verification nor the 🟢 Best Practices tier is an endorsement or warranty of any curator or strategy.
{% endhint %}

All pools on Metric are open to deposits by default, unless a deposit-allowlist extension restricts them. A pool that doesn't meet a tier's requirements is shown as 🔴 High Risk, so depositors can see which pools lack the safeguards. A pool that hasn't been classified yet is shown as Unknown. See [Pool tiers](/use-metric/pool-tiers).

To reach the 🟢 Best Practices tier and attract outside deposits, the operator must become a verified curator.

## What curators do

Curators choose pricing logic, configure bins, fees and safeguards, arrange price updates where needed and monitor the pool. LPs choose where to allocate their liquidity.

## What curators earn

Curators receive their configured share of the spread, which pays for maintaining the pool and building its strategy.

See [Fees](/use-metric/fees).

## Become a curator

Apply through the [curator application form](https://metricxyz.typeform.com/to/b6HVTTqc), or [contact us](/start-here/contact-us).

The process includes KYB, identity verification and due diligence. Most reviews take less than one week.

Verified curators receive access to the curator group, security-tiering support, tools and introductions to potential clients in Metric's ecosystem.


# Referrals

### How it works

1. **Create your referral code** in the [Metric app](https://app.metric.xyz/referrals/).
2. **Share your link.** Eligible new users will be attributed to you.
3. More details will be revealed soon

> Metric may adjust or end the program at any time, and may void rewards from self-referral or other misuse of the program.


# Getting started

Choose the pool's pricing logic, configure how it trades and keep it operational. You can reuse Price Providers and extensions; custom development is optional.

## Before you start

Choose the chain, token pair and who will supply liquidity:

* **Your own capital only:** run a private pool. Pools accept outside deposits by default; configure deposit permissions to restrict them.
* **Outside LP capital + Your own capital:** follow [curator onboarding](/use-metric/provide-and-manage-liquidity/public-pools-and-curation) and choose a configuration that meets the intended [pool tier](/use-metric/pool-tiers). Decide this before deploying: some settings can't be changed later.

Have the token addresses, pool-admin wallet and fee-recipient address ready.

## Choose pricing inputs

Prices can follow an offchain feed, another market or custom strategy logic.

* **Offchain inputs:** Maintain the feed yourself or use a trusted updater. Shared trusted feeds may already have one.
* **Onchain inputs:** Check the source contracts and whether the strategy still needs updates. An update-independent pool needs no price pusher.

Both setups need testing and monitoring.

## Build and launch

| Step                                                            | What you do                                                     | What you need before continuing                                   |
| --------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- |
| 1. [Build a Price Provider](/run-a-pool/build-a-price-provider) | Select or build the provider.                                   | Provider address, correct token addresses and validated logic.    |
| 2. [Build an extension](/run-a-pool/build-an-extension)         | Select existing extensions or build your own.                   | Initialized extension addresses and their order.                  |
| 3. [Configure a pool](/run-a-pool/create-and-configure-a-pool)  | Set bins, fees and permissions; create, fund and test the pool. | Successful quotes, swaps and withdrawals.                         |
| 4. [Operate a pool](/run-a-pool/price-updates-and-monitoring)   | Monitor the pool and respond to failures.                       | Required updates, monitoring and failure response are configured. |

See [Best practices](/run-a-pool/chain-specific-guides) for update-dependent pools and chain-specific guidance, and [Routing quality](/run-a-pool/routing-quality) for Metric's execution-quality requirements.

## Tooling

The [Open source kit](/run-a-pool/examples) is the entry point for beginners, with deployment and update-pusher tooling.


# Build a Price Provider

A Price Provider implements the pool's pricing logic and returns bid, ask and reference price.

## 1. Choose your logic

| Strategy                                   | Example logic                                                                                         |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Follow offchain prices**                 | Read the Compressed Oracle, Chainlink or Pyth; configure spreads and sanity checks.                   |
| **Follow another DEX onchain**             | Use another DEX's mid or TWAP as a reference or cross-check.                                          |
| **Constant price & custom spread**         | Keep the reference constant and adjust bid/ask with custom logic. Bins stay at fixed absolute prices. |
| **Discover price from trades**             | Use an `afterSwap` extension to report fills; read pool state to respond to liquidity imbalance.      |
| **Read backing for a yield-bearing asset** | Use a staking or vault conversion rate to price the token against its underlying asset.               |
| **Custom math or formula**                 | Calculate prices with a model, such as Black-Scholes with volatility inputs, HSVM or LMSR.            |

Identify required updates, including for onchain strategies. See [Oracles](/how-pools-work/oracles) for update infrastructure and [anchored pricing](/how-pools-work/anchored-pricing#anchored-pricing) for constrained custom pricing in public pools.

## 2. Implement the interface

```solidity
interface IPriceProvider {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getQuote() external returns (
        uint128 bidPrice,
        uint128 askPrice,
        uint128 referencePrice
    );
}
```

**Tokens:** `token0` and `token1` must match the pool's tokens, in the same order.<br>

**Units:** Prices are whole `token1` per whole `token0`, as [Q64.64](/reference/glossary) (× 2⁶⁴), whatever the token decimals. With token0 = WETH and token1 = USDC, 2,000 USDC per WETH is 2000 << 64.<br>

**Validity:** Each quote must satisfy:

* `0 < bid < ask`
* `bid < referencePrice < ask` (strictly between)
* the pool's asymmetry limit: with `r₁ = ref/bid − 1`, `r₂ = ask/ref − 1` and `M = 1 + bidAskRefAsymmetryX8/256`, the pool requires `1/M ≤ r₁/r₂ ≤ M`.

bidAskRefAsymmetryX8 can't be 0. At creation it can be at most the factory's creation cap; only the factory owner can set a higher value later. If it is type(uint16).max, the asymmetry check is off and the reference may equal bid or ask.\
\
**Failures:** If an input is stale or missing, revert. The pool wraps the revert in PriceProviderFailed and the swap fails. An invalid quote makes the pool revert with a specific error. See [Price Providers](/how-pools-work/price-providers) for quote semantics.\
\
**State:** getQuote() is not view. The pool calls it with a normal call, so it can update state, for example to refresh a cached price or a TWAP. Custom sources read by an APP through `staticcall` must be static.

### Reference price

Use the geometric mid, sqrt(bid \* ask), or your best estimate of fair price.

{% hint style="warning" %}
**Report an honest reference.** LPs' spread on each side and their markouts are measured from it, and so are the curator's and Metric's fees. Misreporting it can get the pool deprioritized or frozen.
{% endhint %}

The reference price must stay within the pool's `bidAskRefAsymmetryX8` limit.

## 3. Test and deploy

Test unequal decimals, rounding, stale or missing inputs, quoting, halt/recovery and both swap directions.

Use the provider's address when you create the pool.

For Metric routing, keep quotes caller-independent and support zero-origin simulation.

Next: [Build an extension](/run-a-pool/build-an-extension).


# Build an extension

Extensions run before or after swaps, deposits and withdrawals. They can reject an action or record its result, but they cannot replace the current quote or change the executed amounts. You can reuse [standard extensions](/how-pools-work/extensions-and-permissions#standard-extensions) or write your own.

For a public pool, only **withdrawal** extensions can lower the tier. Any extension registered on `beforeRemoveLiquidity` or `afterRemoveLiquidity` makes the pool 🔴 **High Risk**, including Metric's standard extensions. Adding swap or deposit extensions doesn't lower the tier.

See [Pool tiers](/use-metric/pool-tiers).

## 1. Pick the trigger

| Extension               | Runs                | Also receives                                                                    |
| ----------------------- | ------------------- | -------------------------------------------------------------------------------- |
| `beforeSwap`            | Before execution    | Request, bid/ask/ref, initial slot 0                                             |
| `afterSwap`             | After execution     | Everything `beforeSwap` receives, plus final slot 0, amount deltas, platform fee |
| `beforeAddLiquidity`    | Before a deposit    | Owner, salt, bin deltas                                                          |
| `afterAddLiquidity`     | After a deposit     | Everything `beforeAddLiquidity` receives, plus amounts added                     |
| `beforeRemoveLiquidity` | Before a withdrawal | Owner, salt, bin deltas                                                          |
| `afterRemoveLiquidity`  | After a withdrawal  | Everything `beforeRemoveLiquidity` receives, plus amounts removed                |

Slot 0 snapshots hold the whole packed slot 0: bin index and position, fees, asymmetry and pause level.

Any extension can revert. A revert undoes the whole action, token transfers included.

Swap extensions also run inside `simulateSwapAndRevert`. That function is permissionless: the caller chooses `sender` and the quote, and all state changes are rolled back. Extensions should behave the same as in a real swap so that router quotes stay accurate.

Below are a few examples and use cases:

| Use case                | Extension            | Logic                                                                                                                                                                                                                                                                              |
| ----------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Priority-fee cap**    | `beforeSwap`         | Revert when the transaction's priority fee is above the pool's cap, so updates that pay above it take priority over swaps pending at the same time. Arrival time and the chain's ordering rules still apply. See [Freshness and ordering](/how-pools-work/freshness-and-ordering). |
| **Trading hours**       | `beforeSwap`         | Reject swaps outside market hours or during a halt. Withdrawals stay open. Built for tokenized stocks and other assets with a closed market.                                                                                                                                       |
| **Volume budget**       | `afterSwap`          | Sum executed volume per time window and revert when the swap that just executed pushes the window over budget. Use the actual deltas: swaps can fill partially.                                                                                                                    |
| **Fill reporting**      | `afterSwap`          | Record deltas in strategy state that the [Price Provider](/run-a-pool/build-a-price-provider) reads on its next quote: widen the ask after sustained buying, tighten as flow fades. The swap that triggered the report is not repriced.                                            |
| **Deposit eligibility** | `beforeAddLiquidity` | Check the position owner against an allowlist or a signed permit. The caller is usually a router; the owner is the LP.                                                                                                                                                             |
| **Rewards**             | `afterSwap`          | Award points on executed amounts. Fund token rewards separately, never from pool balances.                                                                                                                                                                                         |

## 2. Implement it

Use the exact signatures in `IMetricOmmExtensions`. Implement `initialize(address pool, bytes data)` and the extensions you register, and return each function's own selector on success, for example `IMetricOmmExtensions.afterSwap.selector`. A revert or a wrong selector rejects the action.

* **Initialization.** Authenticate the factory, validate and store settings per pool, and prevent reinitialization. `initialize` runs before the factory records the pool's registry entry and admin, so don't read either of them inside `initialize`.
* **Identity.** In an extension, `msg.sender` is the pool: authenticate it. The `sender` argument is the pool's caller, usually a router, not the trader. `extensionData` is caller-supplied and forwarded unchanged to every extension of that action: validate it, and bind any permit to the account, action, pool, chain, expiry and nonce.
* **State.** Use the supplied deltas and snapshots. Don't reenter the pool's guarded methods, including `getSellAndBuyPrices()`.

{% hint style="warning" %}
**Authenticate the pool.** An extension that doesn't check that `msg.sender` is a pool it was initialized for can be called by anyone, with made-up arguments.
{% endhint %}

## 3. Attach and test

At pool creation, supply up to seven extension addresses, one initialization payload each, and the execution order per action. Addresses and order are fixed for the life of the pool; settings can stay mutable if your extension allows it. Don't register withdrawal extensions unless blocking LP exits is intended.

Confirm that each guard you need is initialized with settings that turn it on. For example, a StopLoss with `drawdownE6 = 0` is attached but does nothing.

Test through the real router and liquidity entry points, not only direct calls: unauthorized callers, replayed permits, partial fills, both directions, limit boundaries, rollback, and withdrawals. Keep extension work bounded and check gas.

Next: [Configure a pool](/run-a-pool/create-and-configure-a-pool).


# Configure a pool

Prepare your Price Provider and extensions, then create the pool through the [Metric app](https://app.metric.xyz) or `MetricOmmPoolFactory.createPool(PoolParameters)`.

{% hint style="info" %}
**Oracle and contract addresses:** see [Contract addresses](/reference/supported-chains-and-deployments).
{% endhint %}

Decide on the [pool tier](/use-metric/pool-tiers) you're aiming for before choosing settings. Several settings are permanent once the pool exists: tokens, bin layout, extension addresses and order, initial share amounts, minimalOperationalLiquidity, and the price provider timelock (so a fixed price provider stays fixed, and a replaceable one can't later be made fixed).

## 1. Pair and pricing

* Set the chain, `token0`, `token1` and the provider address. The provider's `token0()` and `token1()` must match the pool's tokens in the same order, or creation reverts.
* Choose how the provider can be replaced, using priceProviderTimelock:
  * **Fixed**: type(uint256).max. The provider can never be replaced.
  * **Timelocked**: any other value above 0. The admin proposes a new provider and can switch to it after the delay.
  * **Replaceable at once**: 0. The admin still proposes a new provider first, but can switch to it right away.
* Set `bidAskRefAsymmetryX8`. It must be greater than zero and no higher than the factory's creation cap. Test your strategy's quotes with this value. The factory owner can change it after creation, and the cap doesn't apply then.

## 2. Bins and initial liquidity

| Setting                   | What to specify                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Bin layout**            | Widths of each bin and total coverage in percentage deviations from the reference.                            |
| **Starting position**     | The starting deviation.                                                                                       |
| **Additional bin fees**   | Extra fees for each trading direction. These remain adjustable after creation.                                |
| **Initial share amounts** | The token amounts one LP share represents when liquidity is added to an empty bin.                            |
| **Minimum liquidity**     | The smallest number of shares you can add or remove in a bin, and the smallest position you can leave behind. |

For example, 13 bins of 0.01% on each side cover ±0.13%.

Choose bins for the initial deposit; depending on the bins, it needs one token or both. Later LPs choose their own allocations.

## 3. Extensions and permissions

* Supply up to 7 extension addresses (distinct and non-zero), one initialization payload per extension, and the call order for each action. See [Build an extension](/run-a-pool/build-an-extension).
  * Confirm that each guard you need is initialized with settings that turn it on. For example, a StopLoss with `drawdownE6 = 0` is attached but does nothing.
  * To reach 🟡 Neutral or 🟢 Best Practices, lock the StopLoss: set its timelock to `type(uint40).max` and leave no change queued. Any shorter timelock leaves the pool 🔴 High Risk.
* Deposits are open to anyone by default. To run a private pool on your own capital only, attach the deposit allowlist.
* For public pools, it's recommended to complete the [curator onboarding](/use-metric/provide-and-manage-liquidity/public-pools-and-curation) to get better safety rankings.

## 4. Admin and fees

Set the admin, the admin fee recipient (`adminFeeDestination`) and the admin fees:

* Spread fee (`adminSpreadFeeE6`): the admin's share of the spread each trade pays (the half spread plus any additional bin fee). Metric also takes a share of the spread, and LPs keep the rest.
* Notional fee (`adminNotionalFeeE8`): an extra charge on trade volume, added to the trader's price, that would go to the admin, none to LPs. The admin cap is currently 0%, so it must be left at `0`.

Read the defaults and caps from `GET /public/v1/evm/factory-settings`, and read the actual rates from the pool's metadata after creation. If you set a protocol fee below the factory default, it is raised to the default. After creation, the admin can change the admin fees within the factory's caps, the additional bin fees and the admin fee recipient. The admin can also pause the pool, propose a new provider (unless it is fixed) and transfer the admin role. Metric sets the protocol fees. If Metric lowers the caps, admin fees above the new cap are lowered to it the next time Metric updates that pool's protocol fee. See [Fees](/use-metric/fees).

Anyone can call `factory.collectPoolFees(pool)`. Fees go to the configured recipients, not the caller. If a transfer to a recipient fails, the pool emits `FeeTransferFailed` and keeps that fee pending for a later collection, so check the event or balances.

## 5. Create, fund and verify

Record the pool address, verify its configuration and complete required source-access registration. Start with a small deposit and check:

* [ ] Correct tokens, provider, admin, fee recipient and extension settings.
* [ ] Live quotes and simulations succeed through the intended route.
* [ ] Actual swaps work in both directions with expected fees and amounts.
* [ ] Intended LPs can deposit and withdraw; prohibited actions fail.
* [ ] Guard rejection paths work. Account for StopLoss's first-trade baseline before accepting outside funds.
* [ ] Required updates and monitoring are running; displayed tier and routing status match the configuration.
* [ ] The pool holds at least \~$1,000 in liquidity. Most aggregators don’t route to smaller pools

Verify actual swaps and withdrawals before increasing liquidity; simulation alone is insufficient.


# Routing quality

Metric holds every pool to the price it quotes. Each pool is judged on three things:

1. **Revert rate:** the share of swaps through the pool that fail.
2. **Average slippage:** how far, on average, fills through the pool land from their quotes.
3. **Liquidity threshold:** Most aggregators only start routing to a pool once it holds about $1,000 or more in liquidity. Below that, the pool can still trade through Metric’s app and API, but expect little or no aggregator flow. Each aggregator sets its own threshold, so the exact cutoff varies.

Pools with a high revert rate or high slippage can be deprioritized in routing or frozen. A frozen pool can't trade, but LPs can still add and remove liquidity.

Metric treats slippage manipulation as malicious and can freeze any pool whose logic or curator is malicious.


# Operate a pool

Every pool needs monitoring. Only update-dependent pools need an update pipeline.

## Keep pricing operational

* Check that every required feed stays usable, including feeds kept fresh by a shared updater you don't run. A shared updater can stop or change its cadence without telling you.
* Test stale-data handling end to end. Through your Price Provider, confirm that a stale price is rejected and that quoting recovers once a fresh update arrives.
* For **conditional updates**, watch two things: whether your updates reach the update pipeline, and which quote is actually used when trades execute. An idle feed's stored timestamp can look stale even when the pipeline is healthy, and fresh even when it isn't. On its own it doesn't tell you the feed is live.
* Set alerts on missing data, not just bad values. If an update or monitoring signal stops arriving, that should page you. It shouldn't just go quiet.

Setup: [Oracles](/how-pools-work/oracles) · [Best practices](/run-a-pool/chain-specific-guides) · [Open source kit](/run-a-pool/examples).

## Monitor the pool

* **Pricing:** update age, quote failures and divergence from an independent reference.
* **Execution:** failed swaps, slippage and available liquidity in both directions.
* **Capital:** inventory and position value separately from markout. A negative markout means trades are getting ahead of your quotes.
* **Controls:** pause state, guard settings, pending provider/admin changes and pool tier.
* **Routers:** Test through the route traders will use. Gated oracle reads may reject direct calls even while the pool is working correctly, so a failed direct read doesn't prove the pool is broken.
* **Extensions:** Watch extension guards by direction and size. A tripped guard, such as StopLoss, can block trades in one direction or above a certain size while the pool stays open otherwise. Check liquidity on each side separately.

## Respond to failures

1. Stop the damage. If pricing is unavailable or wrong, halt quoting, or have the admin call `factory.pausePool(pool)`.
   * **A swap pause doesn't lock deposits or withdrawals.** Their extension rules still apply.
   * An admin can't lift a protocol-level pause.
2. Fix the cause, then run the [launch checks](/run-a-pool/create-and-configure-a-pool#5-create-fund-and-verify) again before resuming with `factory.unpausePool(pool)`.
3. Confirm the recovery. After unpausing, run a small swap in each direction through the route traders will use, and keep a close watch on pricing and execution until the pool has been stable for a while.


# Best practices

## Operating a feed

For an update-dependent pool, a delayed feed update is a loss for liquidity providers. This page is about minimizing delays, minimizing the losses from delays, and protecting liquidity providers.

There are three components:

* What your Price Provider does (Price Provider)
* How the chain orders transactions (Chain rules)
* How your update gets onchain (Update pipeline)

Update-independent pools, whose providers read onchain state and push nothing, need only the first part.

## Price Provider

**Freshness.** Set the provider's freshness limit and revert when the data is older than it.

**Toxicity protection.** On fee-sorted chains, cap the priority fee a swap may pay and pay above the cap on updates (see fees below). Use other heuristics, such as widening or halting, when your feed is at risk of going stale.

**Halting.** Halt by reverting from `getQuote()`.

**Cross-checks.** Compare against an independent source, such as Chainlink, Pyth or another market's TWAP, and halt on divergence.

**Reference honesty.** Report the reference as the geometric mid or your best fair price. It sets the fee split and the displayed markouts; misreporting can lead to pool deprioritization.

**Caller independence.** `getQuote()` returns identical values regardless of `msg.sender` and `tx.origin`, and does not revert when `tx.origin` is the zero address. Otherwise simulation breaks and the pool is deprioritized in routing.

**Rounding.** Bid rounds down, ask rounds up.

## Chain rules

Chains differ in how transaction order is built and whether your update can be placed before the trades that use it. The regimes are described in [Freshness and ordering](/how-pools-work/freshness-and-ordering); find your chain's row there before building. Chain specifics: [Ethereum](/run-a-pool/chain-specific-guides/ethereum) · [BSC](/run-a-pool/chain-specific-guides/bsc) · [Robinhood](/run-a-pool/chain-specific-guides/robinhood).

Rules that hold on every chain:

1. Learn your chain's transaction ordering before anything else: block time, sub-block batches if any. Confirm the supported submission route before deploying.
2. Optimize delivery of your quotes: pick a location that minimizes the delay of your transaction. Information-source location, sequencer location and a direct endpoint are essential.
3. Submit writes to the sequencer or builder endpoint; use a private RPC for reads. Sequencer endpoints usually accept submissions only.

## Update pipeline

### Sending

1. Sign once locally; submit raw bytes with `eth_sendRawTransaction` over warm connections to several endpoints. Send identical bytes to all of them concurrently.
2. Read receipts (`status`, `gasUsed`, `effectiveGasPrice`) asynchronously through the read RPC. Never block the next send on a receipt.
3. Timestamps are monotonic on every Metric oracle: an older update cannot overwrite a newer one. Send freshest first.

### Fees

1. Never wait for a fee RPC or gas estimation on the send path. Refresh `baseFeePerGas` and `eth_gasPrice` in the background into a cache.
2. On fee-sorted chains, cap the priority fee a swap may pay (a `beforeSwap` extension) and pay above the cap on updates. Your update is then selected ahead of any capped swap pending at the same time. A swap selected before your update arrives stays ahead, so delivery time still matters.
3. Estimate the update call's gas cost before launch and leave 20-30% headroom.
4. Where an additional fee buys no ordering, keep it at zero.

### Nonces and wallets

For your own feed, keep the **administration key separate from the online process** and delegate only update permissions.

1. Use many wallets for updates: at least 50. Refill them before they run dry. A balance must cover fee headroom; a small gas balance leads to irregular updates and can lead to a loss of funds.
2. Reserve each wallet's nonce locally before signing.
3. `replacement transaction underpriced`: another transaction holds the nonce. Handle it separately from nonce sync.

### Measuring

1. Time each stage separately: trigger → send, construction, signing, RPC call, endpoint response - and measure execution separately.


# Ethereum

{% hint style="info" %}
**Oracle and contract addresses:** see [Contract addresses](/reference/supported-chains-and-deployments#oracle-and-price-provider-layer).
{% endhint %}

## Update-independent pools

If your pool doesn't need constant oracle updates, nothing special is required: follow [Best practices](/run-a-pool/chain-specific-guides).

## Update-dependent pools

### Conditional updates protocol

On Ethereum, Metric supports **conditional updates** to cut gas costs. You don't push to a schedule. You sign a quote-update transaction and Metric streams it to partner block builders. A builder includes it only in a block that contains a trade against your pool, placed ahead of that trade. If nobody trades, the update doesn't land and costs no gas. See [Freshness and ordering](/how-pools-work/freshness-and-ordering) for what that covers and what it doesn't.

Metric integrates with Titan, Quasar, BuilderNet (Flashbots), Bombora, Eureka, BTCS and ETHGas for the conditional updates protocol. Delivery to each builder depends on its configured endpoints and credentials; an acknowledgement does not guarantee inclusion.

**Always push your updates to Metric's endpoint**, even if you also push to builders directly. Metric's infrastructure needs the updates for three things:

* Live prices in the UI. Between trades, your feed looks stale onchain. Metric applies each update to its own simulated copy of Ethereum's latest block, so your pool shows a live price.
* Fair aggregator quotes. Aggregators and solvers price your pool from Metric's quote endpoint, which reads that same simulated state. Without your updates they quote your pool at a stale price, or not at all.
* Price history. When your onchain feed has no valid price, Metric records the price from your latest update instead, so your pool's chart has fewer gaps.

Pushing directly to builders is optional. Metric forwards the updates it receives to each builder, but doesn't guarantee delivery. The response tells you which builders acknowledged it in time. A direct connection can still cut latency if you update very often. If you do push directly, still push to Metric too.

{% hint style="info" %}
Send the same update to Metric and to the builders: the same signed transaction, with the same **replacementUuid** and **replacementSeqNumber**. If they differ, Metric shows and quotes one price while the builder includes another. Takers then get quoted a price your pool won't fill, and later updates may be ordered wrongly.
{% endhint %}

### Oracles

**Metric supports these oracles for conditional updates:**

**For propAMMs, Metric recommends:**

1. **Compressed Oracle:** Metric's custom oracle, built for propAMMs: up to 4 feeds in one slot update, about 8,000 gas per feed. Use it when your pools live on Metric and gas per update is what you optimize for.
2. **PUR (Priority Update Registry) by Flashbots:** the shared, venue-neutral registry for priority updates, maintained by Flashbots. Both options are first-class for conditional updates; the trade-off is gas per feed, where the Compressed Oracle is cheaper. Flashbots redeploys PUR from time to time, so take the current address from the [PUR repository](https://github.com/flashbots/priority-update-registry).

**Public oracles:**

**Chainlink and Pyth:** required for APPs; also useful for sanity checks.

For Anchored Price Providers, Metric recommends streaming the anchoring feed and your additional updates together, from the same address. Your two most recent updates should correctly serve one trade.

### Metric endpoint

Submit each signed update with `POST https://api.metric.xyz/public/v1/evm/1/pamm-quote`, authenticated with a trading API key. With every update, always include as much information as you can: `quoteAddress`, `poolAddresses` and `assetPairs`. Builders use it to match the quote to a trade, and a quote they can't match is never included. A builder may not use every field, but the more you send, the more likely your quote is to be included. Request and response formats: [PropAMM quote](/reference/api/pamm-quote).

### Builder endpoints and documentation

The builders use the same protocol: a WebSocket stream of protobuf messages, with an API key you get from each builder.

| Builder    | Maker endpoint(s)                                           | Docs                                                             |
| ---------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| Titan      | `wss://{eu,us,ap}.rpc.titanbuilder.xyz/ws/sendquoteupdate`  | <https://docs.titanbuilder.xyz/propamms/makers>                  |
| Quasar     | `wss://rpc.quasar.win/ws/sendquoteupdate`                   | <https://docs.quasar.win/propamm/makers>                         |
| BuilderNet | `wss://rpc.buildernet.org/ws/sendquoteupdate`               | <https://buildernet.org/docs/api#priority-updates>               |
| Bombora    | `wss://direct-tyo-1.bombora.build/ws/sendquoteupdate`       | <https://bombora.build/docs/propamm-makers>                      |
| BTCS       | `wss://metric-rpc-{us,eu}.btcs.com/ws/sendquoteupdate`      | [Quote API](/reference/api/pamm-quote)                           |
| ETHGas     | `wss://{ap,ap-2}.propamm.rpc.ethgas.com/ws/sendquoteupdate` | <https://docs.ethgas.com/get-started/traders-new/propamm/makers> |

BTCS uses US and EU quote endpoints. Metric submits directly to both and disables BTCS cross-region sharing, regardless of the request flag. Its pool-address list is optional because it also uses event-join matching; send complete quote metadata for compatibility with the other builders.

ETHGas uses four regional clusters: US East, EU Central and two in Tokyo. Metric currently submits directly to the two Tokyo clusters only and disables ETHGas cross-region sharing, regardless of the request flag. ETHGas evicts a quote that has not been updated for 400ms and recommends requoting about every 50ms, even when the price has not moved; slower makers lose ETHGas coverage between updates.


# BSC

On BSC, a higher fee per gas can improve a transaction's position, but final ordering also depends on **total block profitability**. Large transactions, or bundles with big bids or bribes, can be placed ahead of a market maker's oracle update, even when the update pays a higher fee per gas.

Builders have an incentive to include arbitrage against stale quotes when it makes their block proposal more profitable. This can expose propAMM liquidity to repeated adverse trades and erode market-maker returns. Stale quotes are the main risk for update-dependent pools on BSC; the protected execution lane below reduces it.

## Update-independent pools

If your pool doesn't need constant oracle updates, nothing special is required: follow [Best practices](/run-a-pool/chain-specific-guides) for the security and safety of your pool.

## Update-dependent pools

### Protected execution lane

Metric runs a protected execution lane with the BSC builders 48Club and BlockRazor. It is optional. You don't connect to either builder yourself: you submit your signed quote-update transaction to Metric once, and Metric forwards it to both builders at the same time.

```
POST https://api.metric.xyz/public/v1/evm/56/pamm-quote
```

You need a trading API key for this endpoint.

In blocks that 48Club or BlockRazor build, your update is placed ahead of trades against your pool. Blocks from other builders carry no such commitment. See [Freshness and ordering](/how-pools-work/freshness-and-ordering).

* 48Club gets your update over a persistent quote stream. When you send a newer quote (a higher seq on the same uuid), 48Club drops the one it replaces. An empty tx cancels your live quote.
* BlockRazor gets your update as a bundle that goes first in the block. Accepted bundles may land and pay gas even without a trade, regardless of `alwaysInclude`. Metric authenticates every BlockRazor call for you; if its builder credential is unavailable, that leg is skipped for either setting. Authentication does not guarantee conditional inclusion.

#### What each submission must carry

* **alwaysInclude** (optional boolean, default `true`): forwarded directly to 48Club, where `true` selects unconditional inclusion and `false` requests conditional inclusion. BlockRazor does not receive this flag in its bundle body.
* **blockNumber**: the last block your quote is valid for. It must be non-zero for a live quote.
* **maxTimestamp** (unix seconds): when your quote expires. It can be at most 5 seconds ahead of the current time. If you leave it out, your update still goes to 48Club, but the BlockRazor leg is skipped.

#### Canceling and stale quotes

BlockRazor has no cancel operation. A bundle it has accepted stays live until its `blockNumber` or `maxTimestamp` passes, even after you send a newer quote or a cancel. The same signed transaction can only land onchain once, so it can never be included twice. But an outdated quote can still land within that window. A short `maxTimestamp` is your only protection against this, so keep it as tight as your update cadence allows.

#### Accepted is not included

The response lists each builder's result. Metric answers within 500 ms; a builder that hasn't answered by then shows `pending: true`, and your quote is still delivered to it. `accepted: true` means at least one builder took your quote by then. It does not mean the quote landed onchain. For BlockRazor, the response also returns a bundleHash. Judge the lane on what lands onchain, not on acceptances.


# Robinhood

Robinhood Chain orders transactions **first come, first served**, by arrival at the sequencer. Higher gas fees can't move an oracle update ahead of transactions already queued. No builder or fee mechanism places your update ahead of a trade here; see [Freshness and ordering](/how-pools-work/freshness-and-ordering).

Update-dependent pools should therefore minimize their delay to the sequencer.

All other [Best practices](/run-a-pool/chain-specific-guides) apply here.


# Open source kit

Metric provides basic open-source tools for beginner market makers:

1. [Oracle update pusher.](https://github.com/Metricxyz/mm-pusher-ts)
2. [Local analytics dashboard.](https://github.com/Metricxyz/open-swap-dashboard)
3. [Dashboard for estimating pool economics and oracle update gas costs.](https://github.com/Metricxyz/cost-simulator) ([Web version](https://pool-simulator.metric.xyz/))
4. [Dune dashboard: DEX aggregator volume, multichain.](https://dune.com/metric_xyz/aggregator-flow-multichain?1_blockchain_e06956=bsc)


# Integrate Metric liquidity

For aggregators and solvers

## Integration Overview

Connect Metric liquidity in an aggregator, solver or trading application.

Integrate through the Metric API or directly through its contracts.

### Option 1: API integration (recommended)

Use the Metric API for pool metadata, quotes, depth and routes. Quote and route endpoints are public; a Trading API key raises their rate limits. See [Via API](/integrate/integrate-metric-liquidity/via-api).

### Option 2: smart contract or SDK integration

Call Metric's contracts directly: quote offchain with `MetricOmmSwapQuoter` (via `eth_call`), then swap through the router or the pool. A contract cannot get a live quote from inside a transaction for a pool priced by Chainlink or Pyth. See [Via smart contracts](/integrate/integrate-metric-liquidity/via-smart-contracts).

Compressed oracle feeds have public reads. Chainlink and Pyth reads are restricted to registered pools during swaps, approved integrators, and offchain `eth_call`s made without a `from` address. See [Oracles](/how-pools-work/oracles).

### Executing a swap

Before routing, read pool metadata and route only to pools whose `priceProviderStatus` is `healthy` and whose `pauseLevel` is `0`. Route legs return the no-limit `priceLimitX64`, so set your own protection: `amountOutMinimum` for exact input, `amountInMaximum` for exact output.

A successful quote does not guarantee execution. Before sending, simulate the actual transaction with your real `from` address.

### Why a swap reverts

| Cause                                                                                      | Error                                                                                                                                                 | Check before sending                                                                                               |
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Price data too old or missing, or the provider halted                                      | Swap: `PriceProviderFailed(reason)`, where `reason` holds the provider's error (e.g. `FeedStalled()`). Quote: the provider's error itself, unwrapped. | Metadata `priceProviderStatus` is not `healthy`                                                                    |
| Pool not registered with its oracle                                                        | `PriceProviderFailed(reason)`, where `reason` holds the oracle's `NotRegistered(feedId, pool)`                                                        | Quotes still succeed, because offchain reads skip the access check. Simulate the transaction with your real `from` |
| Provider quote failed the pool's price checks                                              | `BidAskRefAsymmetryOutOfBounds`, `BidGreaterThanAsk`, `ReferencePriceOutOfRange`, `BidIsZero`                                                         | A Live quote reverts with the same error, wrapped                                                                  |
| Pool paused                                                                                | `PoolPaused`                                                                                                                                          | Metadata `pauseLevel` above 0. A Live quote reverts with the quoter's `PoolPaused(pool, pauseLevel)`, unwrapped    |
| StopLoss: the swap would leave a bin's per-share value further below its peak than allowed | `OracleStopLossTriggered(binIdx, isToken0Metric, currentMetric, threshold)`                                                                           | A quote runs extensions and reverts with the same error, wrapped                                                   |
| Velocity Guard: the reference price moved faster than allowed                              | `PriceVelocityExceeded(actualDeltaSqE36, allowedDeltaSqE36)`                                                                                          | A quote runs extensions and reverts with the same error, wrapped                                                   |
| Sender not on the pool's swap allowlist                                                    | `NotAllowedToSwap`                                                                                                                                    | Quote with the real `sender` (the router, for router swaps)                                                        |
| Trading hours or another custom extension                                                  | The extension's own error                                                                                                                             | Quote with the real `sender`, `recipient` and `extensionData`                                                      |
| Pool not from the current factory                                                          | `InvalidPool` (router)                                                                                                                                | Use only pools listed by `/metadata`                                                                               |
| Output below your minimum, or input above your maximum                                     | `InsufficientOutput`, `InputTooHigh`                                                                                                                  | Your own limits                                                                                                    |
| Exact output not reached (price limit or thin depth)                                       | `InvalidOutputAmount`                                                                                                                                 | Quote the exact output first                                                                                       |
| A multihop leg filled only partially                                                       | `InvalidInputAmountAtHop`, `InvalidOutputAmountAtHop`                                                                                                 | A quote reverts with the quoter's own errors of the same name, which have different selectors                      |
| Deadline passed                                                                            | `TransactionExpired`                                                                                                                                  | —                                                                                                                  |
| Nothing to fill on that side                                                               | `FinalAmountIsZero`                                                                                                                                   | A quote reverts with the same error, wrapped                                                                       |
| Direct pool swap: callback underpaid                                                       | `IncorrectDelta`                                                                                                                                      | Only when calling the pool directly                                                                                |

In a quote, "wrapped" means the quoter reverts `WrappedError(pool, selector, reason)`; decode `reason` for the error in the table.

On a swap, extension errors reach you unwrapped: the pool re-throws the extension's revert data unchanged.


# Via API

## 1. Discover Available Pools

Query the [metadata endpoint](/reference/api/metadata) to retrieve:

* Supported trading pairs
* Pool addresses
* Quoter and token addresses

Route only to pools whose `priceProviderStatus` is `healthy` and whose `pauseLevel` is `0`.

***

## 2. Fetch Current Bid / Ask and Depth

Request the current [bid and ask prices](/reference/api/retrieve-batch-bid-and-ask) for up to 100 pools on one chain in a single `POST /public/v1/evm/{chain_id}/bid_ask/batch` call. No key is needed; a Trading API key raises the rate limit.

* Each pool gets the same quote as the single-pool [bid / ask endpoint](/reference/api/retrieve-current-bid-and-ask), or an `error` if that pool can't be quoted
* Depth is returned in addition to prices (pass `"depth": false` to skip it). Use it to [estimate what an order fills for](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth)
* Returned prices are already normalized for use with Metric’s contracts

Check each side's price, not only `priceProviderStatus`: a pool can refuse one direction while staying `healthy`, and that side then carries the [unavailable sentinel](/reference/api/retrieve-current-bid-and-ask#unavailable-prices). This check needs depth, so keep it on if you rely on it.

***

## 3. Size the Trade

Pick one:

* **Your own routing:** walk each pool's depth for your order size, as described in [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth).
* **Metric's routing:** call [Find best route](/reference/api/find-best-route). It runs the same depth walk across direct and one-hop paths and can split the order. Pass `poolAddress` to size the trade against a single pool.

Both give the same amounts for the same depth snapshot.

***

## 4. Execute the Trade

Use the [Router's](/reference/smart-contracts/swapping-via-router) `exactInput*` / `exactOutput*` entrypoints, or call [`swap` on the pool](/reference/smart-contracts/swapping-directly-via-pool).

* Supports both **exact-input** and **exact-output** swaps
* Uses the same [Q64 price format](/reference/glossary#price-formats-and-units) for bid, ask, and price limits
* [Price limits](/reference/glossary#pricelimitx64) protect execution against excessive slippage

Depth is an estimate. Set your protection from it:

* **Exact input:** `amountOutMinimum = estimatedOut × (1 − slippageTolerance)`
* **Exact output:** `amountInMaximum = estimatedIn × (1 + slippageTolerance)`

For the exact amount, quote with `MetricOmmSwapQuoter` before sending. Pools with extensions can refuse a trade that depth shows as fillable. To catch this, simulate the transaction with your real `from` address (see [Why a swap reverts](/integrate/integrate-metric-liquidity#why-a-swap-reverts)).

***

On Ethereum, where to send the transaction matters; see [Freshness and ordering](/how-pools-work/freshness-and-ordering#for-integrators).


# Via smart contracts

## 1. Find pools

Read pool, token and quoter addresses from the [metadata endpoint](/reference/api/metadata). Contract addresses are on [Contract addresses](/reference/supported-chains-and-deployments).

## 2. Quote

Call `MetricOmmSwapQuoter` with `eth_call`, never as a transaction, and leave `from` empty. See [Get quote](/reference/smart-contracts/get-quote). On Ethereum, a Live quote reads the last update already onchain, which lags; see [Freshness and ordering](/how-pools-work/freshness-and-ordering#for-integrators).

To price a pool from a contract, don't call the quoter or the Price Provider: restricted providers refuse both outside a swap. Call `swap` instead and check the amounts in your callback; see [Check before you pay](/reference/smart-contracts/swapping-directly-via-pool#check-before-you-pay).

## 3. Swap

Use the [router](/reference/smart-contracts/swapping-via-router)'s `exactInput*` / `exactOutput*` entrypoints, or call [`swap` on the pool](/reference/smart-contracts/swapping-directly-via-pool) from a contract that implements the swap callback. Set `amountOutMinimum` or `amountInMaximum`, and a deadline.

## 4. Submit

On Ethereum, where to send the transaction matters; see [Freshness and ordering](/how-pools-work/freshness-and-ordering#for-integrators).


# Estimate a fill from depth

Price an order from bid/ask depth, including the bin it ends in

The `depth` in [Retrieve pool bid and ask](/reference/api/retrieve-current-bid-and-ask) and [Retrieve pool batch bid and ask](/reference/api/retrieve-batch-bid-and-ask) lets you size an order yourself, without a request per amount. Each level is one bin, with the cumulative input and output to walk through its end; see [Depth levels](/reference/api/retrieve-current-bid-and-ask#depth-levels) for the fields.

[Find best route](/reference/api/find-best-route) runs this same calculation: it walks the depth, then prices the bin an order ends in as in [Exact fill inside a bin](#exact-fill-inside-a-bin). So for the same snapshot, the walk below with the exact fill returns what Find best route returns.

## Walking the depth

Pick the side from the direction: **bids** to sell token0, **asks** to buy token0. Then walk its levels in order, turning the cumulative columns into per-bin amounts:

```
// Exact input: X is what you pay. Returns what you receive.
remaining = X;  received = 0
prevIn = 0;  prevOut = 0
for level in side:
    if level.cumulativeInputVolume <= prevIn: continue   // adds no input: skip
    binIn  = level.cumulativeInputVolume - prevIn
    binOut = level.cumulativeVolume      - prevOut
    prevIn = level.cumulativeInputVolume;  prevOut = level.cumulativeVolume
    if binOut == 0: continue                              // nothing to receive here
    if remaining < binIn:                                 // the order ends in this bin
        return received + fillInside(level.price, binIn, binOut, remaining)
    received  += binOut
    remaining -= binIn
    if remaining == 0: return received
// out of levels: the order is larger than this side's depth
```

`fillInside` prices the part of the bin the order ends in. The simplest choice is the bin's proportional share, `floor(binOut × remaining / binIn)`; for the pool's own fill, use [Exact fill inside a bin](#exact-fill-inside-a-bin).

For **exact output** (`X` is what you receive), swap the roles: pay each whole bin's `binIn` while `remaining >= binOut`, and price the bin the order ends in with `payInside(level.price, binIn, binOut, remaining)`. Its proportional share is `ceil(binIn × remaining / binOut)`; the exact version is in [Exact fill inside a bin](#exact-fill-inside-a-bin).

Use BigInt throughout and multiply before dividing: the proportional products can exceed 2^128.

The average fill price is `received / X` (exact input) or `X / paid` (exact output), in raw units. The proportional share is a lower bound: the start of a bin fills better than its average (see [How the price moves inside a bin](#how-the-price-moves-inside-a-bin)), so it can understate an order that ends inside a bin by up to half of that bin's remaining width. That matters only on pools with wide bins. A small order in a 6.25% bin fills about 3% better; in a 0.01% bin, less than 0.005% better.

**Example:** selling 5 WETH (`X = 5000000000000000000`) into the bids of the [example response](/reference/api/retrieve-current-bid-and-ask#response), a WETH/USDC pool (WETH has 18 decimals, USDC 6):

```json
"bids": [
  { "binIdx": -1, "price": "49937913286041055521250", "cumulativeVolume": "7430706558",  "cumulativeInputVolume": "2744786401520641763" },
  { "binIdx": -2, "price": "49932918995283375647711", "cumulativeVolume": "21731301391", "cumulativeInputVolume": "8027598362225637937" }
]
```

1. The first bin takes all `2744786401520641763` of its input and returns `7430706558` USDC.
2. The remaining `2255213598479358237` ends inside the second bin. That bin has `5282811960704996174` input and `14300594833` output, so its proportional share returns `floor(14300594833 × 2255213598479358237 / 5282811960704996174) = 6104872967`.
3. In total you receive `13535579525` (13,535.58 USDC), an average of 2,707.12 USDC per WETH. The [exact fill](#exact-fill-inside-a-bin) returns `13535754492`.

## How the price moves inside a bin

Depth is exact at bin boundaries. Inside a bin, the pool moves its price in a straight line with the output the bin pays, from the bin's opening price to its closing `price`. So the bin's average price, `binIn / binOut`, sits exactly halfway between the two, and an order that stops partway through pays only the bin's earlier, better prices.

`binIn` and `binOut` are the bin's full size: what crossing all of it costs and pays out. The input `r` or output `o` still to fill when the order reaches the bin is how far into it the order gets.

For example, take an ask bin that pays out 100 for 100, with a closing price of 1.1. Its average price is 1.0, so it opens at 0.9. Buying the first 50 costs 47.5, the average of 0.9 and 1.0 times 50, not the proportional 50.

Depth carries only the closing price, so the exact fill below first recovers the opening price from it, then prices the part of the bin the order takes along that line.

## Exact fill inside a bin

The proportional share stays valid as a lower bound. For what the pool fills inside the bin an order ends in, which is what [Find best route](/reference/api/find-best-route) returns for the same snapshot, use the calculation below as `fillInside` and `payInside` in the walk. Every bin before it is crossed whole.

No estimate from depth can match the pool to the unit. Depth prices and amounts are rounded, and in the bin the pool's price currently sits in, depth leaves out the part already traded, which the pool's own sell-side solve uses. So this calculation gives up a small margin: for exact input it never returns more than the pool pays, and for exact output it never asks less than the pool charges.

It takes the bin's `binIn`, `binOut` and closing `price` from the walk, the input `r` still to fill (exact input) or the output `o` still to receive (exact output), and `dec0`/`dec1`, the `token0Decimals`/`token1Decimals` from [`/metadata`](/reference/api/metadata). The products exceed 2^512, so use arbitrary-precision integers.

The whole calculation, as pseudo-code. Each step is explained below, with its formula.

```
// The bin's curvature: what all of binOut would cost at the closing price, minus binIn.
asks: closeIn = floor(binOut × price × 10^dec1 / (2^64 × 10^dec0))
bids: closeIn = floor(binOut × 2^64 × 10^dec0 / (price × 10^dec1))
E = closeIn − binIn
// E <= 0, E > binIn on asks, or E >= binIn on bids: use the proportional share.

// The bin's lowest price, which sizes the margin for the pool's price rounding.
low = asks: floor(price × (binIn − E) / (binIn + E));  bids: price
margin(a) = ceil(2 × a / low) + 1

// What the pool charges for output o of this bin.
askIn(o) = ceil(o × ((binIn − E) × binOut + E × o) / binOut²)
bidIn(o) = ceil(o × (binIn − E) × ((binIn + E) × binOut − E × o) / (binOut × ((binIn + E) × binOut − 2 × E × o)))

// Exact input, asks: solve (binIn − E)·x + E·x² = r with 64 fractional bits, root rounded up.
S   = 2^64 × (binIn − E)
out = floor(2^64 × 2 × r × binOut / (S + ceilSqrt(S² + 2^128 × 4 × E × r)))
// Exact input, bids: fill at the opening price, then correct once, as the pool does.
g   = min(floor(binOut × r / (binIn − E)), binOut)
out = bidIn(g) <= r ? g : floor(g × r / bidIn(g))
fillInside(r) = clamp(out − margin(out), floor(binOut × r / binIn), binOut)

// Exact output: askIn on asks, bidIn on bids.
payInside(o) = min(in(o) + margin(in(o)), ceil(binIn × o / binOut))
```

Below, $$I$$ is `binIn`, $$O$$ is `binOut`, $$p$$ is `price`, and $$d\_0$$, $$d\_1$$ are `dec0`, `dec1`. Every $$\lfloor\cdot\rfloor$$ and $$\lceil\cdot\rceil$$ is integer division rounded down or up, and $$\lceil\sqrt{\cdot},\rceil$$ is the integer square root rounded up.

**1. The bin's curvature.** $$\mathrm{closeIn}$$ is what the whole bin would cost if every unit were charged the closing price, and $$E$$ is how much more that is than what the bin really costs:

$$
\mathrm{closeIn} =
\begin{cases}
\left\lfloor \dfrac{O \cdot p \cdot 10^{d\_1}}{2^{64} \cdot 10^{d\_0}} \right\rfloor & \text{asks} \\\[2ex]
\left\lfloor \dfrac{O \cdot 2^{64} \cdot 10^{d\_0}}{p \cdot 10^{d\_1}} \right\rfloor & \text{bids}
\end{cases}
\qquad\qquad
E = \mathrm{closeIn} - I
$$

The `2^64` and decimals only convert the level's fixed-point `price` into raw token units. Per unit of output, the bin then costs $$(I + E)/O$$ at its close, $$I/O$$ on average and $$(I - E)/O$$ at its open: $$E$$ is how far the closing cost sits above the average, and the opening cost sits the same distance below it. When $$E = 0$$ the price is flat and the proportional share is exact. In the 100-for-100 bin above, $$\mathrm{closeIn} = 110$$ and $$E = 10$$.

If $$E \le 0$$, $$E > I$$ on asks, or $$E \ge I$$ on bids, the depth doesn't describe a straight-line bin, so use the proportional share instead.

**2. The safety margin.** The pool rounds each price inside a bin to a whole unit of its 64-bit fixed-point grid, so its result can differ from these formulas by a few units. The estimate gives those units up, so that it never returns more than the pool pays or asks less than it charges. A grid step matters most where the price is lowest, so the margin is sized at the bin's lowest price, $$p\_{\text{low}}$$: the opening price on asks, where the price rises through the bin, and the closing `price` on bids, where it falls:

$$
p\_{\text{low}} =
\begin{cases}
\left\lfloor \dfrac{p,(I - E)}{I + E} \right\rfloor & \text{asks} \\\[2ex]
p & \text{bids}
\end{cases}
\qquad\qquad
\mathrm{margin}(a) = \left\lceil \frac{2a}{p\_{\text{low}}} \right\rceil + 1
$$

$$a$$ is the result the margin applies to, the output for exact input or the input for exact output. The margin covers two grid steps of it, plus one unit for rounding the amount itself. At ordinary prices that's 2 units; it grows only on tokens priced at a tiny fraction of the other.

**3. What the pool charges** for output $$o$$ of this bin: $$o$$ times the average of the bin's opening cost and its cost at $$o$$, rounded up. On asks the cost per unit rises in a straight line. On bids the pool's price, output per input, falls in a straight line, so the cost per unit curves and the formula is longer:

$$
\mathrm{askIn}(o) = \left\lceil \frac{o,\big((I - E),O + E,o\big)}{O^2} \right\rceil
$$

$$
\mathrm{bidIn}(o) = \left\lceil \frac{o,(I - E),\big((I + E),O - E,o\big)}{O,\big((I + E),O - 2E,o\big)} \right\rceil
$$

Both are 0 at $$o = 0$$, $$I$$ for the whole bin and the proportional share when $$E = 0$$.

**4. Exact input.** Here the order fixes what you pay and the formula finds what you receive, which runs step 3 backwards. On **asks**, solve $$(I - E),x + E,x^2 = r$$ with 64 fractional bits, rounding the root up:

$$
S = 2^{64},(I - E)
\qquad\qquad
\mathrm{out} = \left\lfloor \frac{2^{65}, r, O}{S + \left\lceil \sqrt{S^2 + 2^{130}, E, r},\right\rceil} \right\rfloor
$$

On **bids**, fill at the opening price, then correct once, as the pool does:

$$
g = \min!\left( \left\lfloor \frac{O, r}{I - E} \right\rfloor,\ O \right)
\qquad\qquad
\mathrm{out} =
\begin{cases}
g & \text{if } \mathrm{bidIn}(g) \le r \\\[1ex]
\left\lfloor \dfrac{g, r}{\mathrm{bidIn}(g)} \right\rfloor & \text{otherwise}
\end{cases}
$$

Then take off the margin, and keep the result between the proportional share and the whole bin:

$$
\mathrm{fillInside}(r) = \min!\left( \max!\left( \mathrm{out} - \mathrm{margin}(\mathrm{out}),\ \left\lfloor \frac{O, r}{I} \right\rfloor \right),\ O \right)
$$

**5. Exact output.** Here the order fixes what you receive, so step 3 prices it directly. With $$\mathrm{in} = \mathrm{askIn}$$ on asks and $$\mathrm{bidIn}$$ on bids, add the margin, and never ask more than the proportional share:

$$
\mathrm{payInside}(o) = \min!\left( \mathrm{in}(o) + \mathrm{margin}\big(\mathrm{in}(o)\big),\ \left\lceil \frac{I, o}{O} \right\rceil \right)
$$

In the [WETH example](#walking-the-depth), the second bin gives `closeIn = 5283076140833714739`, so `E = 264180128718565` and $$p\_{\text{low}}$$ is the level's `price`. Then `g = 6105178272`, and `bidIn(g) = 2255261744665388318` is more than `r`, so `out = floor(g × r / bidIn(g)) = 6105047936`. The margin is `2`, so the bin returns `6105047934` instead of `6104872967`. In total you receive `13535754492` (13,535.75 USDC).

## Before you execute

Depth is a snapshot for estimation, and the pool moves with every trade and price update. Never execute an amount estimated from depth with zero slippage. For an exact amount, quote with `MetricOmmSwapQuoter` before executing.


# API

**Base URL:** `https://api.metric.xyz`

## Where the API runs

The API backend runs on AWS in Tokyo (`ap-northeast-1`), across three availability zones. Requests reach it through Cloudflare, so they enter at the Cloudflare edge nearest to you. For the lowest latency, run your client in or near `ap-northeast-1`.

## Authentication

The maker quote endpoint requires a Bearer token:

{% code overflow="wrap" %}

```
Authorization: Bearer <your_trading_api_key>
```

{% endcode %}

{% hint style="info" %}
Trading API keys are issued by the Metric team: [contact us](mailto:contact@metric.xyz) to by email, or make a ticket in [Discord](https://discord.com/invite/metricxyz) to request one.
{% endhint %}

Public endpoints (chains, metadata, routing, single-pool and batch bid/ask) require no authentication. On routing and bid/ask, a key is optional: it raises your rate limit.

{% hint style="warning" %}
If you send an `Authorization` header to a public endpoint, the key must be valid. An unknown or placeholder key returns `401 {"error": "Invalid API key"}` even where no key is needed. Leave the header out if you don't have a key.
{% endhint %}

***

## Supported chains

Retrieve the list of supported chains and their numeric EVM chain IDs:

{% code overflow="wrap" %}

```
GET /public/v1/chains
```

{% endcode %}

Use the returned `chainId` values as the `{chain_id}` path parameter in all chain-specific endpoints.


# Metadata

`GET /public/v1/evm/{chain_id}/metadata`

Returns the funded pools on a chain together with their onchain metadata, token details, fiat prices and pool state. By default only pools with at least $10 of TVL are listed; pools whose TVL can't be computed are hidden too. Pass `minTvlFiat=0` to list every pool. Results are paginated.

This is a public endpoint. Authentication is not required.

### Path parameters

| Name       | Type    | Description                                                                                                                                                                                        |
| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chain_id` | integer | EVM chain ID, e.g. `1`. Must be one of the chains returned by `GET /public/v1/chains`. An unsupported chain returns `404 {"error": "Unknown chainId: 999999"}`; a non-numeric value returns `400`. |

### Query parameters

All optional. Unknown parameters are ignored.

| Name                      | Type    | Default  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `count`                   | integer | `50`     | Page size, maximum `500` (larger values are capped). `0` is treated as `1`. Page through with `offset` to get the full list.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `offset`                  | integer | `0`      | Index of the first pool to return. Use `nextOffset` from the previous page. An offset past the end returns an empty `data` array.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `sortBy`                  | string  | `tiered` | One of `tiered`, `tvl`, `created`, `volume`, `volume7d`, `volume30d`, `volumeAll`, `apr`, `apr7d`, or a Markout APR sort (case-insensitive). `volume` ranks by 24h volume (`volume24h` also works); `volume7d`, `volume30d` and `volumeAll` rank by `volumeByWindow.7d`, `.30d` and `.all`. Volume sorts break ties by TVL. A Markout APR sort is `markoutApr` followed by a window (`24h`, `7d`, `30d`, `All`) and a horizon (`1m`, `5m`, `30m`), such as `markoutApr24h1m` or `markoutAprAll30m`, and ranks by `markoutByWindow.<window>.byHorizon.<horizon>.aprBps`. The 7d 5m sort is `markoutApr7d` (`markoutApr7d5m` also works). A Recommended sort is `recommended` followed by the same window and horizon, such as `recommended7d5m`: pools stay grouped by tier and rank inside each tier by TVL, volume and Markout APR over that window together. Any other value returns `400`. |
| `sortOrder`               | string  | `desc`   | `asc` or `desc`. Any other value returns `400`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `include24h`              | boolean | `false`  | `true` adds `tvlFiat` and `analytics24h` to every pool. Must be exactly `true` or `false`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `minTvlFiat`              | number  | `10`     | Minimum pool TVL in USD. Pools whose TVL can't be computed are dropped while a floor is set. `0` disables the filter.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `tier`                    | string  | all      | Comma-separated subset of `green`, `yellow`, `red`, e.g. `tier=green,yellow`. Any other value returns `400`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `lockedPricingOnly`       | boolean | `false`  | `true` keeps only pools whose Price Provider can never be changed (`lockedPricing: true`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `depositAllowlistEnabled` | boolean | `false`  | `true` keeps only pools with deposit allowlisting enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `verifiedCuratorOnly`     | boolean | `false`  | `true` keeps only pools run by a verified curator: Metric-operated (`verifiedAdmin: true`) or identity-verified by Metric (no `curator_unverified` in `reasonCodes`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `blacklistedOnly`         | boolean | `false`  | `true` returns only pools delisted by Metric (`tierQualification: "black"`). No default TVL floor applies.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `addresses`               | string  | none     | Comma-separated pool addresses, at most `500`. Returns only those pools. See [Look up specific pools](#look-up-specific-pools).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `contract_version`        | string  | all      | Keep only pools of this contract release, e.g. `v1`. An unknown or disabled release returns `404`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `fiat`                    | string  | `USD`    | Fiat currency for prices. Only `USD` is supported; any other value returns `400`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

The cross-chain endpoint `GET /public/v1/evm/metadata` takes the same parameters except `sortBy=created`, which is per-chain only, plus `chainIds` (comma-separated chain IDs, e.g. `chainIds=1,56`) to limit it to some chains. A non-numeric entry returns `400`; an unknown chain ID returns no rows.

### Pagination

Read `nextOffset` from the response and pass it back as `offset` to fetch the next page. `nextOffset` is `null` on the last page. `total` is the full count across all pages.

### Look up specific pools

If you already know which pools you want, for example the pools a wallet holds positions in, pass them in `addresses` instead of requesting each pool on its own or paging through the full list:

```bash
curl "https://api.metric.xyz/public/v1/evm/1/metadata?include24h=true&addresses=0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24"
```

* Up to `500` addresses, comma-separated. Case doesn't matter and duplicates are ignored.
* Only the pools you name are returned. A pool that isn't listed (an unknown address, a pool delisted by Metric, or one created since the list was last refreshed) is left out rather than returning an error.
* The default `$10` TVL floor doesn't apply, so a small pool you name is still returned. If you pass `minTvlFiat` yourself it applies, as do `tier`, `lockedPricingOnly`, `depositAllowlistEnabled` and `verifiedCuratorOnly`.
* `sortBy`, `sortOrder`, `count` and `offset` work as on the full list, and `total` counts only the pools returned. The default `count` is `50`, so pass a larger `count` when naming more than 50 pools.
* On the cross-chain endpoint, a pool address found on more than one chain returns one row per chain. Use `chainIds` to limit the chains searched.
* A malformed address, an empty value, more than `500` addresses, or `addresses` together with `blacklistedOnly=true` returns `400`.

### Request

```bash
curl "https://api.metric.xyz/public/v1/evm/1/metadata?count=1&sortBy=tvl&include24h=true"
```

```bash
curl "https://api.metric.xyz/public/v1/evm/1/metadata?count=50&offset=50"
```

### Response

`200 OK`: a `PaginatedMetadataResponse` envelope.

```json
{
  "data": [
    {
      "pair": "WETH/USDC",
      "chain": "ethereum",
      "contractVersion": "v1",
      "poolAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
      "priceProviderAddress": "0xa07A8Aa6D5311eCfc01C01eC9Eefd17b0A8A9445",
      "priceProviderDetails": {
        "providerType": "anchored",
        "oracle": "0x30a88bf894D1b806125a23ebf23D715496fD7102",
        "feedMode": "synthetic",
        "marginStep": "0",
        "minMargin": "5000000000000",
        "maxRefStaleness": "15",
        "maxSpreadMult": "1000000000000000000",
        "mutableParams": false,
        "source": "0x599b0027942fC98BadC121cB789fE0D4a11A0879",
        "confidenceParam": null,
        "baseMaxStaleness": "15",
        "quoteMaxStaleness": "15",
        "baseSpreadMult": "0",
        "quoteSpreadMult": "0"
      },
      "token0": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      "token1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "token0Symbol": "WETH",
      "token1Symbol": "USDC",
      "token0Name": "WETH",
      "token1Name": "USDC",
      "token0Decimals": 18,
      "token1Decimals": 6,
      "token0LogoUrl": "https://cdn.metric.xyz/tokens/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png",
      "token1LogoUrl": "https://cdn.metric.xyz/tokens/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png",
      "token0BrandColor": "#101010",
      "token1BrandColor": "#0050b0",
      "token0FiatPrice": 2709.4,
      "token1FiatPrice": 0.999853,
      "tvlFiat": 115757.94,
      "creator": "0x80f0A7d148729CcEdb6cD07FBaAEfC5A16f33D4f",
      "admin": "0x80f0A7d148729CcEdb6cD07FBaAEfC5A16f33D4f",
      "adminUsername": "Carcosa",
      "verifiedAdmin": true,
      "adminFeeDestination": "0x53F0ACE6156DAF7F6b50B395F6DD66C9d2f50c8c",
      "spreadProtocolFeeE6": 150000,
      "spreadAdminFeeE6": 200000,
      "protocolNotionalFeeE8": 0,
      "adminNotionalFeeE8": 0,
      "analytics24h": {
        "fromTs": 1790422039,
        "toTs": 1790508439,
        "generatedAtTs": 1790508439,
        "fiatAvailable": true,
        "fiatCoveragePct": 100,
        "fiatStatus": "active",
        "fiatCurrency": "USD",
        "fromBlock": 26061325,
        "toBlock": 26068435,
        "swapTxCount": 493,
        "liquidityEventCount": 0,
        "liquidityAddEventCount": 0,
        "liquidityRemoveEventCount": 0,
        "transfers": 493,
        "token0": {
          "liquidityRaw": "24431277185857724312",
          "volumeRaw": "287082472219069397624",
          "inflowRaw": "144190709723083426314",
          "outflowRaw": "142891762495985971310",
          "netRaw": "1298947227097455004",
          "liquidityFiat": 66194.1,
          "volumeFiat": 777821.25,
          "inflowFiat": 390670.31,
          "outflowFiat": 387150.94,
          "netFiat": 3519.37
        },
        "token1": {
          "liquidityRaw": "49571124074",
          "volumeRaw": "773469090135",
          "inflowRaw": "385002290049",
          "outflowRaw": "388466800086",
          "netRaw": "-3464510037",
          "liquidityFiat": 49563.84,
          "volumeFiat": 773355.39,
          "inflowFiat": 384945.69,
          "outflowFiat": 388409.7,
          "netFiat": -3464
        },
        "pricingMode": "estimated_spot",
        "usdCoveragePct": 0,
        "aprBps": 843,
        "feesFiat": 26.89,
        "tvlFiat": 115757.94,
        "volumeFiat": 775588.32
      },
      "aprBps7d": 2805,
      "lockedPricing": true,
      "pauseLevel": 0,
      "priceProviderCooldownSecs": 18446744073709551615,
      "depositWhitelistingEnabled": false,
      "createdBlockNumber": 25984907,
      "createdTimestamp": 1789499963,
      "priceProviderSinceBlock": 25984907,
      "extensions": [
        "0x599b0027942fC98BadC121cB789fE0D4a11A0879",
        "0xE4038fA09f0bB9963068afaF97BE0c045155090D",
        "0xEbc53e61078976118E384f110c262A263decB84B"
      ],
      "extensionDetails": {
        "stopLoss": {
          "phases": ["afterSwap"],
          "drawdownE6": "150000",
          "decayPerSecondE18": "1736111111111",
          "timelockSecs": "3600"
        },
        "priceVelocity": {
          "phases": ["beforeSwap"],
          "maxChangePerBlockE18": "100000000000000000"
        }
      },
      "classificationSummary": {
        "tierQualification": "red",
        "reasonCodes": ["stop_loss_mutable"]
      },
      "poolFactoryAddress": "0x2a53833cc95548cf52c7b159110e22D3a9018f32",
      "poolDeployerAddress": "0x2E0A99A2E352043d1CA489cF45bf7B511BCcC738",
      "liquidityAdderAddress": "0x06Cb6C245Cd9219262442A7730D9484B3CfdD901",
      "dataProviderAddress": "0x83d66c5934b1525ee82e6E772c76c7acC778bBB7",
      "depositAllowlistAddress": "0x65FBa4Ee8f50772aB384f19e5520DdA420B1B47D",
      "oracleValueStopLossAddress": "0xE4038fA09f0bB9963068afaF97BE0c045155090D",
      "priceVelocityGuardAddress": "0xEbc53e61078976118E384f110c262A263decB84B",
      "quoterAddress": "0x3B9ea79f482094323d06064E637a7F4528560fcb",
      "priceProviderStatus": "healthy"
    }
  ],
  "total": 9,
  "nextOffset": 1
}
```

{% hint style="warning" %}
`priceProviderCooldownSecs` can be `18446744073709551615` (2^64 − 1), which is larger than JavaScript's `Number.MAX_SAFE_INTEGER`. `JSON.parse` silently rounds it to `18446744073709552000`. Parse it as a BigInt, or treat any value ≥ 2^53 as "never".
{% endhint %}

### Pool fields

#### Identity

| Field                | Type    | Description                                                                                                                                       |
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pair`               | string  | Display pair, `token0Symbol/token1Symbol`.                                                                                                        |
| `chain`              | string  | Chain name as returned by `GET /public/v1/chains`. Casing varies by chain (`ethereum`, `BSC`, `Robinhood`); key on the chain ID, not this string. |
| `contractVersion`    | string  | Contract release, currently `v1`.                                                                                                                 |
| `poolAddress`        | address | The pool.                                                                                                                                         |
| `createdBlockNumber` | integer | Block the pool was created in.                                                                                                                    |
| `createdTimestamp`   | integer | Unix seconds of that block.                                                                                                                       |

#### Tokens

| Field                                  | Type           | Description                                                   |
| -------------------------------------- | -------------- | ------------------------------------------------------------- |
| `token0`, `token1`                     | address        | Pool tokens, in pool order.                                   |
| `token0Symbol`, `token1Symbol`         | string         | ERC-20 symbols. Optional.                                     |
| `token0Name`, `token1Name`             | string         | ERC-20 names. Optional.                                       |
| `token0Decimals`, `token1Decimals`     | integer        | ERC-20 decimals. Optional.                                    |
| `token0LogoUrl`, `token1LogoUrl`       | string         | Token logo URL. Optional.                                     |
| `token0BrandColor`, `token1BrandColor` | string         | Hex color for UI use. Optional.                               |
| `token0FiatPrice`, `token1FiatPrice`   | number \| null | USD price. Always present; `null` when no price is available. |

#### Price provider

| Field                       | Type    | Description                                                                                                                                        |
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `priceProviderAddress`      | address | The pool's active Price Provider.                                                                                                                  |
| `priceProviderDetails`      | object  | Provider configuration. Optional: omitted when the provider wasn't deployed by a Metric provider factory, or its type isn't recognized. See below. |
| `priceProviderStatus`       | string  | Current health of the pool's price, from the last price capture. See [Price provider status](#price-provider-status) below.                        |
| `priceProviderSinceBlock`   | integer | Block since which the current provider has been active.                                                                                            |
| `priceProviderCooldownSecs` | integer | Cooldown on changing the pool's provider. `2^64 − 1` means it can never be changed (see the warning above).                                        |
| `lockedPricing`             | boolean | `true` when the provider can never be changed.                                                                                                     |
| `priceProviderChange`       | object  | Pending provider change. Optional: present only while one exists.                                                                                  |

`priceProviderDetails` for an **anchored** provider (`providerType: "anchored"`): `oracle`, `feedMode` (`single` or `synthetic`), `source`, `marginStep`, `minMargin`, `maxRefStaleness`, `maxSpreadMult`, `mutableParams`, `confidenceParam`, `baseMaxStaleness`, `quoteMaxStaleness`, `baseSpreadMult`, `quoteSpreadMult`.

For a **regular** provider (`providerType: "regular"`): `oracle`, `marginStep`, `maxTimeDelta`, `confidenceParam`.

Numeric values inside `priceProviderDetails` are decimal strings, and any of them may be `null`. `marginStep` is signed and can be negative. `source` is omitted when the provider has no custom source (reference mode).

#### Price provider status

| Value          | Meaning                                                                                                                                              |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `healthy`      | The provider returned a usable price.                                                                                                                |
| `feed_down`    | The feed has no valid price right now.                                                                                                               |
| `pool_refused` | The feed quoted, but the pool refuses to trade on that quote.                                                                                        |
| `pool_paused`  | The pool is paused and its price read otherwise succeeded. A paused pool whose feed is down shows `feed_down`; check `pauseLevel` to detect a pause. |
| `unknown`      | No recent reading for this pool.                                                                                                                     |

Only `healthy` means the pool is quotable. The status comes from Metric's periodic price capture, about every 30 seconds, so it can be out of date by the time you trade. A pool with no recent capture shows `unknown` after about 90 seconds. Use the status to filter out pools that are down, and check [bid and ask](/reference/api/retrieve-current-bid-and-ask#before-you-trade) right before trading.

#### Curator and fees

| Field                   | Type            | Description                                                                                                                                                                                                                                                                                                                                     |
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `creator`               | address         | Address that created the pool.                                                                                                                                                                                                                                                                                                                  |
| `admin`                 | address         | Current pool admin (curator). Optional.                                                                                                                                                                                                                                                                                                         |
| `adminUsername`         | string \| null  | Curator's display name; `null` when none is set.                                                                                                                                                                                                                                                                                                |
| `verifiedAdmin`         | boolean         | Whether the curator is verified by Metric.                                                                                                                                                                                                                                                                                                      |
| `adminFeeDestination`   | address         | Where curator fees are paid.                                                                                                                                                                                                                                                                                                                    |
| `spreadProtocolFeeE6`   | integer \| null | Protocol share of the spread, 1e6 = 100%.                                                                                                                                                                                                                                                                                                       |
| `spreadAdminFeeE6`      | integer \| null | Curator share of the spread, 1e6 = 100%.                                                                                                                                                                                                                                                                                                        |
| `protocolNotionalFeeE8` | integer \| null | Protocol fee on notional, 1e8 = 100%.                                                                                                                                                                                                                                                                                                           |
| `adminNotionalFeeE8`    | integer \| null | Curator fee on notional, 1e8 = 100%.                                                                                                                                                                                                                                                                                                            |
| `aprBps7d`              | integer         | Trailing 7-day LP APR in basis points. Optional.                                                                                                                                                                                                                                                                                                |
| `tradingPnlAprBps7d`    | integer         | Trailing 7-day Trading P\&L APR in basis points, signed: the pool's swap-driven holdings change valued at its own rate, over the same capital as `aprBps7d`. Optional.                                                                                                                                                                          |
| `markoutByWindow`       | object          | The pool's markout: what LPs kept from each swap, checked against the price provider's reference price a fixed horizon later. A measurement of past swaps, not realized profit or a forecast. Keyed by window (`24h`, `7d`, `30d`, `all`), each with `fromTimestamp` and `byHorizon` keyed by horizon (`1m`, `5m`, `30m`). Optional. See below. |
| `volumeByWindow`        | object          | Pool swap volume in USD per window (`24h`, `7d`, `30d`, `all`), each `number \| null`, the same definition as `analytics24h.volumeFiat`. Optional.                                                                                                                                                                                              |

Each `markoutByWindow` window and horizon has:

| Field                  | Type            | Description                                                                                                                                                                                                                                                                        |
| ---------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markoutFiat`          | number \| null  | Markout in USD over `[fromTimestamp, throughTimestamp)`, summed over the swaps with a price at that horizon. Signed: positive means LPs kept money.                                                                                                                                |
| `aprBps`               | integer \| null | `markoutFiat`, annualized and divided by the liquidity of the bins that traded in the window, over the same span, in basis points. Signed. Liquidity in bins no swap reached is left out, unlike `aprBps7d`. It annualizes the window's past markout; it isn't a projected return. |
| `pricedVolumeShare`    | number \| null  | Share of the window's volume with a price at this horizon, 0–1.                                                                                                                                                                                                                    |
| `unpricedFills`        | integer         | Swaps (per bin they crossed) with no price at this horizon.                                                                                                                                                                                                                        |
| `tradedLiquidityShare` | number \| null  | Share of the pool's liquidity that `aprBps` divides by: the liquidity of the bins that traded ÷ the liquidity of every bin, over the same span, 0–1. `1` only when every bin that held liquidity traded.                                                                           |
| `tradedBins`           | integer \| null | Bins with at least one swap in the window.                                                                                                                                                                                                                                         |
| `fundedBins`           | integer \| null | Bins that held liquidity in the window, or had a swap in it.                                                                                                                                                                                                                       |
| `throughTimestamp`     | integer \| null | End of the window at this horizon, Unix seconds. Each horizon has its own, a few minutes (1m, 5m) to about 40 minutes (30m) behind now.                                                                                                                                            |

The figures are `null` until the window has 5 minutes of settled data, when no swap in it has a price, or when under 70% of its volume does. `aprBps` has no minimum liquidity, so a window where only a small bin traded can show a very large APR. A window where no bin traded has `markoutFiat` `0` and `aprBps` `null`. `tradedLiquidityShare` is `0` when the pool held liquidity and nothing traded, is served even when `markoutFiat` and `aprBps` are `null` (under 70% priced volume), and is `null` when no bin held liquidity, a token is unpriced, or the window has no settled data yet; `tradedBins` and `fundedBins` are `null` without settled data, when they can't be read, or when a bin's liquidity can't be computed (which also makes `tradedLiquidityShare` `null`).

The fee fields mirror the onchain `poolFeeConfig(pool)` on `MetricOmmPoolFactory`. For a pool not listed here (e.g. before it is funded), read that onchain.

#### State and access

| Field                        | Type    | Description                                                                                                                                               |
| ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pauseLevel`                 | integer | `0` = active. Any other value blocks swaps. Optional.                                                                                                     |
| `curBinIdx`                  | integer | The pool's current active bin. Can be negative. A position is in range when it holds shares in this bin. Returned with or without `include24h`. Optional. |
| `depositWhitelistingEnabled` | boolean | Deposits are restricted to an allowlist.                                                                                                                  |
| `depositAllowlistProvider`   | address | Allowlist that gates deposits. Optional: present only when `depositWhitelistingEnabled` is `true`.                                                        |

#### Extensions and classification

| Field                                     | Type       | Description                                                                                                                                                                                                                                                |
| ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `extensions`                              | address\[] | Every extension attached to the pool, including custom ones.                                                                                                                                                                                               |
| `extensionDetails`                        | object     | Parameters of the standard extensions the pool uses: `stopLoss` (`drawdownE6`, `decayPerSecondE18`, `timelockSecs`) and `priceVelocity` (`maxChangePerBlockE18`). Each lists the swap `phases` it runs in. Values are decimal strings or `null`. Optional. |
| `classificationSummary.tierQualification` | string     | `green`, `yellow` or `red`; `unclassified` when the pool hasn't been classified yet; `black` for a pool delisted by Metric (returned only with `blacklistedOnly=true`).                                                                                    |
| `classificationSummary.reasonCodes`       | string\[]  | Why the pool is not green. Empty for green pools. See [Reason codes](#reason-codes) below.                                                                                                                                                                 |

#### Reason codes

| Code                                                          | Meaning                                                                                                                           |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `bin_width_exceeded`                                          | Bins are wider than 1%.                                                                                                           |
| `stop_loss_missing`                                           | No StopLoss extension attached.                                                                                                   |
| `stop_loss_uninitialized`                                     | A StopLoss is attached but not initialized.                                                                                       |
| `stop_loss_mutable`                                           | The StopLoss settings can still be changed.                                                                                       |
| `stop_loss_drawdown_exceeded`                                 | StopLoss drawdown is above 15%.                                                                                                   |
| `stop_loss_decay_exceeded`                                    | StopLoss decay is above 15% per day.                                                                                              |
| `app_missing`                                                 | The Price Provider is not an Anchored Price Provider.                                                                             |
| `app_mutable`                                                 | The Anchored Price Provider's parameters can be changed.                                                                          |
| `feed_staleness_exceeded`                                     | A feed's staleness bound is above 15 seconds.                                                                                     |
| `price_provider_mutable`                                      | The pool's Price Provider can be changed.                                                                                         |
| `curator_unverified`                                          | The curator is not verified by Metric.                                                                                            |
| `curator_not_creator`                                         | The curator is not the pool's creator.                                                                                            |
| `remove_liquidity_extension_present`                          | An extension runs when liquidity is removed.                                                                                      |
| `anchored_feed_unmatched`                                     | The Anchored Price Provider's feeds can't be confirmed to price the pool's own tokens.                                            |
| `multiplier_feed_miswired`                                    | A Multiplier Oracle feed is bound to the wrong token or feed, uses an adapter that can't read the token, or skips the multiplier. |
| `multiplier_token_unscaled`                                   | A tokenized stock with an issuer multiplier is priced by an oracle other than the Multiplier Oracle.                              |
| `reference_source_unrecognized`, `external_source_unreviewed` | Reserved. Not emitted yet.                                                                                                        |

`anchored_feed_unmatched` drops the pool one tier. `multiplier_feed_miswired` and `multiplier_token_unscaled` make it `red` whatever else it meets. See [Pool tiers](/use-metric/pool-tiers#tokenized-stocks).

#### Contract addresses

`poolFactoryAddress`, `poolDeployerAddress`, `liquidityAdderAddress`, `dataProviderAddress`, `quoterAddress`, `depositAllowlistAddress`, `oracleValueStopLossAddress` and `priceVelocityGuardAddress` echo the deployment the pool belongs to. See [Contract addresses](/reference/supported-chains-and-deployments).

#### 24h analytics (`include24h=true` only)

| Field                                                                                                          | Type            | Description                                                                                                                                            |
| -------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tvlFiat`                                                                                                      | number          | Pool TVL in USD (same value as `analytics24h.tvlFiat`).                                                                                                |
| `analytics24h.fromTs` / `toTs` / `generatedAtTs`                                                               | integer         | Window bounds and generation time, Unix seconds.                                                                                                       |
| `analytics24h.fromBlock` / `toBlock`                                                                           | integer         | Block range of the window.                                                                                                                             |
| `analytics24h.swapTxCount`, `transfers`                                                                        | integer         | Swap and transfer counts in the window.                                                                                                                |
| `analytics24h.liquidityEventCount`, `liquidityAddEventCount`, `liquidityRemoveEventCount`                      | integer         | Liquidity events in the window.                                                                                                                        |
| `analytics24h.token0` / `token1`                                                                               | object          | Per token: `liquidityRaw`, `volumeRaw`, `inflowRaw`, `outflowRaw`, `netRaw` (raw token units, decimal strings) and the matching `…Fiat` values in USD. |
| `analytics24h.volumeFiat`, `feesFiat`, `tvlFiat`                                                               | number \| null  | 24h volume, 24h fees and current TVL, USD.                                                                                                             |
| `analytics24h.aprBps`                                                                                          | integer \| null | APR from the last 24h, basis points.                                                                                                                   |
| `analytics24h.fiatAvailable`, `fiatStatus`, `fiatCurrency`, `fiatCoveragePct`, `pricingMode`, `usdCoveragePct` | mixed           | How the fiat values were priced.                                                                                                                       |

### Omitted vs null

Optional fields are omitted entirely when absent; they are not returned as `null`. These optional fields are: `token0Symbol`, `token0Name` and `token0Decimals` (and their `token1` counterparts), logo and color fields, `admin`, `aprBps7d`, `tradingPnlAprBps7d`, `markoutByWindow`, `volumeByWindow`, `pauseLevel`, `priceProviderDetails`, `priceProviderDetails.source`, `priceProviderChange`, `depositAllowlistProvider`, `extensionDetails`, `curBinIdx`, and `tvlFiat` / `analytics24h` (only with `include24h=true`).

These fields are always present and may be `null`: `token0FiatPrice`, `token1FiatPrice`, `adminUsername`, the four fee fields (`spreadProtocolFeeE6`, `spreadAdminFeeE6`, `protocolNotionalFeeE8`, `adminNotionalFeeE8`), every numeric value inside `priceProviderDetails` and `extensionDetails`, and the fiat values in `analytics24h` (`aprBps`, `feesFiat`, `tvlFiat`, `volumeFiat`, `fiatCoveragePct` and each token's `…Fiat` values).

### Errors

| Status | When                                                                                                                                      |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Non-numeric `chain_id`, unsupported `sortBy`, `sortOrder`, `tier` or `fiat`, negative `count`, or `include24h` other than `true`/`false`. |
| `404`  | `chain_id` is not served by the API (`{"error": "Unknown chainId: <id>"}`), or `contract_version` is unknown or disabled.                 |

Most error bodies are JSON `{"error": "…"}`. A query parameter that can't be parsed at all (e.g. `count=abc`, `include24h=yes`) returns a plain-text body instead, such as `Failed to deserialize query string: …`.

Examples use the Ethereum WETH/USDC pool. Other pairs returned by this endpoint use the same conventions.


# Retrieve pool bid and ask

`GET /public/v1/evm/{chain_id}/{pool_address}/bid_ask`

Returns the pool's adjusted bid and ask with per-bin depth. These are not the raw provider quote: they already include the pool's spread and fees. See [Liquidity and bins](/how-pools-work/liquidity-and-bins) for pool pricing.

This is a public endpoint. A Trading API key is optional; sending one raises your rate limit:

```http
Authorization: Bearer <your_trading_api_key>
```

A key you send must be valid: an unknown key returns `401`. Leave the header out if you don't have one.

The `pool_address` is the unique identifier used throughout the API.

To quote many pools at once, use [Retrieve batch bid and ask](/reference/api/retrieve-batch-bid-and-ask) (up to 100 per request).

### Path parameters

| Name           | Type    | Description                                                                             |
| -------------- | ------- | --------------------------------------------------------------------------------------- |
| `chain_id`     | integer | EVM chain ID, e.g. `1`. Must be one of the chains returned by `GET /public/v1/chains`.  |
| `pool_address` | address | Pool address as returned by `GET /public/v1/evm/{chain_id}/metadata`. Case-insensitive. |

### Query parameters

All optional.

| Name    | Type    | Default | Description                                                                                                                                       |
| ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `depth` | boolean | `true`  | `false` returns `"depth": null` and skips the one-sided check (see [Unavailable prices](#unavailable-prices)). Must be exactly `true` or `false`. |

### Request

```bash
curl "https://api.metric.xyz/public/v1/evm/1/0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24/bid_ask"
```

### Response

`200 OK`. Depth is trimmed to two levels per side here.

```json
{
  "pair": "WETH/USDC",
  "poolAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
  "priceProviderStatus": "healthy",
  "bidAdj": "49940417631045696435290",
  "askAdj": "49945012360825322506424",
  "totalToken0Available": "24503618827789131821",
  "totalToken1Available": "49785157539",
  "serverTs": 1790508850,
  "depth": {
    "bids": [
      {
        "binIdx": -1,
        "price": "49937913286041055521250",
        "cumulativeVolume": "7430706558",
        "cumulativeInputVolume": "2744786401520641763",
        "priceImpactE6": "51"
      },
      {
        "binIdx": -2,
        "price": "49932918995283375647711",
        "cumulativeVolume": "21731301391",
        "cumulativeInputVolume": "8027598362225637937",
        "priceImpactE6": "151"
      }
    ],
    "asks": [
      {
        "binIdx": -1,
        "price": "49947502535663908680680",
        "cumulativeVolume": "2727338310371559766",
        "cumulativeInputVolume": "7384519505",
        "priceImpactE6": "49"
      },
      {
        "binIdx": 0,
        "price": "49952497285917475071548",
        "cumulativeVolume": "8169278436813730931",
        "cumulativeInputVolume": "22120177339",
        "priceImpactE6": "149"
      }
    ]
  }
}
```

Here `bidAdj / 2^64 = 2707.28` and `askAdj / 2^64 = 2707.52` USDC per WETH. The first bid level fills 2.74 WETH in for 7,430.71 USDC out.

### Response fields

| Field                  | Type           | Description                                                                                                                                                                    |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pair`                 | string         | Display pair, `token0Symbol/token1Symbol` (same as `/metadata`).                                                                                                               |
| `poolAddress`          | address        | The pool.                                                                                                                                                                      |
| `priceProviderStatus`  | string         | `healthy`, `feed_down`, `pool_refused`, `pool_paused`, `internal_error` or `unknown`. Check this before using any price (see [Unavailable prices](#unavailable-prices) below). |
| `bidAdj`               | string         | Best price for selling token0, Q64.64: token1 per whole token0 = `bidAdj / 2^64`.                                                                                              |
| `askAdj`               | string         | Best price for buying token0, same format.                                                                                                                                     |
| `totalToken0Available` | string \| null | The pool's token0 balance, raw token0 units. Includes fees not yet collected, so slightly more than LPs' liquidity.                                                            |
| `totalToken1Available` | string \| null | The pool's token1 balance, raw token1 units. Includes fees not yet collected, so slightly more than LPs' liquidity.                                                            |
| `serverTs`             | integer        | Server time of the snapshot, Unix seconds.                                                                                                                                     |
| `depth`                | object \| null | Per-bin depth: `bids` (you sell token0) and `asks` (you buy token0). `null` when `depth=false`.                                                                                |

All prices and amounts are decimal strings. Parse them as BigInt, not Number.

#### Depth levels

Each entry in `bids[]` / `asks[]` is one pool bin, ordered outward from the current price.

| Field                   | Type    | Description                                                                                                                                                                                                                                                                                                             |
| ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `binIdx`                | integer | The pool's own bin index, not a level number. Both sides start at the current bin, which is partly consumed. Bids walk down (−1, −2, …) and asks walk up (−1, 0, 1, …).                                                                                                                                                 |
| `price`                 | string  | Marginal price at the far edge of this bin, Q64.64 (same format as `bidAdj`).                                                                                                                                                                                                                                           |
| `cumulativeVolume`      | string  | Total **output** you receive by walking through the end of this bin, in raw native units. Bids output token1; asks output token0.                                                                                                                                                                                       |
| `cumulativeInputVolume` | string  | Total **input** you pay to walk through the end of this bin, in raw native units. Bids take token0; asks take token1.                                                                                                                                                                                                   |
| `priceImpactE6`         | string  | Distance of `price` from `bidAdj` (bids) or `askAdj` (asks), in millionths, unsigned. `49` ≈ 0.005%. This is the marginal price at the bin's far edge, not the average price of an order that reaches it. For that, see [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth). |

| Side   | You         | Input token | Output token |
| ------ | ----------- | ----------- | ------------ |
| `bids` | sell token0 | token0      | token1       |
| `asks` | buy token0  | token1      | token0       |

The last level's `cumulativeVolume` approaches the side's total: `totalToken1Available` for bids, `totalToken0Available` for asks, unless a guard cuts that side short.

How the list ends:

* **Bins past the pool's liquidity** repeat the previous cumulative values and add no volume. Ignore them.
* **A guard such as StopLoss** can stop a side early, at the first bin the pool won't fully fill. If the pool still fills part of that bin, the side ends with one extra level for it: its cumulative values include only the part that fills, and its `price` is where that partial fill stops, not the bin's far edge. If nothing in that bin fills, the side ends at the previous level.

### Estimating a fill

Use `depth` to estimate what an order fills for: walk a side's levels, crossing each bin whole until the order runs out, and price the bin it ends in. The bin's proportional share, `floor(binOut × remaining / binIn)`, is the simple choice and a lower bound. The pool moves its price linearly inside a bin, so on wide bins an order that ends inside one fills up to half that bin's remaining width better: about 3% in a 6.25% bin. [Find best route](/reference/api/find-best-route) prices that bin along the pool's curve.

For the full walk, a worked example and the exact fill inside a bin, see [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth).

### Unavailable prices

When the pool can't be quoted, the endpoint still returns `200` with sentinel values:

```json
{
  "priceProviderStatus": "feed_down",
  "bidAdj": "0",
  "askAdj": "340282366920938463463374607431768211455",
  "depth": { "bids": [], "asks": [] }
}
```

`bidAdj = 0`, `askAdj = 2^128 − 1` and empty depth mean the pool is not quotable. `priceProviderStatus` says why:

| Value            | Meaning                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `healthy`        | The pool is quotable (but see the one-sided case below).                                                       |
| `feed_down`      | The feed has no valid price right now.                                                                         |
| `pool_refused`   | The feed quoted, but the pool refuses to trade on that quote, on both sides.                                   |
| `pool_paused`    | The pool is paused.                                                                                            |
| `internal_error` | Metric failed to read the price on its own side; this does not mean the pool's feed is unhealthy. Retry later. |
| `unknown`        | No status could be determined.                                                                                 |

Treat any `priceProviderStatus` other than `healthy` as "do not trade".

One side can be closed while the status stays `healthy`. For some pools the pool refuses trades in one direction only; that side then gets its sentinel (`bidAdj = "0"` or `askAdj = "340282366920938463463374607431768211455"`) while the other side keeps a real price. Check each side's value, not only the status. This check only runs when `depth=true`: with `depth=false`, a side the pool refuses can still show a real price next to `healthy`, so request depth if you rely on it.

### Before you trade

A `healthy` quote is not a promise that your swap goes through. The endpoint reads the price when you call it (`serverTs`), and the pool checks feed freshness again in the block your swap executes in. If the feed goes stale in between, the swap reverts with `PriceProviderFailed` wrapping the provider's `FeedStalled()`.

1. **Pick pools with** [**Metadata**](/reference/api/metadata)**.** Its `priceProviderStatus` is refreshed about every 30 seconds. Use it to skip pools that are down, not to decide whether to trade.
2. **Check this endpoint right before you send the swap,** with `depth=true` (or the [batch endpoint](/reference/api/retrieve-batch-bid-and-ask) for many pools). Trade only when `priceProviderStatus` is `healthy` and the side you trade has a real price, not the sentinel.
3. **On Ethereum, send the swap through participating builders.** Ethereum pools use [conditional updates](/how-pools-work/freshness-and-ordering#how-each-chain-orders-an-update-against-a-trade): the maker's latest price lands onchain only in a participating builder's block that also contains a trade against the pool. This endpoint quotes against that latest streamed price, so `healthy` means the pool fills if your swap lands in such a block. Between trades the onchain price is usually stale, so a swap sent through the public mempool or included by another builder usually reverts. For the same reason, reading the pool's price onchain at the latest block is not a useful pre-check on Ethereum. Integrations built on Titan's [propAMM taker interface](https://docs.titanbuilder.xyz/propamms/takers) reach participating builders.

### Errors

| Status | Error                                   | When                                                                                                                                                                                                                                  |
| ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `Invalid pool address format`           | `pool_address` is not a valid address. An address without the `0x` prefix returns `pool_address is not a valid EVM address`.                                                                                                          |
| `400`  | `Failed to deserialize query string: …` | `depth` is not `true` or `false`. This body is plain text, not JSON.                                                                                                                                                                  |
| `401`  | `Invalid API key`                       | An `Authorization` header was sent with an unknown or revoked key.                                                                                                                                                                    |
| `404`  | `Unknown chainId: <id>`                 | The chain is not served by the API.                                                                                                                                                                                                   |
| `404`  | `Pair metadata not found for <pool>`    | The pool isn't served on this chain: wrong chain, unknown address, a pool delisted by Metric, or a pool on a disabled contract release. A listed pool with no liquidity returns `200`; its depth levels show zero `cumulativeVolume`. |
| `429`  | Rate limited                            | Too many requests. A valid key gets a higher limit. Wait for the time in `Retry-After`.                                                                                                                                               |
| `500`  | `Internal server error`                 | Metric couldn't read the pool onchain. Retry.                                                                                                                                                                                         |
| `503`  | `Service temporarily unavailable`       | A concurrent request for the same pool timed out, or on Ethereum Metric's price service was too busy or slow to answer in time. Retry shortly.                                                                                        |

New API keys work within seconds of being issued.


# Retrieve pool batch bid and ask

`POST /public/v1/evm/{chain_id}/bid_ask/batch`

Returns the [bid and ask](/reference/api/retrieve-current-bid-and-ask) for up to 100 pools on one chain in a single request. Use it to scan or rank many pools, for example by depth, without sending one request per pool.

Each pool gets exactly the quote that `GET /public/v1/evm/{chain_id}/{pool_address}/bid_ask` returns for it, and the two endpoints share a cache, so a pool quoted by one is served to the other from cache.

This is a public endpoint. Its rate limit is lower than the single-pool endpoint's, since one request quotes up to 100 pools. A Trading API key is optional; sending one raises your rate limit:

```http
Authorization: Bearer <your_trading_api_key>
```

A key you send must be valid: an unknown key returns `401`. Leave the header out if you don't have one.

### Path parameters

| Name       | Type    | Description                                                                            |
| ---------- | ------- | -------------------------------------------------------------------------------------- |
| `chain_id` | integer | EVM chain ID, e.g. `1`. Must be one of the chains returned by `GET /public/v1/chains`. |

### Request body

| Name    | Type       | Required | Default | Description                                                                                                                                                                           |
| ------- | ---------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pools` | address\[] | Yes      |         | 1 to 100 pool addresses, as returned by `GET /public/v1/evm/{chain_id}/metadata`. Case-insensitive. An address listed more than once is quoted once.                                  |
| `depth` | boolean    | No       | `true`  | `false` returns `"depth": null` for every pool, as the single-pool `depth` parameter does (see [Unavailable prices](/reference/api/retrieve-current-bid-and-ask#unavailable-prices)). |

Any other field is rejected with `400`.

### Request

```bash
curl -X POST "https://api.metric.xyz/public/v1/evm/1/bid_ask/batch" \
  -H "Authorization: Bearer <your_trading_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "pools": [
      "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
      "0x0000000000000000000000000000000000000001"
    ],
    "depth": false
  }'
```

### Response

`200 OK`. `results` holds one entry per distinct pool, in the order you listed them. Each entry carries either a `quote` or an `error`, never both.

```json
{
  "results": [
    {
      "poolAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
      "quote": {
        "pair": "WETH/USDC",
        "poolAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
        "priceProviderStatus": "healthy",
        "bidAdj": "49940417631045696435290",
        "askAdj": "49945012360825322506424",
        "totalToken0Available": "24503618827789131821",
        "totalToken1Available": "49785157539",
        "serverTs": 1790508850,
        "depth": null
      }
    },
    {
      "poolAddress": "0x0000000000000000000000000000000000000001",
      "error": {
        "status": 404,
        "error": "Pair metadata not found for 0x0000000000000000000000000000000000000001"
      }
    }
  ]
}
```

### Response fields

| Field                   | Type    | Description                                                                                                                                                 |
| ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `results`               | array   | One entry per distinct requested pool, in request order. A repeated address appears once, at its first position.                                            |
| `results[].poolAddress` | address | The pool address as you spelled it in the request.                                                                                                          |
| `results[].quote`       | object  | Present when the pool was quoted. Same shape as the [single-pool response](/reference/api/retrieve-current-bid-and-ask#response-fields), including `depth`. |
| `results[].error`       | object  | Present when the pool couldn't be quoted. `status` is the HTTP status the single-pool endpoint would have returned for this pool, `error` its message.      |

To estimate what an order fills for from a quote's `depth`, see [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth).

A pool whose feed has no price is a `quote`, not an `error`. It carries the unavailable sentinel (`bidAdj = "0"`, `askAdj = 2^128 − 1`) and its `priceProviderStatus`, exactly as on the single-pool endpoint. Check `priceProviderStatus` and each side's price in every quote before using it (see [Unavailable prices](/reference/api/retrieve-current-bid-and-ask#unavailable-prices)). A `healthy` quote can still revert onchain if the feed goes stale before your swap executes, and on Ethereum the swap must go through participating builders: see [Before you trade](/reference/api/retrieve-current-bid-and-ask#before-you-trade).

Typical per-pool errors are `404` (`Pair metadata not found for <pool>`: the pool isn't served on this chain) and the temporary `500` (`Internal server error`), `502` (`Upstream service error`) and `503` (`Service temporarily unavailable`). Retry pools with a temporary error.

### Errors

These fail the whole request. Per-pool problems never do: they are returned as entries in `results`.

| Status | Error                                                            | When                                                                                                          |
| ------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `400`  | `pools must name at least one pool address`                      | `pools` is empty.                                                                                             |
| `400`  | `pools names <n> addresses; at most 100 are allowed per request` | More than 100 addresses, counting repeats.                                                                    |
| `400`  | `pools[<index>] is not a valid pool address`                     | An entry isn't a valid address. `<index>` is its zero-based position.                                         |
| `400`  | `Invalid request body: …`                                        | The body isn't valid JSON, `pools` is missing, or it has unknown fields.                                      |
| `401`  | `Invalid API key`                                                | An `Authorization` header was sent with an unknown or revoked key.                                            |
| `404`  | `Unknown chainId: <id>`                                          | The chain is not served by the API.                                                                           |
| `429`  | Rate limited                                                     | Too many requests. A valid key gets a higher limit. Wait for the time in `Retry-After`.                       |
| `503`  | `Service temporarily unavailable`                                | The batch took longer than 20 seconds. No partial results are returned. Retry, or split into smaller batches. |

The rate limit counts batch requests, not pools, so one batch of 100 pools costs one request.


# Find best route

`POST /public/v1/evm/{chain_id}/route`

Find and price the best execution across direct and one-intermediate-hop paths for an **exact-input or exact-output** swap. Returns one or more routes. For exact-input, the input is split across multiple routes only when the configured thresholds show it reduces price impact and the split returns more total output. For exact-output, the output target is split instead, and a split is kept only if it needs less total input.

This is a public endpoint. A Trading API key is optional; sending one raises your rate limit:

```http
Authorization: Bearer <your_trading_api_key>
```

A key you send must be valid: an unknown key returns `401`. Leave the header out if you don't have one.

### Path parameters

| Name       | Type    | Description           |
| ---------- | ------- | --------------------- |
| `chain_id` | integer | Numeric EVM chain id. |

### Request body

| Field         | Type    | Required        | Description                                                                                                                                          |
| ------------- | ------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tokenIn`     | string  | yes             | Input token address. Must differ from `tokenOut`, non-zero.                                                                                          |
| `tokenOut`    | string  | yes             | Output token address.                                                                                                                                |
| `tradeType`   | string  | no              | `exact_in` or `exact_out`. Inferred when omitted: `amountIn` alone ⇒ `exact_in`, `amountOut` alone ⇒ `exact_out`. Required if you send both amounts. |
| `amountIn`    | string  | for `exact_in`  | Exact input, raw units (base-10, positive).                                                                                                          |
| `amountOut`   | string  | for `exact_out` | Exact output target, raw units.                                                                                                                      |
| `maxRoutes`   | integer | no              | Cap on routes in the plan, from `1` to `4`; `0` is treated as `1` and larger values as `4`. `1` = single best route (no split); omitted = `4`.       |
| `poolAddress` | string  | no              | Restrict routing to this one Metric pool. Its tokens must match `tokenIn`/`tokenOut`; a success returns one direct route through it.                 |

### Request

```bash
# Route (exact-in) - single & split, one endpoint
curl -X POST "https://api.metric.xyz/public/v1/evm/1/route" \
  -H "Content-Type: application/json" \
  -d '{
    "tokenIn":  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "tradeType": "exact_in",
    "amountIn": "50000000000000000",
    "maxRoutes": 4
  }'
```

### Response

`200 OK`

```json
{
  "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "tokenOut": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
  "tradeType": "exact_in",
  "amountIn": "50000000000000000",
  "amountOut": "136120897",
  "effectivePriceX64": "50219747000",
  "priceImpactBestRoute": "0.000082",
  "candidatesConsidered": 8,
  "routes": [
    {
      "tradeType": "exact_in",
      "routeType": "direct",
      "amountIn": "50000000000000000",
      "amountOut": "136120897",
      "effectivePriceX64": "50219747000",
      "priceImpactE6": "82",
      "legs": [
        {
          "tradeType": "exact_in",
          "poolAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24",
          "pair": "WETH/USDC",
          "zeroForOne": true,
          "tokenIn": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
          "tokenOut": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
          "amountIn": "50000000000000000",
          "amountOut": "136120897",
          "effectivePriceX64": "50219747000",
          "priceImpactE6": "82",
          "priceLimitX64": "0"
        }
      ]
    }
  ]
}
```

| Field                  | Type    | Description                                                                                                |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `tokenIn` / `tokenOut` | string  | Echoed endpoints (lowercased).                                                                             |
| `tradeType`            | string  | Echoed; tells you which side was fixed.                                                                    |
| `amountIn`             | string  | For `exact_in`, the request; for `exact_out`, the computed required input.                                 |
| `amountOut`            | string  | For `exact_in`, total computed output; for `exact_out`, ≈ the requested target.                            |
| `effectivePriceX64`    | string  | Blended execution price for the whole plan: `amountOut × 2^64 / amountIn`, in raw token units (see below). |
| `priceImpactBestRoute` | string  | Best single route's price impact as a **decimal fraction** (`"0.0135"` = 1.35%).                           |
| `candidatesConsidered` | integer | Number of candidate paths scored.                                                                          |
| `routes`               | array   | One entry per allocated route (≥1; multiple ⇒ split).                                                      |

`effectivePriceX64` is output per input, in the trade's direction (`tokenOut` per `tokenIn`), scaled by 2^64. It is **not** decimal-normalized, unlike `bidAdj`/`askAdj`. To get a human-readable price, compute `effectivePriceX64 / 2^64 × 10^(tokenIn decimals − tokenOut decimals)`. In the example, `50219747000 / 2^64 ≈ 2.72e-9` USDC base units per wei, so `× 10^(18 − 6)` gives ≈ 2,722 USDC per WETH.

### Response fields

#### Each `routes[]` entry

| Field                    | Type   | Description                                                             |
| ------------------------ | ------ | ----------------------------------------------------------------------- |
| `tradeType`              | string | Echoed.                                                                 |
| `routeType`              | string | `direct` or `one_hop`.                                                  |
| `amountIn` / `amountOut` | string | Amounts allocated to this route (raw units).                            |
| `effectivePriceX64`      | string | This route's execution price, same format.                              |
| `priceImpactE6`          | string | The largest `priceImpactE6` among this route's legs, E6 (`10000` = 1%). |
| `legs`                   | array  | Per-leg execution params.                                               |

#### Each leg

| Field                    | Type    | Description                                                                                                                                                                    |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tradeType`              | string  | Echoed.                                                                                                                                                                        |
| `poolAddress`            | string  | Pool to swap against.                                                                                                                                                          |
| `pair`                   | string  | Pool pair label.                                                                                                                                                               |
| `zeroForOne`             | boolean | Swap direction for this leg.                                                                                                                                                   |
| `tokenIn` / `tokenOut`   | string  | Leg tokens. For one-hop, leg 2 `amountIn` = leg 1 `amountOut`.                                                                                                                 |
| `amountIn` / `amountOut` | string  | Leg amounts (raw units).                                                                                                                                                       |
| `effectivePriceX64`      | string  | Leg execution price, same format.                                                                                                                                              |
| `priceImpactE6`          | string  | Leg price impact, E6: the depth level's `priceImpactE6` at the far edge of the last bin the leg reaches, not the leg's average price. Use `effectivePriceX64` for the average. |
| `priceLimitX64`          | string  | No-limit sentinel for the onchain swap: `0` when `zeroForOne`, `2^128−1` otherwise.                                                                                            |

### Routing and splitting

These details are internal and not returned in the response.

1. Enumerate direct + one-hop candidates; score each by walking `bid_ask` depth bins for the requested amount, as described in [Estimate a fill from depth](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth), and price the bin an order ends in as in [Exact fill inside a bin](/integrate/integrate-metric-liquidity/estimate-a-fill-from-depth#exact-fill-inside-a-bin), never exceeding what the pool would pay.
2. Pick the best single route (baseline). If its price impact is below `split_threshold_bps`, return just that.
3. Otherwise greedily allocate across routes by marginal price, subject to `min_improvement_bps`, `min_leg_fraction_bps`, and `maxRoutes`.
4. A split is returned only if it beats the baseline: more total output for exact-in, less total input for exact-out. The chosen `reason` (`below_split_threshold` / `single_route_after_allocation` / `not_better_than_baseline` / `split_applied`) and `splitApplied` are recorded in **logs/traces only**, not the response.

### Errors

| Status | Meaning                                                                                                                                                                 |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid body, `tokenIn == tokenOut`, zero address, non-positive amount, missing `amountIn`/`amountOut` for the `tradeType`, or both amounts sent without a `tradeType`. |
| `401`  | `Invalid API key`: an `Authorization` header was sent with an unknown key.                                                                                              |
| `404`  | `Unknown chainId: <id>`: the chain is not served by the API.                                                                                                            |
| `422`  | `no_route` (no eligible direct/one-hop path) or `insufficient_liquidity` (no candidate had enough depth).                                                               |
| `429`  | Rate limited. A valid key gets a higher limit. Wait for the time in `Retry-After`.                                                                                      |
| `500`  | Internal error loading pool data. Retry.                                                                                                                                |
| `502`  | An upstream data source failed. Retry.                                                                                                                                  |
| `503`  | Routing exceeded the 5s deadline.                                                                                                                                       |

A `422` body carries a machine-readable `code`:

```json
{
  "code": "insufficient_liquidity",
  "error": "no candidate route had enough depth to fill the requested amount"
}
```


# PropAMM quote

`POST /public/v1/evm/{chain_id}/pamm-quote`

Submits a signed quote-update transaction for a propAMM pool to Metric's partner builders. Ethereum (`1`) and BSC (`56`) are supported.

{% hint style="info" %}
**Oracle and contract addresses:** see [Contract addresses](/reference/supported-chains-and-deployments#oracle-and-price-provider-layer).
{% endhint %}

### Authentication

Authenticate with a Trading API key:

```
Authorization: Bearer $METRIC_TRADING_API_KEY
```

Any valid Trading API key can call this endpoint.

### Path parameters

| Name       | Type    | Description                                                                                                        |
| ---------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `chain_id` | integer | `1` (Ethereum) or `56` (BSC). A chain Metric doesn't serve returns `404`. Any other supported chain returns `400`. |

### Request body

Send JSON. Hex values may include the `0x` prefix.

| Field                       | Type                         | Chains   | Description                                                                                                                                                                                                                                              |
| --------------------------- | ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tx`                        | hex string                   | Both     | Required. Signed quote-update transaction for the path's chain. Send `"0x"` to cancel the quote.                                                                                                                                                         |
| `blockNumber`               | integer                      | Both     | Required. Block for which the quote is valid. On BSC, it is the final valid block, and a live quote requires a value greater than `0`.                                                                                                                   |
| `replacementUuid`           | hex string, 16 bytes         | Both     | Required. Quote identifier.                                                                                                                                                                                                                              |
| `replacementSeqNumber`      | integer                      | Both     | Required. Must be greater than `0`. Increase it for each update using the same `replacementUuid`.                                                                                                                                                        |
| `disableCrossRegionSharing` | boolean                      | Both     | Optional. When `true`, the builder does not share the quote across regions. BTCS and ETHGas always receive `true`, even if omitted or false; Metric sends directly to every BTCS and ETHGas region.                                                      |
| `responseTimeoutMs`         | integer, milliseconds        | Both     | Optional. How long to wait for builder answers before responding, measured from arrival. It can only lower the default of 500 ms. `0` responds as soon as the update is queued for every builder.                                                        |
| `maxTimestamp`              | integer, Unix seconds        | BSC      | Quote expiry. On a live BSC quote it can be at most five seconds ahead of the server clock. Required for BlockRazor. Without it, the quote reaches 48Club but skips BlockRazor. `0` or a millisecond value is rejected on both chains; only BSC uses it. |
| `alwaysInclude`             | boolean                      | BSC      | Optional, default `true`. Forwarded directly to 48Club (`false` requests conditional inclusion there). BlockRazor always authenticates and does not receive this flag; `false` does not guarantee conditional BlockRazor inclusion. Ignored on Ethereum. |
| `quoteAddress`              | address                      | Ethereum | Address a taker must touch to trigger the quote. Ignored on BSC.                                                                                                                                                                                         |
| `poolAddresses`             | address\[]                   | Ethereum | Pools covered by the quote. Ignored on BSC.                                                                                                                                                                                                              |
| `assetPairs`                | hex string\[], 40 bytes each | Ethereum | Token pairs priced by this update. Each joins two 20-byte addresses. Ignored on BSC.                                                                                                                                                                     |

On Ethereum, always include as much information as you can: `quoteAddress`, `poolAddresses` and `assetPairs`. Builders use it to match the quote to a trade, and a quote they can't match is never included. A builder may not use every field, but the more you send, the more likely your quote is to be included.

Unknown fields are dropped without an error, so check field names carefully.

Send the same `tx`, `replacementUuid` and `replacementSeqNumber` to Metric and to any builder you also submit to directly.

#### Building delivery mechanics

| Chain           | Builder delivery                                                                                                                                                                                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Ethereum        | Metric applies the update to the simulated head block. It forwards it to Titan, Quasar, BuilderNet, Bombora, Eureka, BTCS and ETHGas. A builder includes the update only with a trade against your pool. It places the update before that trade.                               |
| BSC: 48Club     | Receives the update over a quote stream. A newer quote replaces the earlier one. Send a higher `replacementSeqNumber` with the same `replacementUuid`. A cancel removes the quote.                                                                                             |
| BSC: BlockRazor | Receives the update as a bundle placed first in the block. This is paid inclusion. An accepted bundle may land and pay gas without a trade, even when the request sets `alwaysInclude: false`. BlockRazor cannot cancel a bundle. A live quote therefore needs `maxTimestamp`. |

`alwaysInclude: false` requests conditional inclusion from 48Club, not a guarantee of conditional inclusion from BlockRazor. Metric authenticates every BlockRazor submission; continue using your Trading API key as usual. If Metric has no usable BlockRazor credential, that leg reports `skipped: Blockrazor submission requires PAMM_BLOCKRAZOR_API_KEY` for either setting; the update still goes to 48Club. An HTTP 200 or builder acknowledgement does not prove onchain inclusion.

### Request

#### Ethereum

```bash
curl -X POST "https://api.metric.xyz/public/v1/evm/1/pamm-quote" \
  -H "Authorization: Bearer $METRIC_TRADING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tx": "0x02f8b10182…", "blockNumber": 23456789, "replacementUuid": "0x6f1c2b7e9a4d4c1e8b3a5d7f0e2c4a61", "replacementSeqNumber": 7, "quoteAddress": "0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24", "poolAddresses": ["0xF85AfbADeCC7F23Dd173ed706f07d9C7e2473e24"] }'
```

#### BSC

```bash
curl -X POST "https://api.metric.xyz/public/v1/evm/56/pamm-quote" \
  -H "Authorization: Bearer $METRIC_TRADING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tx": "0x02f8b13882…", "blockNumber": 63456789, "replacementUuid": "0x6f1c2b7e9a4d4c1e8b3a5d7f0e2c4a61", "replacementSeqNumber": 7, "maxTimestamp": 1790508442 }'
```

#### Cancel

Reuse the replacementUuid, increase the replacementSeqNumber and send an empty tx:

```bash
curl -X POST "https://api.metric.xyz/public/v1/evm/1/pamm-quote" \
  -H "Authorization: Bearer $METRIC_TRADING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tx": "0x", "blockNumber": 23456789, "replacementUuid": "0x6f1c2b7e9a4d4c1e8b3a5d7f0e2c4a61", "replacementSeqNumber": 8 }'
```

{% hint style="warning" %}
On BSC, a cancel only reaches 48Club. BlockRazor has no cancel operation. A bundle it has already accepted stays live until its `blockNumber` or `maxTimestamp` passes, even after a newer quote or cancel. The same signed transaction can only land once. An outdated quote can still land during that window. Keep `maxTimestamp` as short as your update cadence allows.
{% endhint %}

### Response

The endpoint returns 200 OK with a PammQuoteResponse, even if no builder accepts the update. Check accepted.

Metric responds once every builder has answered or the response deadline passes (500 ms from arrival, or your `responseTimeoutMs`). A builder that hasn't answered by then is reported as `pending`. Metric still delivers the update to it, but its final result isn't returned to you.

| Field                     | Description                                                                                                                               |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `accepted`                | `true` when at least one builder acknowledged the update by the response deadline.                                                        |
| `providers[].provider`    | Builder and endpoint, such as `titan:wss://…`, `club48:wss://pamm.48.club`, or `blockrazor:https://tokyo.builder.blockrazor.io`.          |
| `providers[].accepted`    | Whether this builder acknowledged the update.                                                                                             |
| `providers[].timestampNs` | Time the builder acknowledged the update, when reported.                                                                                  |
| `providers[].bundleHash`  | Accepted bundle hash. BlockRazor only.                                                                                                    |
| `providers[].pending`     | `true` when the builder had not answered by the response deadline. `accepted` is then `false` and there is no `error`. Omitted otherwise. |
| `providers[].error`       | Reason the update was not accepted. An error starting with `skipped:` means Metric did not submit to that builder.                        |

#### Ethereum example

```json
{
  "accepted": true,
  "providers": [
    {
      "provider": "titan:wss://eu.rpc.titanbuilder.xyz/ws/sendquoteupdate",
      "accepted": true,
      "timestampNs": 1790508439123456789
    },
    {
      "provider": "bombora:wss://direct-tyo-1.bombora.build/ws/sendquoteupdate",
      "accepted": false,
      "pending": true
    }
  ]
}
```

#### BSC example

```json
{
  "accepted": true,
  "providers": [
    {
      "provider": "club48:wss://pamm.48.club",
      "accepted": true,
      "timestampNs": 1790508439123456789
    },
    {
      "provider": "blockrazor:https://tokyo.builder.blockrazor.io",
      "accepted": true,
      "bundleHash": "0x9c1e…"
    }
  ]
}
```

On BSC, the BlockRazor entry reports a skip in two cases:

* The request had no maxTimestamp: "accepted": false, "error": "skipped: maxTimestamp is required for Blockrazor, which cannot cancel a bundle".
* The request was a cancel: "skipped: blockrazor has no cancel operation".

An acknowledgement is not inclusion. Measure fills, not acknowledgements.

### Timing

* Metric responds within 500 ms of arrival, or sooner if you set `responseTimeoutMs`. Builders that haven't answered by then are reported as `pending`.
* Each builder has three seconds to acknowledge. Metric keeps delivering a `pending` update in the background until then.
* An update with less than 500 ms of that time left can be dropped before it's sent. This applies to the Ethereum builders and 48Club. You only see such an update as `pending`.
* Slow builder connections reconnect automatically.
* Ethereum: the simulated-head update runs in the background.
* BlockRazor:
  * Metric sends one request at a time per endpoint. A push that arrives while a request is in flight waits and is sent next. Each `replacementUuid` keeps only its newest waiting push. An older waiting push from the same `replacementUuid` is never sent and reports skipped: superseded by a newer quote before it was sent. A cancel removes a waiting push the same way. A request already in flight can't be recalled.
  * A push older than one already seen for its `replacementUuid` is ignored (skipped: older than a quote already seen for this stream). A higher `blockNumber` always counts as newer. For 30 s after a cancel, a live quote on the same `replacementUuid` with a `replacementSeqNumber` at or below the cancel's is ignored.
  * Start a new `replacementUuid` for each block, or never restart `replacementSeqNumber` under one `replacementUuid`. BlockRazor works better with the second: a new `replacementUuid` doesn't replace the previous block's waiting push, so that push is still sent first.
  * Resending an already accepted bundle (same tx, blockNumber and maxTimestamp) returns the earlier result without sending it again. Resending a quote whose earlier copy is still waiting or being sent reports skipped: this quote is already queued or being sent.
  * Up to 256 pushes can wait per endpoint. When the queue is full, a push from a `replacementUuid` with nothing waiting reports skipped: parked queue full. A waiting push whose `maxTimestamp` passes is dropped (skipped: maxTimestamp passed while queued).
  * If BlockRazor already holds the bundle (already known or bundle already exist), Metric reports the update as accepted.
  * After a transport error, a timeout, an auth failure, a 429 or a 5xx, Metric pauses that endpoint for 500 ms to 30 s. If BlockRazor asks Metric to wait (Retry-After), Metric waits up to 30 s. Pushes that arrive during the pause report skipped: backing off for …ms more.

### Errors

Errors return an `ApiError` body:

```json
{ "error": "" }
```

| Status | When                                                                                                                                                                                                                                                                                                                                                                                           |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | The JSON is malformed, the `chain_id` is a chain Metric serves other than `1` or `56`, or a value is malformed. This includes malformed hex, wrong field lengths, `replacementSeqNumber` of `0`, or a transaction signed for another chain. On BSC, a live quote cannot have `blockNumber: 0`. `maxTimestamp` cannot be `0` or milliseconds, or, on a live BSC quote, over five seconds ahead. |
| `401`  | The Trading API key is missing or invalid.                                                                                                                                                                                                                                                                                                                                                     |
| `404`  | Metric doesn't serve the `chain_id` (`Unknown chainId`).                                                                                                                                                                                                                                                                                                                                       |
| `415`  | The `Content-Type: application/json` header is missing.                                                                                                                                                                                                                                                                                                                                        |
| `422`  | A required field is missing or has the wrong type.                                                                                                                                                                                                                                                                                                                                             |
| `429`  | Rate limit exceeded.                                                                                                                                                                                                                                                                                                                                                                           |
| `503`  | Maker submission is disabled for this chain, or no builder is configured for it. Every `503` returns the same body, `Service temporarily unavailable`, so the cases can't be told apart.                                                                                                                                                                                                       |

`400` messages name the failing field or value. Body-parsing errors (malformed JSON, `415` and `422`) return a plain-text body instead of `ApiError`.


# Smart contracts

Metric's current contracts are live on Ethereum, BNB Smart Chain and Robinhood Chain, at the same addresses on each.

{% hint style="info" %}
**Addresses:** see [Contract addresses](/reference/supported-chains-and-deployments) for the factory, router, quoter, extensions, oracles and price providers.
{% endhint %}

## **ABI**

Pool:

{% file src="/files/vS5CJbr69QXZ9upWWXb6" %}

Router:

{% file src="/files/bfSbgyCEi1ovaiKdkSpO" %}

Factory:

{% file src="/files/a0e3sLdrXMramBNGpXoF" %}

DataProvider:

{% file src="/files/70TZ5Z3pgXQOnpP60GG2" %}

Quoter:

{% file src="/files/eO72xxfaTsNzwXQGjf4M" %}


# Get quote

Simulate a swap offchain to get exact token deltas, without executing it or changing state.

Two ways, both `eth_call` only:

* **`MetricOmmSwapQuoter`** (recommended): decodes the revert for you and returns `(amountIn, amountOut)` directly.
* **`MetricOmmPool.simulateSwapAndRevert`** (low-level, for testing): takes your own prices and reverts with the result encoded in the revert data; you decode it yourself.

Both are non-`view` in the ABI because they mutate state internally before reverting. Call them with `eth_call`, never as a transaction.

### MetricOmmSwapQuoter

A read-only lens with two flavors, each available in exact-input / exact-output and single-hop / multihop forms.

* **Live**: reads the pool's price provider onchain. You supply no prices. Reverts `PoolPaused(pool, pauseLevel)` if any pool on the route is paused.
* **Hypothetical**: you supply `bidPriceX64`, `askPriceX64` and `referencePriceX64` yourself, to test a price scenario.

#### The `sender` parameter

Every quoter function takes a `sender`. It is forwarded to the pool's swap extensions in place of `msg.sender`, so extensions that gate on the caller behave as they would for a real swap.

**A zero `sender` defaults to `msg.sender`.** Pass the address that will actually send the swap whenever a pool has swap extensions.

Leave `from` empty in the `eth_call`. Live quotes read the Price Provider outside a swap, and restricted providers refuse that read unless `tx.origin` is zero, reverting with no data.

#### Supplying prices (hypothetical only)

Pass your own scenario values in the provider's format: raw, unadjusted Q64.64 bid, ask and reference. Don't read them from the Price Provider: restricted providers refuse the read.

{% hint style="warning" %}
Do **not** substitute the API's `bidAdj` / `askAdj` or the pool's `getSellAndBuyPrices()` output. Those already include the pool's deviation and fees; feeding them back as provider inputs applies the adjustments twice. For a current-provider quote, use the Live path instead.
{% endhint %}

Your three values must satisfy `bid < ask` and `bid <= reference <= ask`, and the pool still enforces its configured bid/ask/reference asymmetry bound on them. `sqrt(bid * ask)` is a safe default for the reference; a reference sitting exactly on `bid` or `ask` reverts unless the pool has the asymmetry check disabled.

#### Single-hop

{% code overflow="wrap" %}

```solidity
// Live: no prices supplied
function quoteLiveExactInSingle(
    address pool,
    address sender,
    bool zeroForOne,
    uint128 amountIn,
    uint128 priceLimitX64
) external returns (uint256 amountIn_, uint256 amountOut);

function quoteLiveExactOutSingle(
    address pool,
    address sender,
    bool zeroForOne,
    uint128 amountOutDesired,
    uint128 priceLimitX64
) external returns (uint256 amountIn_, uint256 amountOut);

// Hypothetical: caller-supplied provider quote
function quoteHypotheticalExactInputSingle(
    address pool,
    address sender,
    bool zeroForOne,
    uint128 amountIn,
    uint128 priceLimitX64,
    uint128 bidPriceX64,
    uint128 askPriceX64,
    uint128 referencePriceX64
) external returns (uint256 amountIn_, uint256 amountOut);

function quoteHypotheticalExactOutputSingle(
    address pool,
    address sender,
    bool zeroForOne,
    uint128 amountOutDesired,
    uint128 priceLimitX64,
    uint128 bidPriceX64,
    uint128 askPriceX64,
    uint128 referencePriceX64
) external returns (uint256 amountIn_, uint256 amountOut);
```

{% endcode %}

Each has an overload that inserts `address recipient` after `sender` and appends `bytes extensionData`, for pools whose extensions gate on those fields:

{% code overflow="wrap" %}

```solidity
function quoteLiveExactInSingle(
    address pool,
    address sender,
    address recipient,
    bool zeroForOne,
    uint128 amountIn,
    uint128 priceLimitX64,
    bytes memory extensionData
) external returns (uint256 amountIn_, uint256 amountOut);
```

{% endcode %}

#### Multihop

{% code overflow="wrap" %}

```solidity
function quoteLiveExactIn(address sender, QuoteExactInputParams calldata params)
    external returns (uint256 amountIn_, uint256 amountOut);

function quoteLiveExactOut(address sender, QuoteExactOutputParams calldata params)
    external returns (uint256 amountIn_, uint256 amountOut);

function quoteHypotheticalExactInput(address sender, QuoteHypotheticalExactInputParams calldata params)
    external returns (uint256 amountIn_, uint256 amountOut);

function quoteHypotheticalExactOutput(address sender, QuoteHypotheticalExactOutputParams calldata params)
    external returns (uint256 amountIn_, uint256 amountOut);
```

{% endcode %}

Parameter structs:

{% code overflow="wrap" %}

```solidity
struct QuoteExactInputParams {
    address[] pools;            // one per hop, at least one
    bytes[] extensionDatas;     // length must match pools
    uint256 zeroForOneBitMap;   // bit i = direction for pools[i]
    uint128 amountIn;           // exact input for the first hop
}

struct QuoteHypotheticalExactInputParams {
    address[] pools;
    bytes[] extensionDatas;
    uint256 zeroForOneBitMap;
    uint128 amountIn;
    uint128[] bidPricesX64;         // one per pool
    uint128[] askPricesX64;         // one per pool
    uint128[] referencePricesX64;   // one per pool
}
```

{% endcode %}

The exact-output structs are identical except `amountIn` becomes `amountOut` (the exact output of the final hop).

Multihop paths use an open price limit on every hop. Slippage is bounded by the amounts you check afterwards, not by per-hop limits.

### Low-level: call the pool directly

{% code overflow="wrap" %}

```solidity
function simulateSwapAndRevert(
    address sender,
    address recipient,
    bool zeroForOne,
    int128 amountSpecified,
    uint128 priceLimitX64,
    uint128 bidPriceX64,
    uint128 askPriceX64,
    uint128 referencePriceX64,
    bytes calldata extensionData
) external returns (int128 amount0Delta, int128 amount1Delta);
```

{% endcode %}

The declared return values are never produced. When the simulation completes, the pool deliberately reverts with:

{% code overflow="wrap" %}

```solidity
error SimulateSwap(int256 amount0Delta, int256 amount1Delta);
```

{% endcode %}

Catch and decode that revert yourself. Use the quoter unless you specifically need the raw signed deltas.

### How it works

Both quoter paths call `pool.simulateSwapAndRevert`. The only difference is where the three prices come from:

* **Live** reads the pool's price provider first (immutable provider, else the mutable slot), then passes those values in.
* **Hypothetical** passes your values in directly.

The pool then validates the prices, runs `beforeSwap` extensions, computes the swap, runs `afterSwap` extensions, and reverts with `SimulateSwap`. The quoter decodes that and returns the amounts.

Any other revert is re-thrown wrapped:

{% code overflow="wrap" %}

```
error WrappedError(address target, bytes4 selector, bytes reason);
```

{% endcode %}

`target` is the pool, `selector` is `simulateSwapAndRevert`, and `reason` is the raw inner revert; decode it a second time to get the real error.

Provider errors on the Live path aren't wrapped: the quoter reads the provider before calling the pool, so they surface as the provider's own error.

Before reading the provider, the Live path checks each pool's `pauseLevel` and reverts `PoolPaused(address pool, uint8 pauseLevel)` for a paused one, also unwrapped. On a multihop route, `pool` names the paused hop.

### What a quote does and does not check

**Runs:** price validation, `beforeSwap` and `afterSwap` extensions, and the full bin-walking swap math. This is true for both Live and Hypothetical.

**Skips:** token settlement (no transfers, no balance or allowance checks). Hypothetical quotes also skip the pause check, so a paused pool still returns one; Live quotes revert `PoolPaused`.

**Caller identity:** the pool sees the quoter contract as `msg.sender`. Pass the real `sender`, `recipient` and `extensionData` on the overloads whenever extensions depend on them.

{% hint style="warning" %}
**A quote doesn't prove the swap will go through.** Before executing, check `pauseLevel` in pool metadata and simulate the actual router transaction from the address that will send it.
{% endhint %}

### Errors

#### Pool errors

| Condition                                                                                 | Error                                                                          |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `amountSpecified == 0`                                                                    | `InvalidAmount()`                                                              |
| Swap computes a zero fill, commonly an empty pool                                         | `FinalAmountIsZero()`                                                          |
| `bidPriceX64 == 0` and `askPriceX64 > 0`                                                  | `BidIsZero()`                                                                  |
| `bidPriceX64 >= askPriceX64`                                                              | `BidGreaterThanAsk()`                                                          |
| Reference outside `[bid, ask]`                                                            | `ReferencePriceOutOfRange()`                                                   |
| Directional fee asymmetry outside the pool's bound, or reference exactly on `bid` / `ask` | `BidAskRefAsymmetryOutOfBounds()`                                              |
| Price provider reverted (Live path)                                                       | The provider's own error, e.g. `FeedStalled()`; no data if the read is refused |
| Bin balance would exceed `uint104`                                                        | `OverflowedOnFittingBinBalances(uint256)`                                      |

The pool's own `PoolPaused()` cannot occur here, because simulation skips the pause check. A Live quote on a paused pool reverts with the quoter's `PoolPaused(address,uint8)` instead (below).

The asymmetry bound is skipped entirely when the pool's `bidAskRefAsymmetryX8` is `type(uint16).max`.

#### Quoter errors

| Error                                             | Meaning                                                                      |
| ------------------------------------------------- | ---------------------------------------------------------------------------- |
| `WrappedError(address,bytes4,bytes)`              | Downstream revert; decode `reason` for the real error                        |
| `InvalidPriceProvider()`                          | Pool has neither an immutable nor a mutable provider configured              |
| `PoolPaused(address,uint8)`                       | Live path only: that pool's `pauseLevel` is non-zero, so a swap would revert |
| `QuoteDidNotRevert()`                             | `simulateSwapAndRevert` returned instead of reverting                        |
| `AmountTooLarge(uint128)`                         | Amount does not fit in `int128`                                              |
| `InvalidSwapDeltas()`                             | Deltas do not match the expected exact-in / exact-out shape                  |
| `InvalidPath()`                                   | Route arrays inconsistent, pools not token-connected, or path too short      |
| `InvalidInputAmountAtHop(uint8,uint256,uint256)`  | An exact-input hop partially filled                                          |
| `InvalidOutputAmountAtHop(uint8,uint256,uint256)` | An exact-output hop did not produce the requested amount                     |


# Get liquidity depth

`MetricOmmPoolDataProvider` returns a pool's depth ladder: every bin from the current one outward, with fee-adjusted prices and exact cumulative amounts. Use it to build liquidity curves for routing instead of simulating one swap per size.

* **One call per pool per block** replaces many `simulateSwapAndRevert` calls.
* **Prices match execution.** Cumulative amounts come from the pool's own `simulateSwapAndRevert`, not re-derived swap math.
* **`eth_call` only.** Both functions are non-`view` in the ABI because they simulate internally. Never send them as a transaction or call them from a contract.

> **On Ethereum, use the API instead.** Ethereum pools receive their price update in the same block as the trade, so between trades an onchain read can be stale or revert. The bid and ask endpoints return the same per-bin depth, simulated against the pending update. On other chains the onchain ladder is authoritative.

### Functions

```solidity
// Live: reads the pool's active Price Provider
function getLiquidityDepthLive(address pool, uint8 maxBinsPerSide)
    external returns (LiquidityDepth memory depth);

// Hypothetical: caller-supplied provider quote (raw Q64.64, same rules as Get quote)
function getLiquidityDepthHypothetical(
    address pool,
    uint8 maxBinsPerSide,
    uint128 bidPriceX64,
    uint128 askPriceX64,
    uint128 referencePriceX64
) external returns (LiquidityDepth memory depth);
```

* **`maxBinsPerSide`**: bins to walk beyond the current bin on each side. Each side returns up to `maxBinsPerSide + 1` rows, clamped to the pool's lowest and highest bin. Must be non-zero.
* **Leave `from` empty in the `eth_call`.** The Live function reads the Price Provider outside a swap, and restricted providers (Chainlink, Pyth) refuse that read unless `tx.origin` is zero, reverting with no data.

### What it returns

```solidity
struct LiquidityDepth {
    uint128 oracleBidX64;           // provider quote used for the snapshot
    uint128 oracleAskX64;
    uint128 oracleReferenceX64;
    uint128 effectiveCurrentBidX64; // fee-adjusted sell price at the exact current position (0 if no bids)
    uint128 effectiveCurrentAskX64; // fee-adjusted buy price at the exact current position (0 if no asks)
    DepthLevel[] asks;              // buy token0: current bin upward
    DepthLevel[] bids;              // sell token0: current bin downward
}

struct DepthLevel {
    int16   binIdx;
    uint256 lowerEffPriceX64;       // fee-adjusted execution price at the bin's lower bound
    uint256 upperEffPriceX64;       // fee-adjusted execution price at the bin's upper bound
    uint256 amountAvailableInBin;   // amount in the bin per pool state (partial for the current bin)
    uint256 amountTradeableInBin;   // amount reachable once extension gates apply
    uint256 cumulativeOut;          // total received through this bin (token0 for asks, token1 for bids)
    uint256 cumulativeIn;           // total paid through this bin (token1 for asks, token0 for bids)
}
```

* **Prices** are Q64.64, token1 per token0. **Amounts** are in each token's native decimals.
* **`cumulativeIn` / `cumulativeOut`** are exact at each bin boundary. For a size inside a bin, interpolate between the bin's `lowerEffPriceX64` and `upperEffPriceX64`.
* **Extension gates.** If an extension such as StopLoss rejects part of a bin, the ladder finds the largest amount that still succeeds, sets `amountTradeableInBin` below `amountAvailableInBin` on that row, and stops.
* **Sender context.** Depth is simulated with the data provider as sender and recipient, with empty extension data. For pools whose extensions gate on the caller, confirm your final size with a Live quote using your router as `sender`.
* **Invalid prices.** Provider prices that fail the pool's own validation (`bid == ask`, the asymmetry bound) return empty ladders instead of reverting.

### Example

```bash
cast call <dataProviderAddress> \
  "getLiquidityDepthLive(address,uint8)((uint128,uint128,uint128,uint128,uint128,(int16,uint256,uint256,uint256,uint256,uint256,uint256)[],(int16,uint256,uint256,uint256,uint256,uint256,uint256)[]))" \
  <poolAddress> 8 \
  --rpc-url $RPC_URL
```

No `--from`. The data provider address is on Deployments and in each pool's `dataProviderAddress` from the Metadata endpoint.

### Gas and caching

The ladder runs one simulation per bin, plus a short binary search on a bin an extension cuts off. Cost grows with `maxBinsPerSide`. With `maxBinsPerSide = 8`, a call uses roughly 1.5 to 3 million gas.

* **Keep `maxBinsPerSide` modest** (for example 5 to 10) and raise it only for pools whose curve you need deeper. Very large values can hit your RPC's `eth_call` gas cap.
* **Rebuild once per block.** The snapshot is tied to the current provider quote and pool state.

### Reading raw bin state

Each bin is one 32-byte storage word, so you can also cache raw state cheaply and rebuild only the bins that changed:

| Bits    | Field                           |
| ------- | ------------------------------- |
| 0–103   | `token0BalanceScaled` (uint104) |
| 104–207 | `token1BalanceScaled` (uint104) |
| 208–223 | `lengthE6` (bin width, uint16)  |
| 224–239 | `addFeeBuyE6` (uint16)          |
| 240–255 | `addFeeSellE6` (uint16)         |

Balances are in the pool's scaled internal units. Read them with the data provider's `binState(pool, binIdx)`, or `binStates(pool, int16[] binIdxs)` for many bins in one call. Read the current bin and position with `slot0(pool)`. Raw state tells you where liquidity sits; execution prices still come from the ladder or a quote.

### Batching several pools

The data provider takes one pool per call. To read many pools at once:

* **JSON-RPC batch (recommended).** Send an array of `eth_call` requests in one HTTP request, all pinned to the same block number instead of `latest` so every ladder comes from the same state. Each call gets its own gas cap, and one failing pool doesn't affect the others.
* **Multicall3 `aggregate3`.** One `eth_call` to Multicall3 (`0xcA11bde05977b3631167028862bE2a173976CA11`) with one entry per pool and `allowFailure = true`. Gas adds up across pools (about 1.5 to 2 million per pool at `maxBinsPerSide = 4`), so split large batches to stay under your RPC's `eth_call` gas cap.

Leave `from` empty either way.

### Compared with quoting each size

The ladder runs the same `simulateSwapAndRevert` as a quote, so extensions are applied. It differs from quoting each size yourself in a few ways:

* **Fixed sender context.** Depth is simulated with the data provider as sender and recipient and empty extension data. Extensions that gate on the caller, recipient or extension data can behave differently for your router.
* **Exact only at bin boundaries.** For sizes inside a bin, you interpolate between its bounds.
* **Exact-output simulation.** Cumulative amounts come from exact-output simulations. Exact-input routing may differ by rounding.
* **Extension cutoffs are searched.** When an extension rejects part of a bin, the ladder binary-searches the largest amount that still succeeds (up to 20 probes) and stops, assuming larger amounts keep failing.
* **No explicit pause signal.** Unlike the quoter's `PoolPaused` error, a paused pool is not flagged. Check `pauseLevel` from `slot0(pool)`, or skip pools that return empty ladders.

**Possible pattern:** build your routing curve from the ladder, then confirm the final routed size with one Live quote using your router as `sender`.

Related: [Get quote](/reference/smart-contracts/get-quote) · [Contract addresses](/reference/supported-chains-and-deployments) · [Bid and ask API](/reference/api/retrieve-current-bid-and-ask)


# Swapping via router

Execute swaps through MetricOmmSimpleRouter without implementing a callback.

`MetricOmmSimpleRouter` executes ERC-20 swaps through one or more pools and handles the settlement callback. Every swap entrypoint (`exact*` and `externalSwap`) has amount-based slippage protection (`amountOutMinimum` / `amountInMaximum`) and a `deadline`. The `multicall`, `sequence`, `selfPermit*` and payment helpers take no deadline.

Routes use ERC-20 token addresses. For WETH input, the router can wrap supplied native ETH. It also provides `unwrapWETH9`, `sweepToken` and `refundETH` payment helpers. For quotes, use `MetricOmmSwapQuoter`.

### Prerequisites

* When paying from your ERC-20 balance, `approve` the router to spend `tokenIn`; its callback transfers the input during settlement.
* Optional: the router supports **`selfPermit`** and **`multicall`**, so you can permit + swap in one transaction (no separate approval tx).

### Entrypoints

```solidity
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
function exactInput(ExactInputParams calldata params)             external payable returns (uint256 amountOut);
function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
function exactOutput(ExactOutputParams calldata params)             external payable returns (uint256 amountIn);
```

Single-hop params (exact input shown; exact output mirrors it with `amountOut` + `amountInMaximum`):

```solidity
struct ExactInputSingleParams {
    address pool;
    address tokenIn;          // the router pulls THIS from you in the callback; set it correctly
    address tokenOut;         // informational only; not checked onchain
    bool    zeroForOne;       // true = token0 → token1
    uint128 amountIn;
    uint128 amountOutMinimum; // slippage floor; reverts InsufficientOutput if not met
    address recipient;        // receives the output token
    uint256 deadline;         // reverts TransactionExpired once passed
    uint128 priceLimitX64;    // Q64.64 marginal bound (see sentinels below)
    bytes   extensionData;    // forwarded to the pool's swap extension; "0x" if unused
}
```

**Returns:** exact-input entrypoints return `amountOut`; exact-output entrypoints return `amountIn` (both `uint256`).

### priceLimitX64 (single-hop)

A Q64.64 marginal bound. The canonical no-limit values are:

| `zeroForOne` | Direction                | No-limit sentinel                 |
| ------------ | ------------------------ | --------------------------------- |
| `true`       | price down (lower bound) | `0`                               |
| `false`      | price up (upper bound)   | `type(uint128).max` (`2^128 − 1`) |

The router normalizes an opposite-direction open sentinel: `2^128 − 1` becomes `0` when `zeroForOne = true`; `0` becomes `2^128 − 1` when false.

Multihop entrypoints **omit per-hop price limits** (each hop runs open); slippage is controlled solely by `amountOutMinimum` / `amountInMaximum`.

### Multihop

`exactInput` / `exactOutput` take a route:

* `tokens[]`: the path `[t0, t1, …, tn]`.
* `pools[]`: pool between each adjacent pair (`length = tokens.length − 1`).
* `extensionDatas[]`: per-pool payload (`length = pools.length`).
* `zeroForOneBitMap`: bit `i` is the direction for `pools[i]` (up to 256 hops).

Exact-output routes execute **last hop → first**; `amountOut` is `tokens[last]`.

{% hint style="warning" %}
The router checks route array lengths and the hop limit, and rejects pools not registered with its factory. **It does not check that each pool connects its adjacent path tokens or that supplied `tokenIn`/`tokenOut` match pool immutables.** If they don't line up, the swap can revert, deliver a different token than you meant, or leave tokens in the router, where anyone can take them with `sweepToken`. Resolve pools and token addresses from the [metadata endpoint](/reference/api/metadata).
{% endhint %}

### externalSwap

```solidity
function externalSwap(address executor, bool payerIsRouter, bool refundAsNative, ExternalSwapParams calldata params)
    external payable returns (uint256 amountOut, uint256 amountSpent);
```

Swaps through a non-Metric router. The router sends `amountInMaximum` of `tokenIn` to `executor` (use the deployed `ExternalSwapExecutor`), which approves `externalRouter` and calls it with `externalRouterCalldata`. `amountOut` is the increase in `recipient`'s `tokenOut` balance; it reverts `ExternalSwapInsufficientOutput` below `amountOutMinimum`. Unused input is refunded to the caller, as ETH when `refundAsNative` is set (WETH input only). With `payerIsRouter`, the input comes from the router's own balance and unused input stays there.

### sequence

```solidity
function sequence(SequenceCall[] calldata calls)
    external payable returns (bytes[] memory results, StepStatus[] memory statuses);
```

Runs router calls in order. Each step sets what happens next: after success, `CONTINUE` or `STOP`; after failure, `CONTINUE`, `STOP` or `REVERT` (the whole sequence reverts `SequenceFailed`). A failed step is rolled back, so `msg.value` stays available to the next one. Use it for fallbacks: try pool A, then pool B, then an `externalSwap`, stopping at the first success.

### Notes

* **Settlement is internal:** the router's callback pulls `tokenIn` from you and the pool sends output to `recipient`. It validates the callback caller (`InvalidCallbackCaller`).
* **Errors:** `TransactionExpired`, `InsufficientOutput` (exact-in min), `InputTooHigh` (exact-out max), `InvalidOutputAmount`, `InvalidPool`, `InvalidPath`, `InvalidSwapDeltas`, `AmountTooLarge`, and the per-hop `InvalidInputAmountAtHop` / `InvalidOutputAmountAtHop`. Pool-level reverts (`BidIsZero`, `BidGreaterThanAsk`, `PoolPaused`, `AmountScaledExceedsMax`) bubble up. A Price Provider revert arrives wrapped as `PriceProviderFailed(bytes reason)`, e.g. `PriceProviderFailed(FeedStalled())`.

{% hint style="warning" %}
Execution uses the onchain provider quote; see [Liquidity and bins](/how-pools-work/liquidity-and-bins#execution-price) for pool pricing. On Ethereum, where to send the swap matters; see [Freshness and ordering](/how-pools-work/freshness-and-ordering#for-integrators).
{% endhint %}


# Swapping directly via pool

Call swap on the pool and settle via the IMetricOmmSwapCallback callback.

Call `swap` directly on `MetricOmmPool` from a contract that implements `IMetricOmmSwapCallback`. Use this for full control over funding (vault, flash logic, aggregator settlement); otherwise prefer the Router, which handles settlement for you.

The sign of `amountSpecified` selects exact-input or exact-output. The Router uses separate `exactInput*` / `exactOutput*` calls.

### Swap Parameters

```solidity
function swap(
    address recipient,        // receives the output tokens
    bool zeroForOne,          // true = token0→token1, false = token1→token0
    int128 amountSpecified,   // positive = exact input, negative = exact output (non-zero)
    uint128 priceLimitX64,    // marginal price bound, Q64.64
    bytes calldata callbackData,   // forwarded to YOUR swap callback
    bytes calldata extensionData   // forwarded to the pool's swap extensions
) external returns (int128 amount0Delta, int128 amount1Delta);
```

* `amountSpecified == 0` reverts `InvalidAmount`.
* `priceLimitX64` is your slippage protection (Q64.64 marginal bound).
* `callbackData` is opaque: the pool never reads it; it's handed back to your `metricOmmSwapCallback` so you can route context (payer, leg, funding source). `"0x"` if unused.
* `extensionData` is separate: the pool forwards it to its `beforeSwap` / `afterSwap` extension hooks, **not** to your callback. `"0x"` if the pool has no extensions or they need no input.
* Reverts `PoolPaused` if the pool is paused.
* A Price Provider revert arrives wrapped as `PriceProviderFailed(bytes reason)`, e.g. `PriceProviderFailed(FeedStalled())`.

{% hint style="warning" %}
**A swap can fill only part of your amount.** If the pool reaches `priceLimitX64` or runs out of liquidity first, `swap` still succeeds. Check the returned deltas before treating the trade as complete.
{% endhint %}

### Swap Callback

The pool settles Uniswap v3-style. Per swap, in order:

1. The pool computes deltas and **sends the output token to `recipient`** (the negative-delta side).
2. It calls `metricOmmSwapCallback` on `msg.sender`.
3. Your callback **transfers the owed input token to the pool** (the positive-delta side).
4. The pool balance-checks repayment and reverts `IncorrectDelta` if you underpaid.

Deltas (pool perspective): **positive** → you must send that token to the pool; **negative** → the pool already sent it to `recipient`; both are nonzero on a successful swap.

```solidity
/// @notice Any contract that calls IMetricOmmPool#swap must implement this.
interface IMetricOmmSwapCallback {
    /// @param amount0Delta  positive => pool must receive token0 from the payer; negative => pool sent it out
    /// @param amount1Delta  positive => pool must receive token1 from the payer; negative => pool sent it out
    /// @param callbackData  opaque bytes forwarded from the swap call
    function metricOmmSwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata callbackData
    ) external;
}
```

{% hint style="danger" %}
Your callback **must verify `msg.sender` is a legitimate Metric pool** before paying; otherwise any contract can call it and drain funds.
{% endhint %}

{% hint style="info" %}
The callback can source input from a wallet, vault or flash operation. The pool does not pull input tokens or require upfront transfers; your callback pays the pool.
{% endhint %}

### Check before you pay

You can't get a live quote from inside a transaction for a pool priced by Chainlink or Pyth. The quoter's live path reads the Price Provider outside a swap, and those oracles refuse that read onchain. To price a pool from a contract, call `swap` and decide in `metricOmmSwapCallback`: pay if the deltas are acceptable, or revert with your own error and catch it around the `swap` call. To use it as a quote, revert with the deltas and decode them in the `catch`. Set `priceLimitX64` to stop at your price.

{% hint style="warning" %}
Execution uses the onchain provider quote; see [Liquidity and bins](/how-pools-work/liquidity-and-bins#execution-price) for pool pricing. On Ethereum, where to send the swap matters; see [Freshness and ordering](/how-pools-work/freshness-and-ordering#for-integrators).
{% endhint %}


# SDK

TypeScript SDK for quoting, swapping and managing liquidity on Metric.

Latest version under <https://www.npmjs.com/package/@metric.xyz/metric-omm-sdk-v1>

Use 0.6.5 or later. Before 0.6.4 the SDK bundles the previous `MetricOmmSwapQuoter`, which returns Live quotes for paused pools. 0.6.4 bundles the current quoter but can't decode its `PoolPaused` revert. From 0.6.5, a Live quote on a paused pool rejects with `errorName === "PoolPaused"`.

{% hint style="info" %}
SDK documentation is in progress. See the [API](/reference/api) and [Smart contracts](/reference/smart-contracts) references.
{% endhint %}


# Contract addresses

Current Metric contract addresses (factory, router, oracles, price providers, extensions) and supported chains.

Metric is live on Ethereum, BNB Smart Chain and Robinhood Chain.

{% hint style="success" %}
**These are the current `v1` addresses.** Jump to [Core](#core), [Periphery](#periphery), [Extensions](#extensions) or [Oracles and price providers](#oracle-and-price-provider-layer).
{% endhint %}

{% hint style="warning" %}
**Only use the addresses on this page.** Older contract generations still exist on chain but are deprecated. If an address is not listed here, do not deploy against it or route to it.
{% endhint %}

### Chains

The same contract set is deployed on all three chains.

| Chain             | Chain ID | Public API (no key) | Partner-tier key |
| ----------------- | -------- | ------------------- | ---------------- |
| Ethereum          | 1        | ✅                   | ✅                |
| BNB Smart Chain   | 56       | ✅                   | ✅                |
| Robinhood Chain   | 4663     | ✅                   | ✅                |
| Base              | 8453     | —                   | ✅                |
| Arbitrum One      | 42161    | —                   | ✅                |
| HyperEVM          | 999      | —                   | ✅                |
| Polygon PoS       | 137      | —                   | ✅                |
| Avalanche C-Chain | 43114    | —                   | ✅                |
| Monad             | 143      | —                   | ✅                |
| MegaETH           | 4326     | —                   | ✅                |

Use these chain IDs as the `{chain_id}` path parameter in the [API](/reference/api).

{% hint style="info" %}
A deployed oracle contract does not guarantee that a feed is configured or enabled on that chain. Discover live chain and feed availability through the API.
{% endhint %}

### Why the addresses are the same everywhere

This release uses deterministic CREATE2/CREATE3 deployments, so each contract has the same address on every chain in the table. Addresses are still version-specific: a future deployment can use a new salt and therefore a new address.

### Core

| Contract                | Address                                      |
| ----------------------- | -------------------------------------------- |
| `MetricOmmPoolFactory`  | `0x2a53833cc95548cf52c7b159110e22D3a9018f32` |
| `MetricOmmPoolDeployer` | `0x2E0A99A2E352043d1CA489cF45bf7B511BCcC738` |

### Periphery

| Contract                      | Address                                      |
| ----------------------------- | -------------------------------------------- |
| `MetricOmmSimpleRouter`       | `0x02201E6f13c41506c7Bb635d7543F98826229aB7` |
| `ExternalSwapExecutor`        | `0x578C80DeC5D4Aa1F2879985e6e8d91Fc268e4AC2` |
| `MetricOmmSwapQuoter`         | `0x3B9ea79f482094323d06064E637a7F4528560fcb` |
| `MetricOmmPoolDataProvider`   | `0x83d66c5934b1525ee82e6E772c76c7acC778bBB7` |
| `MetricOmmPoolLiquidityAdder` | `0x49A0dcb8dBD304B80aA475026cC558d4baE54382` |

### Extensions

| Contract                       | Address                                      |
| ------------------------------ | -------------------------------------------- |
| `OracleValueStopLossExtension` | `0xE4038fA09f0bB9963068afaF97BE0c045155090D` |
| `DepositAllowlistExtension`    | `0x65FBa4Ee8f50772aB384f19e5520DdA420B1B47D` |
| `PriceVelocityGuardExtension`  | `0xEbc53e61078976118E384f110c262A263decB84B` |
| `SwapAllowlistExtension`       | `0x4Ca8d78873f0a66445CC05f9A1E705f1a58A6582` |

Metric's API does not index `SwapAllowlistExtension` membership, and its routes and quotes do not check it.

### Oracle & Price Provider layer

| Contract                    | Address                                      |
| --------------------------- | -------------------------------------------- |
| `CompressedOracleV1`        | `0xE844701934D9C6085B292e428F41b911cC0b3872` |
| `PriceProviderFactory`      | `0x050502eE4Da0eE2a9D44eE6046310F9f1532aCF2` |
| `PythOracle`                | `0x30a88bf894D1b806125a23ebf23D715496fD7102` |
| `ChainlinkOracle`           | `0x810A649974f21bB6D3b2E781597EdBDAabe82228` |
| `AnchoredProviderFactory`   | `0xb0eAE19259500Ac4Ca59b9a5cb8cDB2Da8076F0A` |
| `MultiplierOracle`          | `0x6c38232B0597ef335bCdC86029cDc9e23A17F166` |
| `B20AssetParams`            | `0x1D4995C8d5e8946cd2A60Baa25802Bb7D877E41c` |
| `RobinhoodStockTokenParams` | `0xCefea5dE67DF272c994d8a67cB4dd4bB495EE420` |

The [Multiplier Oracle](/how-pools-work/oracles#multiplier-oracle-tokenized-stocks) contracts are deployed only on the chains with tokenized stocks: `MultiplierOracle` on Base and Robinhood Chain, `B20AssetParams` on Base, and `RobinhoodStockTokenParams` on Robinhood Chain.

PUR, Flashbots' Priority Update Registry, is not a Metric contract. Flashbots redeploys it, so take its current address from the [PUR repository](https://github.com/flashbots/priority-update-registry).

### Fee policy

Do not infer live fee rates from the deployment. Protocol defaults and caps are on-chain factory state, while curator fees are set per pool.

* Protocol defaults and caps: `GET /public/v1/evm/factory-settings`
* A pool's live rates: pool metadata (`spreadAdminFeeE6`, `adminNotionalFeeE8`, and related fields)

See [Fees](/use-metric/fees) for the full breakdown.

### ABIs and source

The current ABI downloads are attached to [Smart contracts](/reference/smart-contracts).

| What                              | Where                                                                                                                |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Oracle & Price Provider contracts | [oracles-contracts](https://github.com/Metricxyz/oracle-contracts)                                                   |
| Pool contracts                    | [pool-contracts](https://github.com/Metricxyz/pool-contracts)                                                        |
| Periphery contracts               | [metric-periphery](https://github.com/Metricxyz/periphery-contracts)                                                 |
| TypeScript SDK                    | [sdk](https://github.com/Metricxyz/sdk) · [npm package](https://www.npmjs.com/package/@metric.xyz/metric-omm-sdk-v1) |


# Glossary

Definitions of the terms used across these docs, from pools and fees to contract details.

### Pool roles

#### Operator

What the Metric app calls a pool's admin who isn't a [verified curator](#verified-curator): the pool header reads "operated by" instead of "curated by". An operator has the same control over the pool as a curator; it just hasn't passed Metric's curator verification. Metric doesn't endorse any operator, curator or strategy; see [Curators](/use-metric/provide-and-manage-liquidity/public-pools-and-curation).

#### Verified curator

A curator who has passed KYB, is in Metric's registry and created the pool. Verification identifies the curator; it doesn't vouch for the strategy. See [Pool tiers](/use-metric/pool-tiers#definitions).

#### Private pool curator

A market maker running a pool with its own capital and pricing strategy.

### Pools & liquidity

#### Private pool

A pool with a deposit allowlist, run on its curator's own capital. It's shown as 🟣 Private instead of receiving a tier.

#### Classic AMM

An AMM that sets its price from its own token balances, so the price moves only when someone trades against it.

#### Bin

A percentage range around the reference price, such as +1% to +2%, where LPs place liquidity. Trading happens in the **active bin**. See [Liquidity and bins](/how-pools-work/liquidity-and-bins).

#### Impermanent loss

In a classic AMM, the loss LPs take compared with holding, when arbitrageurs trade the pool to a new market price. It only ever runs against LPs. Metric pools don't have it as long as their reference keeps up with the market, but LPs can still lose money; see [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns#trading-pnl).

#### Markout

How much your bins made or lost on each swap, measured against the pool's reference price a fixed time after the trade (1, 5 or 30 minutes). It's after the curator's and Metric's shares. Positive means you traded at a good price; negative means traders got ahead of the market. It's an estimate drawn from past trades, not realized profit. Markout APR annualizes it, and it's a past figure, not a forecast. See [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns#markout).

#### Trading PnL

How much more or less you have than if you had simply held the tokens you deposited, with both valued at today's prices. It comes from trades changing your tokens, the same trades your [Markout](#markout) measures. It can be positive or negative. See [Understanding your returns](/use-metric/provide-and-manage-liquidity/understanding-your-returns#trading-pnl).

### Spread & fees

#### Spread

The gap between a pool's bid and ask, usually quoted relative to the mid price, in basis points. It is the cost of buying and selling back. See [Fees](/use-metric/fees).

#### Half spread

Half the bid-ask spread: what one trade pays relative to the reference price, before price impact, averaged over the buy and sell sides. Compare it with another venue's swap fee, which is also charged once per trade.

Each side is measured from the reference, and the two can differ:

* Buying token0 from the pool: `ask / reference − 1`.
* Selling token0 to the pool: `reference / bid − 1`.

The contracts call it the base fee and calculate these ratios in Q64.64. The additional bin fee and the notional fee are separate.

#### Additional bin fee

Extra spread the pool curator can add to a bin, with separate rates for buying and selling. A trade pays each bin's rate on the part filled in that bin.

#### Notional fee

An optional fixed percentage of trade size, capped at 0% for the curator and 1% for Metric, so for now only Metric can apply one, on some pools. It's off by default. If a pool has one, it's in the quote and goes to Metric, not LPs.

### Safeguards

#### Anchored pricing

Pricing through an Anchored Price Provider, which takes its reference from Chainlink or Pyth data (for tokenized stocks, Pyth through the Multiplier Oracle) and sets a maximum bid and a minimum ask that the curator can't quote through. See [Anchored standard](/how-pools-work/anchored-pricing#anchored-pricing).

#### Withdrawal extension

Any extension registered on `beforeRemoveLiquidity` or `afterRemoveLiquidity`, including Metric's standard ones. It can block LP exits, so it makes a pool High Risk. Adding extensions on swaps or deposits doesn't lower the tier. See [Pool tiers](/use-metric/pool-tiers).

### Price formats & units

#### Q64.64

Fixed-point number format used for all prices. The integer is the real value × 2^64; convert to human-readable with `value / 2^64`. A price is token0 expressed in token1 units (e.g. 1 ETH in USDC). Decimals are normalized internally, so no manual decimal adjustment is needed when feeding values back into the contracts.

#### E6

"Parts per million" fixed-point: `1_000_000` = 100%, `10_000` = 1%. Used for bin deviations, spread-fee shares and `priceImpactE6`. Note: the routing API's per-leg `priceImpactE6` is a raw E6 string, while the split response's top-level `priceImpactBestRoute` is the same quantity as a **decimal fraction** (`0.01` = 1%).

#### bps (basis points)

`1 bps = 0.01% = 100 E6`. Used for split-routing thresholds (`split_threshold_bps`, `min_improvement_bps`, `min_leg_fraction_bps`).

#### Scaling

Cumulative depth volumes are reported in each token's **external (onchain) decimals**: `cumulativeVolume` in the output token, `cumulativeInputVolume` in the input token. Depth and `bidAdj`/`askAdj` prices use normalized Q64.64, while `/route`'s `effectivePriceX64` uses raw token units. Quoter prices use normalized Q64.64; its token amounts use each token's native base units. Divide prices by `2^64` and token amounts by `10^decimals` to display human-readable values. The pool's internal scale factor for each token is `10^(max(18, token0.decimals(), token1.decimals()) − token.decimals())`.

### Prices & oracle

#### Price Provider

A contract that returns a [provider quote](/how-pools-work/price-providers) through `getQuote()`, in normalized Q64.64. It can use oracle data or custom pricing logic. Metadata exposes the address as `priceProviderAddress`. `pool.getImmutables()` contains the immutable provider address; a pool configured for a mutable provider resolves its current address separately.

#### Multiplier Oracle

An oracle for tokenized stocks whose issuer applies a multiplier for splits and other corporate actions. It prices the token as the Pyth share price × the multiplier it reads from the token. Live on Base (B20 Assets) and Robinhood Chain (Robinhood Stock Tokens). See [Oracles](/how-pools-work/oracles#multiplier-oracle-tokenized-stocks).

#### Raw provider prices

The unadjusted provider quote returned by `IPriceProvider.getQuote()`. `swap()` reads it from the provider; hypothetical quotes and `simulateSwapAndRevert(...)` take all three values as explicit arguments. Do not replace them with adjusted pool prices.

#### Reference price

The provider-supplied price corresponding to 0% deviation. It anchors the bin ranges and the two directional base fees; it is not necessarily the midpoint between bid and ask.

#### Deviation

The pool's current marginal price before the spread, relative to its reference: `price / reference − 1`. It combines the active bin's lower deviation with the position within that bin. Halfway through a +1% to +2% bin means +1.5%.

#### bidAdj / askAdj

The pool's adjusted marginal bid/ask, including its current deviation and directional fees. They can be used as Q64.64 price limits, but are **not** substitutes for the raw provider quote in simulation.

### Swap mechanics

#### Slippage

The difference between the output you were quoted and the output you get, caused by prices or liquidity changing before your trade executes. Your slippage tolerance sets the worst result you accept; if execution would be worse, the swap reverts.

#### zeroForOne

Swap direction. `true` = token0 → token1 (deviation moves down); `false` = token1 → token0 (deviation moves up), at a given reference.

#### amountSpecified

The swap amount (`int128`). **Positive** = exact input; **negative** = exact output. `0` reverts with `InvalidAmount` on both `swap` and `simulateSwapAndRevert`.

#### recipient

The address that receives the output tokens. May differ from the caller (`msg.sender`), which is the contract that implements and is charged in the callback.

#### amount0Delta / amount1Delta

The token balance changes from the **pool's perspective**: positive = the pool receives that token (you owe it), negative = the pool sends that token (to `recipient`). A successful swap has one positive and one negative delta.

#### priceLimitX64

A Q64.64 limit on the fee-inclusive marginal pool price: a **floor** when `zeroForOne = true` and a **ceiling** when `false`. The raw pool can stop with a partial fill. Its open-limit values are `0` for true and `2^128 − 1` for false; a limit blocking all progress reverts with `FinalAmountIsZero`.

The SimpleRouter and SwapQuoter normalize an opposite-direction open sentinel. The router's `exactInputSingle` can accept a partial fill if minimum output is met; multihop exact-input and exact-output methods enforce their specified amounts.

#### Swap callback (`metricOmmSwapCallback`)

The settlement hook the pool calls on `msg.sender` mid-swap. The pool sends the output token first, then the callback must transfer the owed input token to the pool (verified via balance check, else `IncorrectDelta`). Deltas are passed as `int256`. Required only when calling the pool directly; the Router implements it for you.

### Contracts

#### Pool (`MetricOmmPool`)

The core AMM contract. Holds liquidity and executes swaps using the provider quote. Exposes `swap` and `simulateSwapAndRevert`. See [Liquidity and bins](/how-pools-work/liquidity-and-bins#execution-price) for pool pricing.

#### Quoter (`MetricOmmSwapQuoter`)

A periphery lens for `eth_call`. Live methods read the pool's provider quote, then call `simulateSwapAndRevert`; hypothetical methods pass a caller-supplied provider quote to it. The quoter decodes the deliberate revert and returns `(amountIn, amountOut)`. Live methods revert `PoolPaused(pool, pauseLevel)` for a paused pool. See [Get quote](/reference/smart-contracts/get-quote) for simulation limits.

#### SimpleRouter (`MetricOmmSimpleRouter`)

Periphery, execution. Implements the swap callback and supports exact-input/output swaps, payment helpers and slippage guards. It is separate from the Quoter.

#### Factory (`MetricOmmPoolFactory`)

Deploys pools (`createPool`) and holds protocol-fee settings.

#### simulateSwapAndRevert

Pool function that simulates with a caller-supplied provider quote, then **reverts** with the resulting signed token deltas. No state persists. See [Get quote](/reference/smart-contracts/get-quote) for the checks it includes and omits.

#### SimulateSwap

The custom error `SimulateSwap(int256 amount0Delta, int256 amount1Delta)` that carries simulation results in the revert data.

#### WrappedError

`WrappedError(address target, bytes4 selector, bytes reason)` is raised by `MetricOmmSwapQuoter` to wrap an unexpected inner revert so callers can see what failed.


# LLM Context

Give your AI assistant or agent accurate, up-to-date context on Metric, so it can answer questions and write integration code without guessing.

### What's available

| Resource                                                                                                                                                                                                                                                            | What it is                                                                                                                                      | Best for                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| <div data-gb-custom-block data-tag="file" data-src="https://2762670563-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyvQVWzbfEJJ8Aelwvxmj%2Fuploads%2Fgit-blob-6d892a2a1e0961bf494f2672e4201ec997e98071%2Fllm_v1_updated.md?alt=media"></div> | One plain-text file with contract addresses, ABIs, the price and simulation rules, the API reference and integration gotchas, written for LLMs. | Pasting into a chat or a project knowledge base, or adding to an agent's system prompt. |
| `https://docs.metric.xyz/~gitbook/mcp`                                                                                                                                                                                                                              | An MCP server for these docs. It has two read-only tools: `searchDocumentation` and `getPage`.                                                  | Agents and coding assistants that should look things up live as they work.              |
| [`llms.txt`](https://docs.metric.xyz/llms.txt)                                                                                                                                                                                                                      | An index of every docs page, with links to Markdown versions.                                                                                   | Tools that follow the llms.txt convention.                                              |
| [`llms-full.txt`](https://docs.metric.xyz/llms-full.txt)                                                                                                                                                                                                            | Every docs page in a single Markdown file.                                                                                                      | Loading the full docs into a long-context model.                                        |


# Security and audits

Metric's contracts have been audited five times. The full reports are attached.

**Zellic, June 2026.** Smart contract security assessment of the core, periphery and oracle contracts (metric-core 6aa6c3b, metric-periphery 0069084, oracle contracts eda97bb).

{% file src="/files/pIoiL99sSYeA45YDmjdi" %}

**Zellic, September 2026**. Re-audit of all changes to the core, periphery and oracle contracts since the June review (metric-core 33d0a34 → 4410fec, metric-periphery cc8fef9 → 9b69f75, oracle contracts edbc5f0 → 8b72555).

{% file src="/files/Aj2HuD7xB9J0h3OKbxPI" %}

**Sherlock, May to June 2026.** Collaborative audit of the core, periphery and oracle contracts (audited at metric-core 6aa6c3b, metric-periphery 90039f9 and oracle contracts a7644f6; fixes at 7b9ab56, d210a84 and 056c204).

{% file src="/files/oO7I6HKMDQfyLcVCI6ZL" %}

**Sherlock, July 2026.** Public bug bounty audit contest on the core, periphery and oracle contracts (audited at metric-core 7b9ab56, metric-periphery d210a84 and oracle contracts 056c204; fixes at 4410fec, 9b69f75 and 8b72555).

{% file src="/files/ozHXCEy5cZtoEz2KurGk" %}

**Sherlock, September 2026.** Update review of the periphery contracts, including the external swap executor and the propAMM adapter (metric-periphery, audited at commit 378a748, fixes at ed58520; propamm-adapter, audited at commit 8f127a2, fixes at 652d2a1).

{% file src="/files/kt9SJQtxNMgmwc4om8Wc" %}

## Audited code

The contracts were developed and audited in private repositories. Every core, periphery and oracle commit named in the reports above is published as a tag in our public repositories, so you can read exactly the code each audit covered:

| Repository                                                                                                                 | Audited commits, published as tags                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [pool-contracts](https://github.com/Metricxyz/pool-contracts#audited-code) (metric-core)                                   | [`6aa6c3b`](https://github.com/Metricxyz/pool-contracts/tree/audit-6aa6c3b), [`7b9ab56`](https://github.com/Metricxyz/pool-contracts/tree/audit-7b9ab56), [`4410fec`](https://github.com/Metricxyz/pool-contracts/tree/audit-4410fec), plus [`33d0a34`](https://github.com/Metricxyz/pool-contracts/tree/audit-33d0a34), where the Zellic re-audit started                                                                                                                                                                                                                                                                           |
| [periphery-contracts](https://github.com/Metricxyz/periphery-contracts#audited-code) (metric-periphery)                    | [`0069084`](https://github.com/Metricxyz/periphery-contracts/tree/audit-0069084), [`90039f9`](https://github.com/Metricxyz/periphery-contracts/tree/audit-90039f9), [`d210a84`](https://github.com/Metricxyz/periphery-contracts/tree/audit-d210a84), [`9b69f75`](https://github.com/Metricxyz/periphery-contracts/tree/audit-9b69f75), [`378a748`](https://github.com/Metricxyz/periphery-contracts/tree/audit-378a748), [`ed58520`](https://github.com/Metricxyz/periphery-contracts/tree/audit-ed58520), plus [`cc8fef9`](https://github.com/Metricxyz/periphery-contracts/tree/audit-cc8fef9), where the Zellic re-audit started |
| [oracle-contracts](https://github.com/Metricxyz/oracle-contracts/blob/main/docs/README.md#audited-code) (oracle contracts) | [`eda97bb`](https://github.com/Metricxyz/oracle-contracts/tree/audit-eda97bb), [`a7644f6`](https://github.com/Metricxyz/oracle-contracts/tree/audit-a7644f6), [`056c204`](https://github.com/Metricxyz/oracle-contracts/tree/audit-056c204), [`8b72555`](https://github.com/Metricxyz/oracle-contracts/tree/audit-8b72555), plus [`edbc5f0`](https://github.com/Metricxyz/oracle-contracts/tree/audit-edbc5f0), where the Zellic re-audit started                                                                                                                                                                                    |

Each tag is named `audit-<commit>`, for example `audit-4410fec`. A tag is a new commit without the private history, so its own hash differs from the audited one, but its `contracts/` folder is identical to the audited commit's. The oracle tags also leave out deployment scripts and internal tooling.

### How to verify a tag

Each tag includes a `VERIFY.md` and a small script that prove its `contracts/` folder matches the commit hash printed in the audit report. You need only git and Python 3, no access to our private repositories:

```bash
git clone --branch audit-4410fec https://github.com/Metricxyz/pool-contracts.git
cd pool-contracts
python verify/verify.py .
```

The script hashes the audited commit object and its root tree, which are included in the tag, checks that the first matches the hash in the report, and compares the `contracts/` hash listed in that tree with the one in the tag. The last line reads `OK: contracts/ at this tag is identical to contracts/ in audited commit …`. Because a git hash covers every file beneath it, a match proves each contract file is the audited code. Swap in any repository and tag from the table above; each repository's README lists the expected `contracts/` hashes.

## Changes since the last audit

The current code in each public repository includes a small number of changes made after its last audit:

* **pool-contracts**, since `4410fec`: a fix for precision loss in the bid/ask/reference asymmetry check, and a higher optimizer setting. This version is the core dependency of the periphery code Sherlock reviewed in September 2026.
* **periphery-contracts**, since `ed58520`: live quotes from `MetricOmmSwapQuoter` revert with `PoolPaused` for paused pools, and unused code was removed.
* **oracle-contracts**, since `8b72555`: a change to how `PriceProvider` and `AnchoredPriceProvider` set their minimum ask, and the new multiplier oracle for tokenized stocks (Base and Robinhood only), which is not covered by the audits above.

Everything else changed after these audits is comments, documentation and formatting. Each repository's README lists the details.

Found a vulnerability? Write to <security@metric.xyz> and get a bounty.


