Para desenvolvedores / APIs das aplicações

A API do DEX Pepper

Um índice somente de leitura sobre os logs do Pepper, servindo duas gerações de pool a partir de dois conjuntos de tabelas. Ele responde em strings decimais, trava cada lista em quinhentas linhas sem dizer, e não envia nenhum cabeçalho CORS.

Dois DEX, uma API

O Pepper tem pares de produto constante e pools concentrados lado a lado. Este serviço serve os dois, a partir de dois conjuntos de tabelas, sob dois prefixos: os caminhos nus são os de produto constante e tudo o que está sob /cl/ é concentrado. Não são variantes um do outro.

As duas famílias não são intercambiáveis

Um par de produto constante tem duas reservas e um preço. Um pool concentrado não tem nem uma coisa nem outra: ele tem um preço, um tick, a liquidez ativa naquele tick, e um conjunto não limitado de faixas que não estão ativas de forma alguma. Ler activeLiquidity como profundidade, ou um saldo como tamanho negociável, é errado do jeito preciso que custa dinheiro - a profundidade no preço corrente pode valer um milésimo do saldo, com o resto parado em faixas que a negociação seguinte nunca toca.

Um endereço de pool pertence a uma única família. Pedir /pool/{address} para um pool concentrado dá 404, não um redirecionamento.

Lançou um token? Dê a ele um logo e a marca de verificado

Um token lançado no Pepper não tem a marca de verificado enquanto não estiver na lista pública de tokens, que é também de onde as aplicações Pickle tiram o logo, o nome e o símbolo de um token listado. Chegar lá é um pull request com o logo e a entrada dele - Lista de tokens e tags de endereço traz os passos e os critérios.

URL de base e CORS

Ponto de acessoValue
Públicohttps://pepper.picklechain.xyz/api/uma montagem nginx na origem do site Pepper
Uma pilha suahttp://127.0.0.1:4020a porta própria do serviço
CORSnenhumsem cabeçalho, sem manipulador OPTIONS, nenhum verbo além de GET

Um navegador em outra origem não consegue chamar este serviço

Não há em lugar nenhum um cabeçalho access-control-allow-origin nem um do_OPTIONS para responder a um preflight. Um fetch a partir de uma página do seu próprio domínio falha enquanto a requisição idêntica a partir do curl ou de um servidor tem sucesso, que é a falha que todo mundo confunde com um problema de rede. Coloque-o atrás da sua própria origem, ou chame-o de um backend.

Toda resposta é enviada com no-store e x-content-type-options: nosniff. Não há limite de taxa no serviço: o que limita quem chama é a trava no limit, que vale 500 em toda rota de lista, com um padrão de 50.

As rotas de produto constante

Params
none
Retorno
{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.

Vale saber. 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
Retorno
{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.

Vale saber. 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
Retorno
one pool object, plus ethUsd

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

Vale saber. 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
Retorno
{tokens: [{address, symbol, decimals, ...}]}

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

Vale saber. 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
Retorno
{swaps: [{txHash, logIndex, block, pair, sender, recipient, amount0In, amount1In, amount0Out, amount1Out}]}

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

Vale saber. 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
Retorno
{events: [{txHash, logIndex, block, pair, kind, sender, recipient, amount0, amount1}]}

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

Vale saber. `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
Retorno
{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.

Vale saber. `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'

As rotas concentradas

Params
none
Retorno
{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.

Vale saber. `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
Retorno
one pool object, plus ethUsd

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

Vale saber. 404 when unknown. A malformed address is a 400.

Params
none
Retorno
{tiers: [{fee, tickSpacing, enabledBlock}]}

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

Vale saber. 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
Retorno
{positions: [{pool, owner, tickLower, tickUpper, liquidity, deposited0/1, withdrawn0/1, collected0/1, lastBlock}]}

Open ranges and their sizes, newest activity first.

Vale saber. 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)
Retorno
{ticks: [{tick, liquidityGross, liquidityNet}]}

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

Vale saber. `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
Retorno
{swaps: [{txHash, logIndex, block, pool, sender, recipient, amount0, amount1, sqrtPriceX96, liquidity, tick}]}

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

Vale saber. 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
Retorno
{events: [{txHash, logIndex, block, pool, kind, owner, sender, recipient, tickLower, tickUpper, liquidity, amount0, amount1}]}

Mints, burns and collects, newest first.

Vale saber. 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.

Os preços voltam em strings, calculados uma vez só

sqrtPriceX96 é o armazenamento próprio do pool e price é o que uma unidade inteira de token0 compra, ajustado para as decimais dos dois tokens e formatado com dezoito algarismos significativos. Calculá-lo você mesmo a partir da raiz quadrada é fácil de errar sutilmente: dividir antes de elevar ao quadrado arredonda, e o quadrado depois dobra o erro - é assim que um preço de exatamente um volta como uma longa sequência de noves.

O que querem dizer os números de valor

valueEthWei não é a TVL e não deve ser rotulado assim

É o lado WETH de um pool, dobrado, somado sobre os pools que têm um lado WETH. Esse dobro é a forma padrão de valorizar um pool de produto constante por um único lado e só faz sentido porque esse lado é o ativo de gas da própria cadeia. Um pool sem lado WETH não contribui com nada e é contado à parte sob poolsWithoutEth - o número é, portanto, um limite inferior sobre um subconjunto, não um total.

Os pools concentrados nunca são dobrados para dentro dele. Os dois lados deles não valem a mesma coisa, então dobrar um deles é uma afirmação que os dados não sustentam: um pool cujo preço subiu acima de todas as faixas detém um token e nada do outro. Os números deles vivem sob concentrated e sob wethBalanceWei, que não é dobrado e é nomeado por exatamente aquilo que é.

Várias rotas também carregam um número em dólares e um instantâneo de preço. Eles vêm de uma camada que esta página não possui - leia a referência dela para saber o que o número significa e que idade tem. Duas regras daqui valem levar junto assim mesmo: o número vale null e nunca 0 quando não pode ser derivado, porque um zero é uma medida; e um preço por token calculado a partir das reservas não é publicado e não deve ser inferido.

O indexador por trás

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.
  • Os símbolos e as decimais dos tokens são lidos uma vez por processo e guardados em cache por toda a vida dele - não por otimização, mas porque o nó mede o eth_call através de um único balde compartilhado por todos os seus clientes.

A consequência prática está no /health: streams.cl pode estar bem atrás de streams.v2 e isso é normal em vez de quebrado, porque o fluxo concentrado recupera desde zero enquanto o outro fica na ponta. Compare cada fluxo com o spoolHeadBlock, não com o outro.

Erros

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

São só três, não há código de erro no corpo, e as strings de mensagem são fixas em vez de descritivas. Decida pelo status. Um 400, em particular, não diz nada sobre qual parâmetro foi rejeitado.