Skip to content

Live Uniswap prices from the CLI and SDK

Leani’s subscribe command turns the built-in Uniswap V3 observation processor into a small live-price utility. It follows Ethereum directly and prints only matching pool updates to stdout; lifecycle and peer messages go to stderr, so stdout is safe to pipe.

CLI

After installing the binary, subscribe to the ETH/USDC 0.05% Mainnet pool:

Terminal window
leani subscribe uniswap-v3 ETH/USDC

That exact command is also the standalone demo. Auto mode first checks for a reachable node from the explicit endpoint or local configuration; when there is none, it starts the embedded runtime. Use --mode embedded only when a reachable local node should intentionally be ignored.

The first run asks three independent checkpoint providers for a recent finalized weak-subjectivity checkpoint. More than half of them, so at least two of the three, must return the exact same slot and block root, and no responding provider may report another root at that slot. Leani then shows every provider’s answer and asks you to accept the agreed one. Providers must use HTTPS, except on loopback, and their redirects are not followed. Execution then follows native P2P peers, while later Beacon API responses are proof-checked from that agreed root. For an intentional non-interactive invocation, use --yes.

To reproduce a completely cold embedded start, reset the reconstructible state for the exact protocol, market set, and finality mode, then subscribe again:

Terminal window
leani reset subscription uniswap-v3 ETH/USDC
leani subscribe uniswap-v3 ETH/USDC

The reset prints the exact hashed subscription directory and asks for confirmation; --yes is available for an intentional scripted reset. It removes only that embedded subscription’s checkpoint, peer cache, P2P identity, and SQLite state. It does not touch leani.toml or prepared-node state. Stop a matching embedded subscriber before resetting it. For a finalized subscription, pass --finality finalized to both commands.

The reset only removes a directory that leani subscribe marked as its own state with a .leani-subscription file: every directory it creates, and a subscription’s default directory from an earlier release once that subscription runs again. A node that takes over a marked directory, such as leani serve with that data_dir, removes the marker. The reset refuses any other directory, such as a node’s data_dir passed as --data-dir. An explicit --data-dir from an earlier release that already held state is not marked; delete it yourself once you have checked that it holds only subscription state.

To test the binary with no retained node or subscription state at all, stop every Leani process that uses the configured data directory and run:

Terminal window
leani reset all

This removes the entire resolved runtime data directory, including the node database and WAL, raw history, processor artifacts, checkpoints, execution-peer cache, P2P identity, and every embedded subscription. It preserves leani.toml and the binary. The command rejects broad targets that contain the working directory or configuration; use --yes only for intentional scripted cold-start tests. It first takes the lock of every embedded subscription under the data directory and refuses, deleting nothing, while one is running.

After the light client has verified a newer finality anchor and its consensus-committed execution hash, Leani persists that locally verified anchor. Ordinary restarts reuse it without contacting the checkpoint providers while it remains fresh. The transport can also be fully peer-to-peer after bootstrap:

Terminal window
leani subscribe uniswap-v3 ETH/USDC --finality-source p2p

P2P finality still needs an initial trust root. It reuses the local verified anchor when available; otherwise it performs the same checkpoint quorum once, then obtains and verifies light-client updates over consensus P2P instead of a Beacon API. Override the provider set with repeatable --checkpoint-url flags (or LEANI_CHECKPOINT_URLS) and the threshold with --checkpoint-quorum.

The default pretty stream looks like this:

2026-08-28T19:22:11Z ETH/USDC 2439.03084279 volume=0.601207569812525017 ETH block=25855798 unattested

Volume is the absolute swap amount in the first, or base, token of the market. For example, ETH/USDC reports ETH volume while WBTC/ETH reports WBTC volume.

One embedded runtime can filter several pools at once:

Terminal window
leani subscribe uniswap-v3 ETH/USDC ETH/USDT WBTC/ETH

Use the stable leani.market-price.v2 NDJSON contract for scripts. Its baseVolume string uses exact base-token decimal units:

Terminal window
leani subscribe uniswap-v3 ETH/USDC --format json |
jq --unbuffered 'select(.operation == "apply") | [.market, .price, .baseVolume] | @tsv'

--format raw exposes the underlying change envelope, and --once exits after one matching update. --finality finalized builds an embedded finalized-only processor that prints a price only after Ethereum consensus has finalized its block. The default unattested mode emits head updates and marks undo records when a block is undone (see finality levels).

An undo repeats the row of the observation it reverts, with "operation": "undo", however late it arrives; in raw mode it is the undo change envelope, whose data is null or the previous value, never the reverted one. An undo of a change the subscription saw but did not print, such as another pool’s, stays hidden. An undo of a change it never saw, because it was applied before the run started, is a leani.subscription-undo.v1 row naming the reverted blockNumber, blockHash, and change key while it is fresh; attached to a node with more pools, such a row can name another pool’s change. The JSON stream also carries leani.subscription-gap.v1 rows (see the block feed), so scripts should dispatch on each row’s schema. --once never exits on an undo or gap row.

Embedded mode does not bind API or RPC ports, activate unrelated processors, or start a historical backfill. It starts from an independently verified finalized execution anchor and connects native execution P2P. Output from the configured processor lane says unattested, attested or finalized; use --finality finalized to wait for a block Ethereum consensus has finalized. Finalized output still waits for the fully joined path. Its small SQLite store, locally verified checkpoint, and peer cache live under the platform data directory; use --data-dir PATH to select an explicit location. The durable lane of a completely cold run can take longer because public peer discovery and the finalized-to-head catch-up are network dependent; subsequent runs reuse verified canonical material and the peer cache. If a local configuration exists, embedded fallback seeds its private peer cache from the configured node context, but it never copies the node’s P2P identity or shares its writable database. Once verified startup is complete, Leani prints the newest still-fresh observation it acquired during that startup—one per requested market—instead of silently discarding those prices. It then continues with new swaps. Consequently, embedded --once returns the newest fresh startup price when one exists.

When a node is already running from a project-local leani.toml, run the same command from another terminal in that directory:

Terminal window
# One-time setup
leani init uniswap-v3 ETH/USDC
# Terminal 1
leani serve
# Terminal 2
leani subscribe uniswap-v3 ETH/USDC

Auto mode discovers the reachable api.bind in ./leani.toml and attaches; otherwise it starts the embedded runtime. Use --mode client --endpoint URL when attaching to a remote node should be mandatory rather than automatic; with --endpoint, neither mode reads a local configuration. The client takes a head cursor, reads the node’s query-only latest observation for an immediate still-fresh price, then opens SSE and applies the same timestamp freshness gate as embedded mode, so retained history and in-progress node catch-up are not presented as live prices. Before opening the stream, it reads the processor’s immutable pool catalog and rejects any requested market that the node does not process. Running-node subscriptions never mutate processor scope; all clients reuse the configured durable stream.

Nothing prints until the stream’s hello shows an Ethereum mainnet node (chain 1) serving uniswap-observations under the requested --processor, and every reconnect checks it again. A node for another chain or processor, or a service that is not a Leani node answering at api.bind, ends the command with an error instead of printing its stream. A stream error the node marks retryable reconnects from the last cursor. An attached --finality finalized subscription replays the node’s retained change log and prints each observation once the node’s finality marker covers its block, however late finality arrives, skipping blocks that were already final when it connected; the freshness gate applies only to unattested and attested output. See the block feed for the gap row it prints when the node stops reporting finality.

The initializer fetches a current majority checkpoint quorum and pins it in a compact configuration. It asks before accepting a new trust root and refuses to overwrite an existing file. endpoints holds the managed PublicNode and Lodestar Beacon API pool plus every responding checkpoint provider that also serves the Beacon light-client API, without duplicates. leani --config demo.toml init ... writes to a different path; --yes is available for an intentional non-interactive setup. A generated ETH/USDC file has this shape (the root and slot are live values):

config_version = 1
network = "ethereum-mainnet"
data_dir = "./data"
[finality]
checkpoint = "0x..."
checkpoint_slot = 15100000
endpoints = [
"https://ethereum-beacon-api.publicnode.com/",
"https://lodestar-mainnet.chainsafe.io/",
]
[uniswap]
markets = ["ETH/USDC"]
[api]
bind = "127.0.0.1:18080"

The compact document expands to the standard local-node profile: native execution P2P with an opportunistic minimum of one peer and a 16-peer healthy target, locally verified Beacon finality over the endpoints transport pool, which leani init fills with the ordinary PublicNode (https://ethereum-beacon-api.publicnode.com/) and Lodestar (https://lodestar-mainnet.chainsafe.io/) Beacon API endpoints and the responding checkpoint providers that serve the Beacon light-client API, Xatu history fallback, bounded 512 MiB memory and 2 GiB temporary-disk budgets, on-demand Uniswap history, unattested publication, undo records kept until 256 blocks past finality, 64 MiB/24 hour delivery retention, ephemeral P2P listener ports, RPC on 18545/18546, and the native API on 18080. Pool addresses, fee tiers, and the earliest processor block come from the same built-in market catalog as subscribe: ETH/USDC starts at its pool’s creation block, 12,376,729, and ETH/USDT and WBTC/ETH at the Uniswap V3 factory deployment block, 12,369,621, which no pool predates. The start block is part of the processor’s identity, so a release that moves one makes the node refuse the state it kept and name the ways to move on (see the changelog). Operators who need to tune any of these can use the full configuration schema instead. The pinned checkpoint is the trust root; Beacon API endpoints only transport untrusted consensus data and never provide execution blocks or Uniswap observations. Leani queries those transports concurrently and verifies their responses locally. After the first verified response it waits about two seconds for the others, then follows the highest finalized slot that the configured agreement threshold of endpoints has reached or passed; with the compact default of one, the most recent verified finality wins, and finality never moves backwards. The node needs the pinned checkpoint only for its first start: it then keeps the newest verified finality anchor in data_dir and restarts from it, so the checkpoint does not expire while the node keeps verifying finality (see the runbook’s checkpoint lifecycle).

TypeScript SDK

The node API deliberately publishes exact raw pool state. This example derives USDC per ETH with bigint, preserving the V3 ratio until the final decimal rendering:

import { createLeaniClient } from "@smart-byte/leani-sdk";
const leani = createLeaniClient({ baseUrl: "http://127.0.0.1:18080" });
const Q192 = 1n << 192n;
function decimalRatio(numerator: bigint, denominator: bigint, places = 8) {
const scale = 10n ** BigInt(places);
const rounded = (numerator * scale + denominator / 2n) / denominator;
const digits = rounded.toString().padStart(places + 1, "0");
return `${digits.slice(0, -places)}.${digits.slice(-places)}`;
}
function usdcPerEth(sqrtPriceX96: string) {
const sqrt = BigInt(sqrtPriceX96);
// Pool token0 is USDC (6 decimals), token1 is WETH (18 decimals).
return decimalRatio(Q192 * 10n ** 18n, sqrt * sqrt * 10n ** 6n);
}
function tokenAmount(rawAmount: string, decimals: number) {
const signed = BigInt(rawAmount);
const amount = signed < 0n ? -signed : signed;
const digits = amount.toString().padStart(decimals + 1, "0");
const integer = digits.slice(0, -decimals);
const fraction = digits.slice(-decimals).replace(/0+$/, "");
return fraction ? `${integer}.${fraction}` : integer;
}
for await (const change of leani.uniswap.subscribe()) {
if (
change.operation === "apply" &&
change.data?.pool.toLowerCase() ===
"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640" &&
change.data.sqrtPriceX96 &&
change.data.amount1
) {
console.log({
price: usdcPerEth(change.data.sqrtPriceX96),
baseVolume: tokenAmount(change.data.amount1, 18),
});
}
}

The CLI uses the same integer formulas. It does not convert sqrtPriceX96 or swap amounts to floating-point numbers.

Built-in Mainnet markets

Market Fee tier Pool
ETH/USDC 0.05% 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
ETH/USDT 0.30% 0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36
WBTC/ETH 0.30% 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD