Para desarrolladores / API de las aplicaciones
La API del DEX Pepper
Un índice de solo lectura sobre los logs de Pepper, que sirve dos generaciones de pool desde dos juegos de tablas. Responde en cadenas decimales, acota cada lista a quinientas filas sin decirlo, y no envía ninguna cabecera CORS.
Dos DEX, una API
Pepper tiene pares de producto constante y pools de liquidez concentrada uno al lado del otro. Este servicio sirve ambos, desde dos juegos de tablas, bajo dos prefijos: las rutas desnudas son las de producto constante y todo lo que cuelga de /cl/ es concentrado. No son variantes la una de la otra.
Las dos familias no son intercambiables
Un par de producto constante tiene dos reservas y un precio. Un pool concentrado no tiene ni lo uno ni lo otro: tiene un precio, un tick, la liquidez activa en ese tick, y un conjunto no acotado de rangos que no están activos en absoluto. Leer activeLiquidity como profundidad, o un saldo como tamaño negociable, está mal de la forma precisa que cuesta dinero - la profundidad al precio actual puede valer una milésima del saldo, con el resto reposando en rangos que la siguiente operación nunca toca.
Una dirección de pool pertenece a una sola familia. Pedir /pool/{address} para un pool concentrado da un 404, no una redirección.
Un token lanzado en Pepper no tiene marca de verificación hasta que está en la lista pública de tokens, que es también de donde las aplicaciones de Pickle toman el logo, el nombre y el símbolo de un token listado. Llegar a ella es una pull request con su logo y su entrada - Lista de tokens y etiquetas de dirección tiene los pasos y los criterios.
URL de base y CORS
| Punto de acceso | Value |
|---|---|
| Público | https://pepper.picklechain.xyz/api/un montaje nginx sobre el origen del sitio Pepper |
| Una pila propia | http://127.0.0.1:4020el puerto propio del servicio |
| CORS | ningunosin cabecera, sin manejador OPTIONS, ningún verbo salvo GET |
Un navegador en otro origen no puede llamar a este servicio
No hay en ninguna parte una cabecera access-control-allow-origin ni un do_OPTIONS que responda a un preflight. Un fetch desde una página de su propio dominio falla mientras la petición idéntica desde curl o desde un servidor funciona, que es el fallo que todo el mundo confunde con una avería de red. Póngala tras su propio origen, o llámela desde un backend.
Cada respuesta se envía con no-store y x-content-type-options: nosniff. No hay límite de tasa en el servicio: lo que acota a quien llama es el acotamiento sobre limit, que vale 500 en cada ruta de lista con un valor por defecto de 50.
Las rutas de producto constante
/health
GET- Params
- none
- Devuelve
- {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.
Conviene saberlo. 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.
/pools
GET- Params
- none
- Devuelve
- {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.
Conviene saberlo. 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.
/pool/{pair}
GET- Params
- the pair address in the path, matched case-insensitively
- Devuelve
- one pool object, plus ethUsd
One pool, in the same shape as a row of /pools.
Conviene saberlo. 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.
/tokens
GET- Params
- none
- Devuelve
- {tokens: [{address, symbol, decimals, ...}]}
Every token seen on either side of a constant-product pool, which is what a swap picker needs.
Conviene saberlo. 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.
/swaps
GET- Params
- pair, sender, limit
- Devuelve
- {swaps: [{txHash, logIndex, block, pair, sender, recipient, amount0In, amount1In, amount0Out, amount1Out}]}
Newest first, filterable by pair and by sender, both exact addresses.
Conviene saberlo. 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.
/liquidity
GET- Params
- pair, limit
- Devuelve
- {events: [{txHash, logIndex, block, pair, kind, sender, recipient, amount0, amount1}]}
Mints and burns, newest first, `kind` being one of those two words.
Conviene saberlo. `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.
/stats
GET- Params
- none
- Devuelve
- {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.
Conviene saberlo. `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.
# 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'Las rutas concentradas
/cl/pools
GET- Params
- none
- Devuelve
- {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.
Conviene saberlo. `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
- Devuelve
- one pool object, plus ethUsd
One concentrated pool. Unlike /pool/{pair} this one is a keyed lookup, so it is cheap.
Conviene saberlo. 404 when unknown. A malformed address is a 400.
/cl/tiers
GET- Params
- none
- Devuelve
- {tiers: [{fee, tickSpacing, enabledBlock}]}
The fee tiers the factory has enabled, in fee order. `fee` is in millionths, so 3000 is 0.30 percent.
Conviene saberlo. 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
- Devuelve
- {positions: [{pool, owner, tickLower, tickUpper, liquidity, deposited0/1, withdrawn0/1, collected0/1, lastBlock}]}
Open ranges and their sizes, newest activity first.
Conviene saberlo. 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.
/cl/ticks
GET- Params
- pool (required)
- Devuelve
- {ticks: [{tick, liquidityGross, liquidityNet}]}
The initialised ticks of one pool, in tick order - the input to a depth chart.
Conviene saberlo. `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.
/cl/swaps
GET- Params
- pool, sender, limit
- Devuelve
- {swaps: [{txHash, logIndex, block, pool, sender, recipient, amount0, amount1, sqrtPriceX96, liquidity, tick}]}
Newest first, with the pool's state as of that swap.
Conviene saberlo. 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.
/cl/events
GET- Params
- pool, owner, kind, limit
- Devuelve
- {events: [{txHash, logIndex, block, pool, kind, owner, sender, recipient, tickLower, tickUpper, liquidity, amount0, amount1}]}
Mints, burns and collects, newest first.
Conviene saberlo. 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 es el almacenamiento propio del pool y price es lo que compra una unidad entera de token0, ajustado por los decimales de ambos tokens y formateado a dieciocho cifras significativas. Calcularlo uno mismo desde la raíz cuadrada es fácil de errar de forma sutil: dividir antes de elevar al cuadrado redondea, y elevar al cuadrado duplica después el error, que es como un precio de exactamente uno vuelve convertido en una larga fila de nueves.
Qué significan las cifras de valor
valueEthWei no es la TVL y no debe etiquetarse así
Es el lado WETH de un pool, doblado, sumado sobre los pools que tienen lado WETH. Ese doblado es la manera estándar de valorar un pool de producto constante desde un solo lado y solo tiene sentido porque ese lado es el activo de gas de la propia cadena. Un pool sin lado WETH no aporta nada en absoluto y se cuenta aparte bajo poolsWithoutEth - la cifra es, pues, una cota inferior sobre un subconjunto, no un total.
Los pools concentrados nunca se pliegan dentro de ella. Sus dos lados no valen lo mismo, así que doblar uno de ellos es una afirmación que los datos no sostienen: un pool cuyo precio ha subido por encima de todos los rangos tiene un token y nada del otro. Sus cifras viven bajo concentrated y bajo wethBalanceWei, que no está doblado y lleva el nombre de exactamente lo que es.
Varias rutas llevan además una cifra en dólares y una instantánea de precio. Vienen de una capa que esta página no posee - lea su propia referencia para saber qué significa el número y qué antigüedad tiene. Dos reglas de aquí merecen llevarse de todos modos: la cifra vale null y nunca 0 cuando no se puede derivar, porque un cero es una medida; y un precio por token calculado desde las reservas no se publica y no debe inferirse.
El indexador que hay detrá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.
- Los símbolos y los decimales de los tokens se leen una vez por proceso y se guardan en caché durante toda su vida - no por optimización, sino porque el nodo mide
eth_calla través de un único cubo compartido por todos sus clientes.
La consecuencia práctica está en /health: streams.cl puede ir muy por detrás de streams.v2 y eso es normal más que roto, porque el flujo concentrado se pone al día desde cero mientras el otro se queda en la punta. Compare cada flujo con spoolHeadBlock, no con el otro.
Errores
| Estado | Significado |
|---|---|
| 400 | bad parameterany address or number the handler could not parse. The message is always the same string. |
| 404 | unknown pool, or no such routethere is no route table - an unmatched path falls through to this. |
| 503 | index 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. |
Solo hay tres, no hay códigos de error en el cuerpo, y las cadenas de mensaje son fijas más que descriptivas. Decida sobre el estado. Un 400 en particular no dice nada sobre qué parámetro fue rechazado.