LiveProvider
LiveProvider reads pool state directly from a chain RPC and turns it into a PoolSnapshot. From there, StateTwinBuilder constructs a usable exchange object — and every primitive in DeFiPy works against it the same way it works against a MockProvider recipe.
v2.1 supports Uniswap V2 and V3. Balancer and Stableswap LiveProvider implementations are v2.2 work. For those protocols today, use MockProvider recipes.
Install
Section titled “Install”LiveProvider lives in the core package, but the chain-reading machinery (web3, web3scout) is an optional install. Two install paths cover the common cases:
pip install 'defipy[chain]'Adds web3.py and web3scout on top of the core install. Use this when you want LiveProvider for analytics or simulation, without the MCP server layer.
pip install 'defipy[agentic]'The canonical Python SDK for Agentic DeFi full-stack install. Composes [chain] and [mcp] in one step — LiveProvider plus the MCP server SDK for serving DeFiPy primitives as tools to Claude Desktop, Claude Code, or any MCP-compatible client. Equivalent to pip install 'defipy[chain,mcp]' but spelled with intent.
Without [chain] (or [agentic]), importing LiveProvider works but calling .snapshot() raises an import error pointing at the missing dependencies.
Quickstart — Uniswap V2
Section titled “Quickstart — Uniswap V2”Pull V2 pool state, run a primitive against it, and reach the underlying web3.Web3 instance via get_w3() for any direct chain manipulation.
from defipy.twin import LiveProvider, StateTwinBuilder
provider = LiveProvider("https://eth-mainnet.g.alchemy.com/v2/<key>")
# WETH/USDC V2 mainnet poolsnap = provider.snapshot( "uniswap_v2:0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")
lp = StateTwinBuilder().build(snap)
# Same shape as any other twin — every primitive works against itfrom defipy import CheckPoolHealthhealth = CheckPoolHealth().apply(lp)print(f"TVL in WETH: {health.tvl_in_token0:.2f}")print(f"Spot price: {health.spot_price:.4f}")
# Reach the underlying web3.Web3 directly when you need it — see# "Signing transactions: bring your own" below for the read-only-by-design framing.w3 = provider.get_w3()print(f"Chain ID: {w3.eth.chain_id}")Quickstart — Uniswap V3
Section titled “Quickstart — Uniswap V3”Same pattern for V3 pools, with the V3-specific result fields (fee_pips, tick_current) populated. The get_w3() escape hatch behaves identically — one connection per provider, shared with the snapshot path.
from defipy.twin import LiveProvider, StateTwinBuilder
provider = LiveProvider("https://eth-mainnet.g.alchemy.com/v2/<key>")
# USDC/WETH V3 500bps mainnet poolsnap = provider.snapshot( "uniswap_v3:0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
lp = StateTwinBuilder().build(snap)
# All V3 primitives work — pool health, position analysis, slippage, scenariosfrom defipy import CheckPoolHealthhealth = CheckPoolHealth().apply(lp)print(f"V3 fee: {health.fee_pips} pips")print(f"TVL ratio: {health.tvl_in_token0 / health.tvl_in_token1:.2e}")
# Same w3 escape hatch as V2 — see "Signing transactions: bring your own" below.w3 = provider.get_w3()print(f"Block: {w3.eth.block_number}")The pool_id format
Section titled “The pool_id format”The first argument to .snapshot() is a string of the form "<protocol>:<address>":
uniswap_v2:0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dcuniswap_v3:0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640Address casing is normalized internally — lowercase, uppercase, or EIP-55 checksum-mixed all work. The protocol prefix selects the read path. Valid protocols:
| Prefix | Status |
|---|---|
uniswap_v2 | v2.1 (works) |
uniswap_v3 | v2.1 (works) |
balancer | v2.2+ (raises NotImplementedError) |
stableswap | v2.2+ (raises NotImplementedError) |
Malformed pool_id (missing colon, empty protocol, empty address) raises ValueError with a message naming the offending input and listing the valid protocols.
Block pinning
Section titled “Block pinning”By default, LiveProvider resolves "latest" to a concrete block number once at the start of .snapshot(), then pins every subsequent read to that block. State drift across reads inside one snapshot is impossible — you get a coherent view of the pool at one specific block.
To read a historical block, pass block_number:
# Snapshot WETH/USDC V2 at block 19,500,000snap = provider.snapshot( "uniswap_v2:0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc", block_number=19_500_000,)The resolved block_number is also written onto the snapshot itself (see Chain context fields below), so downstream consumers know exactly which block the data came from.
V3 tick range — defaults and overrides
Section titled “V3 tick range — defaults and overrides”For V3 pools, the snapshot represents the pool’s active liquidity as if it were a single position spanning a tick range [lwr_tick, upr_tick]. By default the range is full-range — getMinTick(tick_spacing) to getMaxTick(tick_spacing) — which gives MockProvider-parity twins and works cleanly with all V3 active-liquidity primitives.
To narrow the range — for example, to simulate a tighter concentrated position around the active tick — pass lwr_tick / upr_tick kwargs:
# Tight range around active tick — useful for IL-at-concentration scenariossnap = provider.snapshot( "uniswap_v3:0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", lwr_tick=-600, upr_tick=600,)The amounts derived for the snapshot scale with the chosen range. Three regimes:
- Active tick inside range — both
reserve0andreserve1are nonzero - Active tick at or below
lwr_tick— single-sided in token0;reserve1 == 0 - Active tick at or above
upr_tick— single-sided in token1;reserve0 == 0
The single-sided regimes are valid V3 semantics, not error states. They reflect the real shape of a position whose range has been crossed.
Reserves are decimal-adjusted floats
Section titled “Reserves are decimal-adjusted floats”Both V2 and V3 snapshots have reserve0 and reserve1 as Python floats in whole-token units, not raw uint112 / uint128 wei values. LiveProvider divides by 10 ** decimals per token before constructing the snapshot.
This matches MockProvider’s contract — the same reserve0 = 1000.0 shape — and lets StateTwinBuilder and every primitive consume the snapshot uniformly regardless of source. If you need the raw values, query the chain directly with web3.py; LiveProvider’s job is to produce values that compose cleanly with the rest of DeFiPy.
Multicall batching (V3)
Section titled “Multicall batching (V3)”V3 snapshots batch all pool reads (token0, token1, slot0, liquidity, fee, tickSpacing, plus getCurrentBlockTimestamp) into a single Multicall3 aggregate3 call. That’s one RPC round trip for the V3-specific reads, plus separate sequential reads for the two tokens’ symbol and decimals (which web3scout’s FetchToken doesn’t fold into multicall).
V2 snapshots use sequential reads — four eth_calls pinned to the resolved block, plus eth_getBlockByNumber for the timestamp. The multicall optimization buys less for V2’s smaller read set.
The Multicall3 contract address (0xcA11bde05977b3631167028862bE2a173976CA11) is hardcoded; this address is the same on every major EVM chain Multicall3 has been deployed to. Chains without Multicall3 will fail with a descriptive error — that’s a v2.2 problem.
Chain context fields
Section titled “Chain context fields”Every snapshot from LiveProvider carries three fields populated from the chain:
| Field | Source |
|---|---|
block_number | The resolved concrete block — the value of block_number kwarg if passed, otherwise the result of eth_blockNumber at the top of .snapshot() |
timestamp | Block header timestamp (V2) or Multicall3’s getCurrentBlockTimestamp() pinned to the resolved block (V3) |
chain_id | eth_chainId, cached on the underlying RpcClient after first read so repeat snapshots on the same provider don’t re-fetch |
These fields are Optional[int] = None on the snapshot dataclasses. MockProvider leaves them None — synthetic snapshots have no chain context, and inventing fake values would be dishonest. Consumers that need chain context check for None and branch:
snap = provider.snapshot("uniswap_v2:0x...")if snap.block_number is not None: print(f"Live snapshot pinned at block {snap.block_number}") print(f"Timestamp: {snap.timestamp}, Chain ID: {snap.chain_id}")This is the substrate consumers like caching layers, reorg detectors, or multi-chain routers build on. LiveProvider provides the data; what the consumer does with it is their concern.
Connection lifecycle
Section titled “Connection lifecycle”Snapshots are stateless across calls — no caching of pool state, block data, or snapshot results between .snapshot() invocations. Each call returns a fresh snapshot.
The underlying RPC connection, however, is cached on the LiveProvider instance for efficiency. The first call to .snapshot() or get_w3() (whichever comes first) constructs the RpcClient lazily; subsequent calls on the same provider reuse it. Both methods share one connection per LiveProvider instance for its lifetime.
For long-running processes that may see the connection go stale, construct a fresh LiveProvider periodically or build your own connection-management layer around get_w3().
LiveProvider exposes a small surface — a constructor, the snapshot method, and an escape hatch to the underlying web3 instance.
| Method | Returns | Purpose |
|---|---|---|
LiveProvider(rpc_url: str) | LiveProvider | Construct a provider against the given RPC endpoint. No chain reads happen until .snapshot() or .get_w3() is called. |
.snapshot(pool_id, *, block_number=None, lwr_tick=None, upr_tick=None) | PoolSnapshot | Read pool state and return a typed snapshot. pool_id is "<protocol>:<address>". block_number defaults to "latest" (resolved once at the top of the call); pass an int for historical reads. lwr_tick / upr_tick apply to V3 pools only — they default to full-range. |
.get_w3() | web3.Web3 | Return the underlying web3.Web3 instance. See Signing transactions: bring your own below. |
All three respect the lazy-import contract: importing LiveProvider does not require [chain] to be installed; calling either of the two methods does. Without the install, the call surfaces a clear ImportError pointing at the missing extras.
Signing transactions: bring your own
Section titled “Signing transactions: bring your own”LiveProvider is read-only by design. The substrate exposes pool state via typed snapshots; it does not sign or send transactions. Signing infrastructure varies enormously across users — local key, hardware wallet, MPC vault, signing service, hosted custodial flow — and embedding any opinion would be wrong for most. DeFiPy stays out of the keys-and-execution layer because that’s where security and policy opinions diverge most.
For consumers who need to act on-chain after analysis, the underlying web3.Web3 instance is available via provider.get_w3():
from defipy.twin import LiveProvider
provider = LiveProvider("https://eth-mainnet.g.alchemy.com/v2/<key>")w3 = provider.get_w3()
# From here, your signing infrastructure takes over.# DeFiPy doesn't ship a signing path; you bring your own.latest_block = w3.eth.block_numberchain_id = w3.eth.chain_idThe web3.Web3 instance is shared with the snapshot path — both provider.get_w3() and provider.snapshot(...) use the same connection, lazily constructed on first use and reused for the life of the LiveProvider instance.
What’s not here, by design: no provider.sign(), no provider.send_transaction(), no transaction-builder pattern, no key management, no gas estimation helpers. The substrate exposes the underlying web3 instance and stops. Transaction tooling beyond get_w3() is the consumer’s domain or sibling-library territory, not DeFiPy’s.
Error surface
Section titled “Error surface”| Condition | Outcome |
|---|---|
Malformed pool_id (no colon, empty parts) | ValueError with message and valid-protocols list |
| Unknown protocol prefix | ValueError |
Known-but-unimplemented protocol (balancer, stableswap) | NotImplementedError pointing at v2.2 |
lwr_tick >= upr_tick for V3 | ValueError from LiveProvider (caught before chain reads) |
| RPC unreachable / pool address has no contract / multicall reverts | Underlying web3.py exception propagates |
For V3, if any single call inside the multicall reverts, the whole snapshot fails — allowFailure=False is set on every call. The intent is loud failure; a half-populated snapshot is worse than no snapshot.
What’s coming v2.2 and beyond
Section titled “What’s coming v2.2 and beyond”- Balancer LiveProvider — V3 of the same pattern for Balancer 2-asset weighted pools
- Stableswap LiveProvider — same for Curve-style stableswap pools
- V3 tick bitmap walking — pairs with
AssessLiquidityDepth, enables non-active-tick analyses - Anvil fork support — optional CI lane against a forked node for higher-confidence integration tests
See the Roadmap for the full v2.2+ timeline.
See also
Section titled “See also”- State Twin Concept — the architectural why behind the provider/builder split
- Installation —
[chain]extra and other install options - defipy.twin module reference — the full export list