alina schanz

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

contents
  1. record
  2. readme
  3. files
  4. history
  5. used in

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 most liquidity() wins. slot0().sqrtPriceX96 is 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 get n/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_call over json-rpc via urllib; endpoints publicnode, drpc, mevblocker, tenderly, blastapi in that order, or --rpc yours. 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

  • dev is spot minus one in basis points. ! marks at least --warn bp (default 50).
  • liquidity is 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

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

history

  1. fcaa690github-actions[bot]no signature

  2. ff47b59github-actions[bot]no signature

  3. 1483d77github-actions[bot]no signature

  4. dc40020github-actions[bot]no signature

  5. 009bbfegithub-actions[bot]no signature

  6. d4d0142github-actions[bot]no signature

  7. 1aaf88agithub-actions[bot]no signature

  8. 508f2b9megood signature from 17BA…F3AB

  9. 9e26dbfmegood signature from 17BA…F3AB

  10. afd4b06megood signature from 17BA…F3AB

the latest 10 commits. all 19, with the signature of each: the commit list.

used in