Pour les développeurs / API des applications

L'API du DEX Pepper

Un index en lecture seule sur les logs de Pepper, servant deux générations de pool depuis deux jeux de tables. Il répond en chaînes décimales, écrête chaque liste à cinq cents lignes sans le dire, et n'envoie aucun en-tête CORS.

Deux DEX, une API

Pepper a des paires à produit constant et des pools concentrés côte à côte. Ce service sert les deux, depuis deux jeux de tables, sous deux préfixes : les chemins nus sont ceux à produit constant et tout ce qui est sous /cl/ est concentré. Ce ne sont pas des variantes l'une de l'autre.

Les deux familles ne sont pas interchangeables

Une paire à produit constant a deux réserves et un prix. Un pool concentré n'a ni l'un ni l'autre : il a un prix, un tick, la liquidité active à ce tick, et un ensemble non borné de plages qui ne sont pas actives du tout. Lire activeLiquidity comme de la profondeur, ou un solde comme une taille échangeable, est faux de la façon précise qui coûte de l'argent - la profondeur au prix courant peut valoir un millième du solde, le reste reposant dans des plages que l'échange suivant ne touche jamais.

Une adresse de pool appartient à une seule famille. Demander /pool/{address} pour un pool concentré donne un 404, pas une redirection.

Vous avez lancé un jeton ? Donnez-lui un logo et la marque de vérification

Un jeton lancé sur Pepper n'a pas de marque de vérification tant qu'il n'est pas sur la liste publique de jetons, qui est aussi l'endroit où les applications Pickle prennent le logo, le nom et le symbole d'un jeton listé. Y figurer passe par une pull request avec son logo et son entrée - Liste de jetons et étiquettes d'adresse donne les étapes et les critères.

URL de base et CORS

Point d'accèsValue
Publichttps://pepper.picklechain.xyz/api/un montage nginx sur l'origine du site Pepper
Une pile à voushttp://127.0.0.1:4020le port propre du service
CORSaucunpas d'en-tête, pas de gestionnaire OPTIONS, aucun verbe sauf GET

Un navigateur sur une autre origine ne peut pas appeler ce service

Il n'y a nulle part d'en-tête access-control-allow-origin et pas de do_OPTIONS pour répondre à un préflight. Un fetch depuis une page de votre propre domaine échoue alors que la requête identique depuis curl ou un serveur réussit, ce qui est l'échec que tout le monde prend pour une panne réseau. Mettez-le derrière votre propre origine, ou appelez-le depuis un backend.

Chaque réponse est envoyée en no-store avec x-content-type-options: nosniff. Il n'y a pas de limite de débit dans le service : ce qui borne un appelant est l'écrêtage sur limit, qui vaut 500 sur chaque route de liste avec une valeur par défaut de 50.

Les routes à produit constant

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

Bon à savoir. 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
Retour
{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.

Bon à savoir. 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
Retour
one pool object, plus ethUsd

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

Bon à savoir. 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
Retour
{tokens: [{address, symbol, decimals, ...}]}

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

Bon à savoir. 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
Retour
{swaps: [{txHash, logIndex, block, pair, sender, recipient, amount0In, amount1In, amount0Out, amount1Out}]}

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

Bon à savoir. 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
Retour
{events: [{txHash, logIndex, block, pair, kind, sender, recipient, amount0, amount1}]}

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

Bon à savoir. `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
Retour
{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.

Bon à savoir. `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'

Les routes concentrées

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

Bon à savoir. `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
Retour
one pool object, plus ethUsd

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

Bon à savoir. 404 when unknown. A malformed address is a 400.

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

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

Bon à savoir. 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
Retour
{positions: [{pool, owner, tickLower, tickUpper, liquidity, deposited0/1, withdrawn0/1, collected0/1, lastBlock}]}

Open ranges and their sizes, newest activity first.

Bon à savoir. 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)
Retour
{ticks: [{tick, liquidityGross, liquidityNet}]}

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

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

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

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

Mints, burns and collects, newest first.

Bon à savoir. 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.

Les prix reviennent en chaînes, calculés une seule fois

sqrtPriceX96 est le stockage propre du pool et price est ce qu'achète une unité entière de token0, ajusté pour les décimales des deux jetons et formaté à dix-huit chiffres significatifs. Le calculer soi-même depuis la racine carrée est facile à rater subtilement : diviser avant d'élever au carré arrondit, et l'élévation double ensuite l'erreur - c'est ainsi qu'un prix d'exactement un revient en une longue suite de neuf.

Ce que veulent dire les chiffres de valeur

valueEthWei n'est pas la TVL et ne doit pas être étiqueté ainsi

C'est le côté WETH d'un pool, doublé, sommé sur les pools qui ont un côté WETH. Ce doublement est la façon standard de valoriser un pool à produit constant depuis un seul côté et il n'a de sens que parce que ce côté est l'actif de gas de la chaîne. Un pool sans côté WETH ne contribue rien du tout et est compté à part sous poolsWithoutEth - le chiffre est donc une borne inférieure sur un sous-ensemble, pas un total.

Les pools concentrés n'y sont jamais repliés. Leurs deux côtés ne valent pas la même chose, donc doubler l'un d'eux est une affirmation que les données ne soutiennent pas : un pool dont le prix est monté au-dessus de chaque plage détient un jeton et pas l'autre. Leurs chiffres vivent sous concentrated et sous wethBalanceWei, qui n'est pas doublé et est nommé pour exactement ce qu'il est.

Plusieurs routes portent aussi un chiffre en dollars et un instantané de prix. Ils viennent d'une couche que cette page ne possède pas - lisez sa propre référence pour ce que le nombre signifie et quel âge il a. Deux règles d'ici valent tout de même d'être emportées : le chiffre vaut null et jamais 0 quand il ne peut pas être dérivé, parce qu'un zéro est une mesure ; et un prix par jeton calculé depuis les réserves n'est pas publié et ne doit pas être inféré.

L'indexeur derrière

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.
  • Les symboles et les décimales des jetons sont lus une fois par processus et mis en cache pour toute sa vie - non par optimisation mais parce que le nœud mesure eth_call à travers un seul seau partagé par tous ses clients.

La conséquence pratique est sur /health : streams.cl peut être très en retard sur streams.v2 et c'est normal plutôt que cassé, parce que le flux concentré rattrape depuis zéro pendant que l'autre reste à la pointe. Comparez chaque flux à spoolHeadBlock, pas à l'autre.

Erreurs

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

Il n'y en a que trois, il n'y a pas de code d'erreur dans le corps, et les chaînes de message sont fixes plutôt que descriptives. Branchez-vous sur le statut. Un 400 en particulier ne dit rien sur quel paramètre a été rejeté.