codepythonmit
stablepeg
whether each stablecoin still trades at a dollar: uniswap v3 spot and a ten-minute twap from the deepest pool, a curve 3pool quote for 100k, coingecko next to it.
head fcaa6902026-09-1526 files463 lines of python
record
- commits
- 19 on main, 2026-09-08 to 2026-09-15
- signed
- 10 good signature from 17BA…F3AB
9 from the daily data workflow no signature - tests
- 15 passed 3 skipped
python 3.11.9 on my machine, 2026-09-23 - ci
- ci.yml lists python 3.10, 3.12, 3.13.
- history
- every commit up to fcaa690, each with its signed record. the history itself stays off this site, so from here you cannot tie a file to a commit.
- source
- stablepeg.tar.gz, 17.0 KB · sha256
cd34c4cd834a…: the files at fcaa690, without README.md, which is shown below. - license
- mit (LICENSE)
readme
rendered from README.md at fcaa690. i left out the badges, which load from github and shields.io, and one link, which shows as plain text. the install lines point at github, which answers 404 for this account at the moment.
stablepeg
are the stablecoins still a dollar? for each coin: spot and a 10 minute twap from its deepest uniswap v3 pool against usdc, what a 100k swap returns on curve 3pool, and the coingecko price next to it for a sanity check. everything but the last column is read straight from ethereum mainnet over a public rpc. no keys, nothing to install.
$ stablepeg
stablecoins vs USDC on ethereum mainnet, block 25,929,333 (2026-09-08 01:22 utc)
USDC itself: $0.9999 on coingecko
coin v3 pool spot twap 10m dev liquidity coingecko
USDT 0.01% 0.99998 1.00000 -0.2bp 25.0e15 0.9999
DAI 0.01% 1.00001 1.00000 +0.1bp 9108.4e18 0.9998
USDe 0.01% 0.99997 0.99990 -0.3bp 4973.2e18 0.9997
FRAX 0.05% 0.99164 0.99164 -83.6bp 11.4e18 0.9921 !
LUSD 0.05% 1.00889 1.00884 +88.9bp 11.9e15 1.0090 !
PYUSD 0.01% 1.00004 1.00000 +0.4bp 709.8e12 0.9998
crvUSD 0.3% 1.00167 1.00170 +16.7bp 526.0e15 0.9999
GHO 0.01% 0.99902 0.99900 -9.8bp 54.6e18 0.9990
USDS 0.3% 0.99877 0.99880 -12.3bp 6.0e18 0.9998
curve 3pool: 100,000 USDT -> 99,987 USDC (-1.3bp), 100,000 DAI -> 99,985 USDC (-1.5bp), virtual price 1.0398
! = at least 50 bp off the peg: FRAX, LUSD
usdt, dai and usde inside one basis point is what "pegged" looks like. frax and lusd at 80-90 bp is not news, they trade like that most weeks; the flag is there so a real depeg would not hide in a wall of numbers.
install
pipx install git+https://github.com/alinaschanz/stablepeg
or clone it and run python -m stablepeg from the folder. python 3.10 or newer, no dependencies.
use
stablepeg # the usual suspects against usdc
stablepeg --coins USDT,DAI,FDUSD,TUSD --quote USDT
stablepeg --twap 1800 --size 1000000 # 30 minute twap, 1m curve swap
stablepeg --warn 20 # flag at 20 bp
stablepeg --depth # what a 100k / 1m / 10m sale returns, impact in bp (quoter v2)
stablepeg --json
stablepeg --no-coingecko --no-curve # chain only
stablepeg --summary-append data/daily.csv --quiet # one row per coin, the daily dataset
coins on offer: USDC, USDT, DAI, USDe, FRAX, LUSD, TUSD, PYUSD, crvUSD, GHO, USDS, FDUSD.
the live test checks each address against symbol() and decimals() on chain, and the
curve pool coin order against coins(i).
how it works
- uniswap v3 factory
getPool(a, b, fee)for the 0.01%, 0.05%, 0.3% and 1% tiers; the pool with the mostliquidity()wins.slot0().sqrtPriceX96is the spot price: token1 per token0 = (sqrtPriceX96 / 2^96)^2 x 10^(dec0 - dec1), flipped when the coin is token1. observe([600, 0])gives tick cumulatives; their difference over the window, floored, is the mean tick, and 1.0001^tick is the twap. pools with too few observations answer "OLD" and getn/a.- curve 3pool
get_dy(i, j, dx)for a 100k swap is an effective price with the fee in it, which is the number a real seller gets.get_virtual_price()is printed for the curious. - coingecko simple price for the last column; skip it with
--no-coingecko. - every call is an
eth_callover json-rpc viaurllib; endpoints publicnode, drpc, mevblocker, tenderly, blastapi in that order, or--rpcyours. the fee tiers and the coins are fetched in parallel.
--depth asks the uniswap quoter v2
what selling 100k, 1m and 10m of the coin into the same pool would return; the impact is the
effective price against the spot. a peg that holds for 100k and breaks at 10m is the interesting case.
this is one uniswap pool: for dai and the newer coins most of the depth lives on curve and in the
maker psm, so a big impact here reads as "not on uniswap", not as "depegged".
reading the table
devis spot minus one in basis points.!marks at least--warnbp (default 50).liquidityis uniswap's L (sqrt of x times y in raw units). it is only comparable within a pair, so read it as "which tier the price came from", not as a ranking of coins.- a 0.01% pool with real liquidity and a twap that agrees with the spot is a price you can trust. a 1% pool with a wide gap between spot and twap is a thin market, and the coingecko column will usually disagree with it.
the dataset
data/daily.csv gets one row per coin every night at 00:27 utc (spot, twap, pool, coingecko,
curve): a small peg history that anyone can plot. columns are described in
data/README.md; a rerun for the same day replaces the day.
exit codes and scripting
0 after a table, 2 when a coin is unknown or no rpc answered. --json carries everything the table
shows plus the raw pool addresses and liquidity, so stablepeg --json | jq '.pools[] | select(.spot < 0.995)'
is a depeg alert in one line; --summary-append is the same data as rows, one per coin per day.
see also
- bigmoves: the large transfers of these coins
- onchain-notes, gasweek, ens-lookup
- the notes: alinaschanz.life, the short version on x
verify a release
from the next release on, every release carries the sdist and the wheel, a SHA256SUMS file, an
opentimestamps proof of that file, and a build provenance attestation made in github's own signing
flow. with the files downloaded into one folder:
sha256sum -c SHA256SUMS
gh attestation verify ./*.whl --owner alinaschanz
ots verify SHA256SUMS.ots
the attestation names the commit and the workflow run that produced the file; the timestamp proves the checksums existed before a certain bitcoin block; the commit itself is signed.
license
mit. numbers, not calls. not financial advice.
files
| path | lines | bytes |
|---|---|---|
.editorconfig# | 15 | 233 |
.github/dependabot.yml# | 8 | 237 |
.github/workflows/ci.yml# | 47 | 1,156 |
.github/workflows/daily.yml# | 29 | 864 |
.github/workflows/pypi.yml# | 30 | 1,101 |
.github/workflows/release.yml# | 43 | 1,519 |
.github/workflows/scorecard.yml# | 33 | 889 |
.gitignore# | 7 | 66 |
.pre-commit-config.yaml# | 13 | 321 |
CHANGELOG.md# | 20 | 819 |
LICENSE# | 21 | 1,069 |
README.md# | shown above | |
data/README.md# | 10 | 500 |
data/daily.csv# | 73 | 6,203 |
pyproject.toml# | 42 | 1,160 |
stablepeg/__init__.py# | 2 | 140 |
stablepeg/__main__.py# | 5 | 52 |
stablepeg/abi.py# | 62 | 1,960 |
stablepeg/cli.py# | 203 | 10,084 |
stablepeg/coins.py# | 49 | 1,966 |
stablepeg/curve.py# | 38 | 1,148 |
stablepeg/quoter.py# | 34 | 1,471 |
stablepeg/rpc.py# | 71 | 2,963 |
stablepeg/uniswap.py# | 112 | 4,324 |
tests/test_live.py# | 32 | 1,184 |
tests/test_math.py# | 169 | 7,850 |
history
the latest 10 commits. all 19, with the signature of each: the commit list.