Для разработчиков / API приложений

API DEX Pepper

Индекс только на чтение поверх логов Pepper, отдающий два поколения пулов из двух наборов таблиц. Он отвечает десятичными строками, ограничивает каждый список пятьюстами строками, не говоря об этом, и не отправляет никакого заголовка CORS.

Два DEX, один API

У Pepper бок о бок есть пары с постоянным произведением и концентрированные пулы. Этот сервис отдаёт и те и другие, из двух наборов таблиц, под двумя префиксами: голые пути - это пути постоянного произведения, а всё под /cl/ - концентрированное. Это не варианты одного и того же.

Эти два семейства невзаимозаменяемы

У пары с постоянным произведением два резерва и одна цена. У концентрированного пула нет ни того ни другого: у него есть цена, тик, ликвидность, активная на этом тике, и неограниченное множество диапазонов, которые не активны вовсе. Читать activeLiquidity как глубину, а баланс - как торгуемый размер, неверно ровно тем способом, который стоит денег: глубина по текущей цене может быть тысячной долей баланса, а остальное лежит в диапазонах, которых следующая сделка никогда не коснётся.

Адрес пула принадлежит одному семейству. Спросить /pool/{address} о концентрированном пуле - это 404, а не редирект.

Выпустили токен? Дайте ему логотип и отметку проверки

Токен, выпущенный на Pepper, не получает отметку проверки, пока не попадёт в публичный список токенов, - оттуда же приложения Pickle берут логотип, имя и символ токена из списка. Попасть туда - это pull request с его логотипом и записью: Список токенов и метки адресов описывает шаги и критерии.

Базовый URL и CORS

Точка доступаValue
Публичныйhttps://pepper.picklechain.xyz/api/монтирование nginx на источнике сайта Pepper
Стек, который вы запускаете самиhttp://127.0.0.1:4020собственный порт сервиса
CORSникакогони заголовка, ни обработчика OPTIONS, никакого глагола, кроме GET

Браузер с другого источника не может вызвать этот сервис

Нигде в нём нет заголовка access-control-allow-origin и нет do_OPTIONS, чтобы ответить на предварительный запрос. fetch со страницы на вашем собственном домене отказывает, тогда как идентичный запрос из curl или с сервера проходит - это тот отказ, который все принимают за сетевой сбой. Поставьте его за свой собственный источник или вызывайте с бэкенда.

Каждый ответ отправляется с no-store и x-content-type-options: nosniff. Ограничения частоты в сервисе нет: вызывающего ограничивает сверху limit, который равен 500 на каждом маршруте списка при значении по умолчанию 50.

Маршруты постоянного произведения

Params
none
Возвращает
{cursorBlock, lagBlocks, updatedAt, spoolHeadBlock, streams:{v2, cl}}

Where each indexer stream has got to, against the head of the spool. The three top-level keys are the constant-product stream, kept there so a caller written before the concentrated pools existed still reads what it meant.

Полезно знать. Check `lagBlocks` before trusting anything else in this service. A stream that has stopped serves its last answer indefinitely and nothing else on any route says so.

Params
none
Возвращает
{pools: [...], ethUsd}

Every constant-product pool, ordered by swap count: both tokens' metadata, reserves, the block each reserve was read at, creation block, swap count and cumulative per-side volume.

Полезно знать. An OBJECT, not a bare array - the pools are under `pools`. `volume0`/`volume1` are cumulative amounts PAID IN per side, so they are not comparable across pools and are not a price.

Params
the pair address in the path, matched case-insensitively
Возвращает
one pool object, plus ethUsd

One pool, in the same shape as a row of /pools.

Полезно знать. It is implemented by building the FULL pool list and scanning it, so it costs exactly what /pools costs. Fetching ten pools one at a time does ten times the work of fetching all of them. 404 when unknown.

Params
none
Возвращает
{tokens: [{address, symbol, decimals, ...}]}

Every token seen on either side of a constant-product pool, which is what a swap picker needs.

Полезно знать. Token metadata is read once per process and cached for the life of that process - not to be fast, but because the node meters `eth_call` through one bucket shared by every client of it. A token that changes its symbol keeps the old one until a restart.

Params
pair, sender, limit
Возвращает
{swaps: [{txHash, logIndex, block, pair, sender, recipient, amount0In, amount1In, amount0Out, amount1Out}]}

Newest first, filterable by pair and by sender, both exact addresses.

Полезно знать. Amounts are decimal strings. A malformed address parameter is a 400, but an address written without its 0x prefix is accepted rather than silently truncated.

Params
pair, limit
Возвращает
{events: [{txHash, logIndex, block, pair, kind, sender, recipient, amount0, amount1}]}

Mints and burns, newest first, `kind` being one of those two words.

Полезно знать. `recipient` is null on a mint. The pool's Mint event carries no recipient, so the field is left empty rather than filled in from the sender, which would read as a fact.

Params
none
Возвращает
{pools, swaps, mints, burns, poolsWithEth, poolsWithoutEth, valueEthWei, valueEthDenominator, valueUsd, ethUsd, concentrated}

Counters for the constant-product side, an ETH-denominated figure for the pools that hold WETH, and the concentrated side's counters nested under `concentrated` rather than added in.

Полезно знать. `valueEthWei` IS NOT TVL and the service says so in its own source. It is the WETH side doubled, summed over the pools that have a WETH side; a pool without one contributes nothing and is counted separately under `poolsWithoutEth`. The concentrated figures are kept apart because the doubling rule does not hold for them at all.

bash
# Health first, always. lagBlocks is the only field that tells you
# whether anything else on this service is current.
curl -s "$DEX_API/health"

# Pools are under a key, not at the top level.
curl -s "$DEX_API/pools" | jq '.pools[0] | {pair, reserve0, reserve1, swapCount}'

# Swaps for one pair. The address may be written with or without 0x.
curl -s "$DEX_API/swaps?pair=$PAIR&limit=100" | jq '.swaps | length'

Маршруты концентрированной ликвидности

Params
none
Возвращает
{pools: [...], ethUsd}

Every concentrated pool: fee tier, tick spacing, whether it is initialised, the square-root price, the current tick, the liquidity active at that tick, both balances and the derived human price.

Полезно знать. `activeLiquidity` is the depth AT the current price, not the size of the pool, and `balance0`/`balance1` are the size. They are the same number only in a pool where every position spans the whole range, which is the one shape nobody opens a concentrated pool to build.

Params
the pool address in the path
Возвращает
one pool object, plus ethUsd

One concentrated pool. Unlike /pool/{pair} this one is a keyed lookup, so it is cheap.

Полезно знать. 404 when unknown. A malformed address is a 400.

Params
none
Возвращает
{tiers: [{fee, tickSpacing, enabledBlock}]}

The fee tiers the factory has enabled, in fee order. `fee` is in millionths, so 3000 is 0.30 percent.

Полезно знать. One pair can have a pool at every enabled tier, so a tier is part of a pool's identity here rather than a property of the pair.

Params
pool, owner, closed, limit
Возвращает
{positions: [{pool, owner, tickLower, tickUpper, liquidity, deposited0/1, withdrawn0/1, collected0/1, lastBlock}]}

Open ranges and their sizes, newest activity first.

Полезно знать. A closed position is kept as a row of zeroes and is HIDDEN unless you pass `closed=1`. And `collected0`/`collected1` are principal and fees together - no event separates them, so subtracting to show fees earned is right only for a fully closed position.

Params
pool (required)
Возвращает
{ticks: [{tick, liquidityGross, liquidityNet}]}

The initialised ticks of one pool, in tick order - the input to a depth chart.

Полезно знать. `pool` is REQUIRED and its absence is a 400, not an empty list. Every tick of every pool in one answer would be a depth chart of nothing, so the service refuses rather than serving it.

Params
pool, sender, limit
Возвращает
{swaps: [{txHash, logIndex, block, pool, sender, recipient, amount0, amount1, sqrtPriceX96, liquidity, tick}]}

Newest first, with the pool's state as of that swap.

Полезно знать. The amounts are SIGNED, as the chain emits them: one side is always negative and that is which way the trade went. Summing them without taking absolute values nets a market to nothing.

Params
pool, owner, kind, limit
Возвращает
{events: [{txHash, logIndex, block, pool, kind, owner, sender, recipient, tickLower, tickUpper, liquidity, amount0, amount1}]}

Mints, burns and collects, newest first.

Полезно знать. A burn moves no tokens. It credits what is owed inside the pool and waits for a collect, so a burn and its collect are two events and only the second is money moving.

Цены возвращаются строками, вычисленные один раз

sqrtPriceX96 - это собственное хранилище пула, а price - это то, что покупает одна целая единица token0, с поправкой на десятичные знаки обоих токенов и отформатированное до восемнадцати значащих цифр. Вычисляя его самостоятельно из квадратного корня, легко ошибиться незаметно: деление до возведения в квадрат округляет, а возведение затем удваивает ошибку - именно так цена, равная ровно единице, возвращается длинной чередой девяток.

Что значат цифры стоимости

valueEthWei - это не TVL, и так подписывать его нельзя

Это сторона WETH пула, удвоенная, просуммированная по тем пулам, у которых сторона WETH есть. Это удвоение - стандартный способ оценить пул с постоянным произведением по одной стороне, и он осмыслен лишь потому, что эта сторона - собственный газовый актив сети. Пул без стороны WETH не вносит ничего вовсе и считается отдельно под poolsWithoutEth - так что цифра является нижней границей по подмножеству, а не итогом.

Концентрированные пулы в неё никогда не сворачиваются. Их две стороны стоят не одинаково, поэтому удвоить одну из них - это утверждение, которого данные не подтверждают: пул, цена которого ушла выше каждого диапазона, держит один токен и ничего из второго. Их цифры живут под concentrated и под wethBalanceWei, который не удвоен и назван ровно тем, чем является.

Несколько маршрутов несут также цифру в долларах и снимок цены. Они приходят из слоя, которым эта страница не владеет, - читайте его собственный справочник о том, что это число значит и какого оно возраста. Два правила отсюда всё же стоит унести: цифра равна null и никогда 0, когда её нельзя вывести, потому что ноль - это измерение; и цена за токен, вычисленная из резервов, не публикуется и не должна выводиться.

Индексатор за ним

It reads the sequencer's log spool in Postgres, not `eth_getLogs`. The node serves roughly two minutes of logs, so an indexer built on the RPC would see a window and present it as history.

Two streams, two topic sets, two cursors, and they never run in the same pass. The constant-product cursor has long been at the head of the chain; adding the concentrated topics to its filter would have skipped every such log already behind it, permanently and silently, so the concentrated stream starts at zero and backfills on its own.

  • It sleeps two seconds between passes once it has caught up, and takes at most 5000 logs per batch; both are environment variables.
  • The cursor moves only inside the transaction that wrote the rows, so a crash repeats a batch rather than skipping one.
  • Символы и десятичные знаки токенов читаются один раз на процесс и кешируются на всю его жизнь - не как оптимизация, а потому что узел считает eth_call через одно ведро, общее для всех его клиентов.

Практическое следствие - на /health: streams.cl может сильно отставать от streams.v2, и это нормально, а не сломано, потому что концентрированный поток догоняет с нуля, пока другой держится у головы. Сравнивайте каждый поток со spoolHeadBlock, а не друг с другом.

Ошибки

СтатусЗначение
400bad parameterany address or number the handler could not parse. The message is always the same string.
404unknown pool, or no such routethere is no route table - an unmatched path falls through to this.
503index not readya database error, including the ordinary case of the indexer never having run, so the tables do not exist yet. It carries the first line of the driver's own message.

Их всего три, кодов ошибок в теле нет, а строки сообщений фиксированы, а не описательны. Разбирайте по статусу. 400 в частности ничего не говорит о том, какой параметр был отвергнут.