面向开发者 / 应用 API
Pepper DEX API
一个建在 Pepper 日志之上的只读索引,从两组表里提供两代池子。它以十进制字符串作答,把每一份列表悄悄钳制在五百行,而且根本不发送任何 CORS 头。
两个 DEX,一个 API
Pepper 的恒定乘积交易对和集中流动性池是并排存在的。这个服务从两组表里、在两个前缀下同时提供 两者:裸路径是恒定乘积那一批,而 /cl/ 底下的一切是集中流动性那一批。它们不是 彼此的变体。
这两个家族不可互换
一个恒定乘积交易对有两个储备和一个价格。一个集中流动性池两者都没有:它有一个价格、一个 tick、在那个 tick 处活跃的流动性,以及一组根本不活跃的、无界的区间。把 activeLiquidity 读成深度、或者把一个余额读成可成交规模,错的方式正是那种要花 钱的方式:当前价格处的深度可能只有余额的千分之一,其余都坐在下一笔交易永远碰不到的区间里。
一个池子地址只属于一个家族。拿 /pool/{address} 去问一个集中流动性 池,得到的是 404,不是一次重定向。
在 Pepper 上发行的代币,在被收录进公开的代币列表之前都没有已验证标记,而 Pickle 应用显示的已收录代币的 logo、名称和符号也都取自这份列表。想被收录,就提一个带上 logo 和条目的 pull request,步骤和标准见代币列表与地址标签。
基础 URL 与 CORS
| 接入点 | Value |
|---|---|
| 公开 | https://pepper.picklechain.xyz/api/Pepper 站点来源上的一个 nginx 挂载点 |
| 你自己运行的一套栈 | 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。
恒定乘积路由
/health
GET- 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.
/pools
GET- 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.
/pool/{pair}
GET- 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.
/tokens
GET- 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.
/swaps
GET- 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.
/liquidity
GET- 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.
/stats
GET- 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.
# 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'集中流动性路由
/cl/pools
GET- 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.
/cl/tiers
GET- 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.
/cl/ticks
GET- 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.
/cl/swaps
GET- 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.
/cl/events
GET- 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 能买到 多少,已按两个代币的小数位调整过,并格式化到十八位有效数字。自己从平方根算它很容易出微妙的 错:先除后平方会取整,而平方又把误差放大一倍,这正是一个正好为一的价格会回来变成一长串 9 的原因。
那些价值数字是什么意思
valueEthWei 不是 TVL,也不得被贴上这个标签
它是一个池子的 WETH 那一边,翻倍,再在那些有 WETH 边的池子上求和。那个翻倍是从一边 给一个恒定乘积池估值的标准做法,而它之所以有意义,只是因为那一边是这条链自己的 gas 资产。 一个没有 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 比较,而不是和另一条比较。
错误
| 状态码 | 含义 |
|---|---|
| 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. |
一共只有三个,请求体里没有任何错误码,而那些消息字符串是固定的而不是描述性的。请匹配状态码。 尤其是一个 400,它完全不说明是哪一个参数被拒绝了。