Skip to main content
LpClient is the surface an LP uses to buy in-range impermanent-loss protection: enumerate eligible Uniswap v3 positions, price one against a market, read its payoff-with-cap curve, sign the create transaction, watch the protection mature, and settle it at expiry. It is the only surface that carries claim A — the LP is always paid in FULL mode, qualified — exported verbatim as LP_CLAIM_A and stamped onto ProtectionStatus.noBadDebtFull. It hangs off the factory as sdk.lp:
Two design properties run through every method on this client — repeat them.
  1. Reads are graceful. Every read that needs the live price P0P_0 catches an OracleManager.getPrice revert and returns a typed NotPriceable { priceable:false, reason:'oracle-degraded' } envelope — it never throws. See degradation.
  2. Writes degrade, never crash at construction. With no walletClient the client constructs fine; the write call (buyProtection, settle, poke, autoProtect) throws a single clear error — the one place a missing signer is a hard error.
The framing this surface must keep exact. This is an in-range convexity hedge, not “IL insurance.” Entry requires Pa≤P0≤PbP_a \le P_0 \le P_b; out-of-range positions are rejected at creation by the inRange gate. The payout is min⁡(realized_IL, MaxIL)\min(\text{realized\_IL},\ \text{MaxIL}) and the cap is load-bearing for the no-bad-debt guarantee. Never state no-bad-debt unqualified — use the disclosure at the foot of this page.

Method map


previewPremium — the load-bearing on-chain read [DEEP]

previewPremium(tokenId, marketId, opts?) answers the only question that matters before signing: what dollar premium will I be charged on this position if I sign in the next block? It is a fresh on-chain FairValue read plus the position’s geometry — never an off-chain price guess.

What it returns

PreviewResult:
Hex
The market the position was priced against.
bigint
The cap and the unit of risk — numéraire raw, 6-dec. Pure geometry, frozen at the moment of pricing.
bigint
The on-chain Φ\Phi-sum fair value, before any load. Read, never reimplemented.
bigint
The dimensionless fair rate (WAD).
bigint
The σref\sigma_{\text{ref}} EWMA the regime band keys off (WAD).
bigint
The Path-A pool premium — fairPremium plus the full load stack, capped at MaxIL.
bigint?
The best live MM quote premium. Present only when an MM quote beat the pool.
bigint
The cheaper of premiumA / premiumB — what you would actually pay.
'A' | 'B'
Which rail produced best.

The five-step mechanic

This is the pricing stack executed client-side around the two on-chain reads. The Φ-sum is read on-chain; only the deterministic load is finished off-chain via the parity-locked cvamm TS port — the single permitted duplication, because the deployed CvammPricing library is delegatecall-only and reverts a direct eth_call.
1

Read FairValue on-chain (Path A floor)

FairValueOracle.fairPremium(token, aWad, bWad, durationSeconds, maxIL) returns (premium, fairRateWad, sigmaRefWad). This is the closed-form Φ-sum — exact, on-chain, never reimplemented off-chain.
2

Read pool inventory + load params

ConvexityVault.inventory() → (total, locked, free, util, conc) and Core.loadParams(). util and conc are the pool-state inputs to the load skew.
3

Finish the load stack client-side (parity-locked)

premiumA=cvamm.premiumFromLoad(fairPremium, totalLoad)=⌈fairPremium⋅(1+totalLoad)⌉\text{premiumA} = \text{cvamm.premiumFromLoad}(\text{fairPremium},\ \text{totalLoad}) = \Big\lceil \text{fairPremium}\cdot(1 + \text{totalLoad}) \Big\rceil where totalLoad = cvamm.totalLoadWad(sigmaRef, util, conc, params), then capped at MaxIL. This is exactly _pricePathAFromFair on-chain. See the load stack.
4

Fetch the best MM quote (Path B, best-effort)

GET ${engineBaseUrl}/quote?marketId=… for the cheapest live signed MM quote, then premiumB=cvamm.pathBPremium(fairPremium, loadBps, maxIL)=⌈fairPremium⋅(1+loadBps10000)⌉\text{premiumB} = \text{cvamm.pathBPremium}(\text{fairPremium},\ \text{loadBps},\ \text{maxIL}) = \Big\lceil \text{fairPremium}\cdot\big(1 + \tfrac{\text{loadBps}}{10000}\big) \Big\rceil capped at the same MaxIL. An absent or unreachable engine simply leaves premiumB undefined and routes Path A — never an error.
5

Pick the cheaper rail

best / path = the cheaper of premiumA / premiumB. A fire-and-forget telemetry ping (latent-demand Signal 4) is sent — never awaited into the result, never throwing.
Why both premiums share one MaxIL. MaxIL is the unit of risk: pure geometry, frozen at creation, identical across durations. Because the cap is geometry, not a quote parameter, an MM quote is per-market (a loadBps + a MaxIL-ratio band + capacity), never per-NFT. Pool and MM are pricing the same unit, so their premiums are directly comparable — that comparability is what makes routing honest.
Limit: when the oracle is degraded (stale round or the L2 sequencer is down), previewPremium returns NotPriceable, not a number.Why: there is no honest premium without a live P0P_0 — OracleManager.getPrice reverts by design rather than serve a stale price, and the SDK surfaces that as a typed envelope instead of inventing a value.Mitigation: render preview.reason (“oracle is catching up”) and retry; optionally pass { poke: true } to nudge VolOracle.poke(token) first (a write) so the next read has a fresh EWMA. The price oracle itself recovers on the next valid Chainlink round.

resolveGeometry & listEligiblePositions

Before pricing, the SDK must turn a Uniswap NFT tokenId into the exact geometry the math consumes.

resolveGeometry

resolveGeometry(tokenId, marketId) reads NPM.positions(tokenId), the market config and oracleDerivedSqrtPriceX96 from Core, walks TickMath for the range bounds, reads P0P_0 from OracleManager, and computes the entry amounts and MaxIL via ILMath.computeMaxIL / getAmountsForLiquidity. It returns Priceable<{ geometry: LpGeometry }>. LpGeometry:
The inRange flag is exact geometry — sqrtP0 >= sqrtPa && sqrtP0 <= sqrtPb, the same gate createSwap enforces on-chain. The aWad/bWad conversion is cvamm.abFromSqrt(sqrtP0, sqrtPa, sqrtPb) where a=(sqrtPa/sqrtP0)2a = (\text{sqrtPa}/\text{sqrtP0})^2, b=(sqrtPb/sqrtP0)2b = (\text{sqrtPb}/\text{sqrtP0})^2. These dimensionless ratios are what the Φ-sum and the MaxIL derivation consume — geometry, not dollar amounts. Degradation routes: market-unknown (no such market on Core), position-unknown (no such NFT), oracle-degraded (no live P0P_0).

listEligiblePositions

listEligiblePositions(owner, opts?{durations?}) enumerates a wallet’s positions and prices each across one or more durations:
It calls NPM.balanceOf / tokenOfOwnerByIndex / positions, then resolveGeometry per (position × duration). Degradation is graceful: an empty array if there is no NPM, and a per-row inRange:undefined if the oracle is degraded for that row — never a thrown call.
Limit: an out-of-range position never appears as protectable, and its badge will not turn green.Why: Inflexion hedges in-range convexity — the bounded, deterministic IL geometry between PaP_a and PbP_b. Outside the range a v3 position is a single-sided spot bet, and its MaxIL geometry no longer defines a bounded cap, so the no-bad-debt construction does not hold.Mitigation: the row is returned with inRange:false (not dropped silently) so the UI can explain the gate. Re-center the position back in range to make it eligible.

getPayoffCurve — payoff with the cap drawn in

getPayoffCurve(tokenId, marketId, opts?{points?}) returns the LP’s settlement payoff across a grid of terminal prices, with the MaxIL cap applied — this is the chart that makes the convexity hedge legible.
It evaluates ILMath.computeIL over a sqrt-price grid spanning the range and applies the cap at each node. The payoff is exactly: payout(PT)=min⁡(realized_IL(PT), MaxIL),realized_IL={Vhold−VlpVhold>Vlp0otherwise\text{payout}(P_T) = \min\big(\text{realized\_IL}(P_T),\ \text{MaxIL}\big), \qquad \text{realized\_IL} = \begin{cases} V_{\text{hold}} - V_{\text{lp}} & V_{\text{hold}} > V_{\text{lp}} \\ 0 & \text{otherwise} \end{cases} The non-negativity branch is invariant I3 — never an unchecked subtraction. Where capHit is true, the curve is flat at MaxIL: that flat ceiling is the no-bad-debt construction made visible — the underwriter posted exactly MaxIL, and the payout cannot exceed it. See the IL formula and the worked example.

buyProtection — approvals + escape hatches [DEEP]

buyProtection(params) is the one write that mints a protection. It sends approvals (by default), selects the create call, and returns the transaction hash.

Parameters

bigint
required
The Uniswap v3 position NFT to protect.
Hex
required
The market to protect it in.
bigint
required
The LP’s slippage guard — numéraire raw, 6-dec. The on-chain create reverts if the routed premium would exceed this.
'A' | 'B' | undefined
Force a rail. Omit (default) for best-of-both routing.
SignedQuote
Required when escapeHatch:'B'. The per-market signed MM quote.
Hex
Required when escapeHatch:'B'. The EIP-712 signature over quote.
boolean
default:"true"
When not explicitly false, send the NPM + dUSDC approvals before the create call.

Approvals — the spender is InflexionCore

When approve is not explicitly false, the SDK sends two approvals first:
  1. NPM approve(core.inflexionCore, tokenId) — hands the position NFT to the core.
  2. dUSDC — reads allowance(account, core.inflexionCore); if it is < maxPremium, sends approve(core.inflexionCore, maxPremium).
The dUSDC spender is InflexionCore — not the ConvexityVault (that is the depositor flow). See the approvals & spenders table.
Approve-and-buy in one call, or split it. Leave approve at its default to do both approvals plus the create in one buyProtection. If your UI pre-approves elsewhere (or you want to inspect allowances first), pass approve:false and the SDK fires only the create call — assuming sufficient allowance already exists.

Which create call fires

escapeHatch: undefined → createSwapRouted(quoteTuple, signature, tokenId, maxPremium). The on-chain router gives the LP the cheaper of pool vs MM and never reverts on a bad MM quote — it falls back to the pool. A missing quote is passed as the EMPTY_QUOTE sentinel (mm == address(0), empty signature) so the router takes Path A. Only the executed rail mutates nonce / capacity / lock. This is the recommended path. See routing.
Limit: buyProtection throws a clear error if the client was built without a walletClient.Why: a buy is a state-changing transaction; there is no honest way to “degrade” a missing signer for a write the way reads degrade.Mitigation: this is the one place a missing signer is intentionally a hard error — construct the SDK with a wallet (browser: wagmi useWalletClient; server: a privateKey) before calling. Reads work without one.

getProtectionStatus & getClaimableFees

getProtectionStatus

getProtectionStatus(swapId) reads the live state of a protection. It returns ProtectionStatus or { available:false, reason } for an unknown swap.
It reads Core.swaps plus settlePreview (via simulateContract), OracleManager.getPrice, and Core.oracleDerivedSqrtPriceX96. isPathA is mm == convexityVault. Critically, ilToDate is itself a Priceable — if the oracle is degraded the status still resolves, but the live IL inside it degrades independently.

getClaimableFees

getClaimableFees(swapId) returns the position’s checkpointed tokensOwed0/1 from NPM.positions.
Limit: getClaimableFees under-states fees.Why: it reads only the on-chain checkpointed tokensOwed, which lags real-time accrual between checkpoints.Mitigation: the precise, real-time figure comes from the subgraph once it is indexing. Until then, treat this value as a conservative lower bound — never an error.
Structures ship from day one; the full DYNAMICS (pool-vs-MM spread as a forward-vol signal) mature with multiple competing MMs and volume. The dataset begins at the single redeploy; the off-chain telemetry halves begin at the first interaction logged.What Inflexion offers today is the architecture and the first public view into the microstructure of the DeFi LP volatility-risk premium — not yet a mature dataset:
  • Structures are present day-one. All five non-circular behavioral signals are defined and queryable from launch.
  • Dynamics mature with volume. The richer signal — the pool-vs-MM spread read as a forward-vol indicator — only sharpens once multiple market makers compete and real flow accumulates.
  • The dataset begins at the single redeploy, and the off-chain telemetry halves begin at the first interaction logged. History is served live from the subgraph via the API; if a backing (subgraph URL / RPC / telemetry) is absent, surfaces degrade to a typed pending state — never an error.

settle & findHintRoundId

At expiry the protocol pays the LP min⁡(realized_IL, MaxIL)\min(\text{realized\_IL},\ \text{MaxIL}) from the underwriter’s collateral. settle does that on-chain, and findHintRoundId provides the Chainlink round it settles against.

settle

settle(swapId, opts?{hintRoundId?}) recovers the swap’s expiry from Core.swaps, finds the bracketing Chainlink round (via findHintRoundId if no hint is given), and calls Core.settle(swapId, hintRoundId).

findHintRoundId

findHintRoundId(oracleToken, expiry, opts?{maxWalk?}) walks Chainlink latestRoundData / getRoundData backwards to find the round whose timestamp brackets expiry. It returns a bigint round id — and on an un-fetchable phase boundary it returns the best round it found rather than throwing.
Why a hint round id at all — Fork 1, settlement liveness. Settlement must always succeed within expiry + LIVENESS_WINDOW + MAX_STALENESS + GRACE_PERIOD (invariant I8). The contract verifies the caller-supplied round actually brackets expiry, so the off-chain walk is just a gas-saving search for the right round id; a wrong hint is rejected on-chain, never trusted. Anyone can call settle — it is permissionless and the payout is deterministic. See oracles.
Limit: settle throws a clear error if the client has no walletClient.Why: settlement is a transaction. Like buyProtection, a missing signer for a write is a hard error, not a degraded read.Mitigation: build the SDK with a signer. Note that settle is permissionless — anyone (a keeper, the MM, or the LP) can settle once expiry passes, so the LP does not have to be the caller.

autoProtect — orchestrated batch protection

autoProtect(options) orchestrates the whole flow — list → preview → buy — across a wallet’s eligible positions, with ACTIVE-swap dedupe so it never double-protects.
It scans Core.swaps / nextSwapId for already-ACTIVE protections to skip, then prices and (unless dryRun) buys each eligible row. It is resilient by construction: every row returns a skipped or error result rather than aborting the batch — autoProtect never throws.
Always dry-run first. dryRun:true returns the full set of would-be buys with their premiums and skip reasons and sends no transaction — the right way to preview a batch in a UI before asking the user to sign.

End-to-end worked example

A complete LP lifecycle against the live deployment — price, guard, buy (routed best-of), watch, settle. This is server/key-based; for the browser pattern see getting started.
lp-lifecycle.ts

Live deployment addresses

Every address in this example resolves from the deployment registry — never hardcode one.
Network: Arbitrum Sepolia · chainId 421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05

Core protocol

Stylus & libraries

Oracles

Uniswap

Demo tokens / pool / LP

Lifecycle swaps

Live create→settle lifecycle (2026-06-05, short 300s fee-500 market 0xb8bbd684f213d5833886ade7b531a6949d85522249881a2b5d46a5cc76e439c2):
  • Path A (cvAMM convexity) — swapId 1, LP tokenId 3220 · V0 $270,531.28 · MaxIL $1,669.24 · premium $9.70 (0.58% of MaxIL) · settled payout $148.64 from ConvexityVault.
  • Path B (MM routing) — swapId 2, LP tokenId 3221 · MM beat cvAMM ($8.93 vs $13.80) · MaxIL $3,215.65 · MM collateral locked $3,215.65 · settled payout $245.66 from MM collateral.

Decimals & units recap

  • dUSDC = 6 decimals — every maxPremium, fairPremium, maxIL, premiumA/B, best, and payout is a 6-dec raw bigint (1 dUSDC = 1_000_000n). dWETH = 18, Chainlink feeds = 8.
  • WAD = 1e18 for ratios — aWad, bWad, fairRateWad, sigmaRefWad, and the load components.
  • sqrtPriceX96 is the Uniswap Q64.96 sqrt price; sqrtToPriceWad(sqrtPX96) = (\text{sqrtP}/2^{96})^2 \cdot 10^{18}.
Full detail: addresses & units and decimals & units.

The claim this surface carries

LPs are always paid with no bad debt in FULL mode — qualified: capped payoff + solvent USDC + oracle/settlement liveness.The no-bad-debt guarantee is exact only under the full clause:
  • FULL collateralization — the underwriter has posted exactly MaxIL per swap, locked for the life of the position.
  • Capped payoff — the payout is min(realized_IL, MaxIL). The cap is load-bearing: it is what makes payout ≤ collateral hold by construction.
  • Solvent USDC — the collateral numeraire (dUSDC) holds its peg and remains redeemable.
  • Oracle / settlement liveness — the price oracle stays live and settle() runs within the protocol’s settlement window.
  • No rehypothecation breach — locked collateral stays instantly liquid; it is never routed to utilization-gated venues.
Stated without these qualifiers, the guarantee is not correct.
LP no-bad-debt (claim A) and depositor / MM capital risk (claim B) are separate claims and are never merged. This page is claim A only.

See also

Getting started

Wire the SDK in browser and server modes.

Routing

How createSwapRouted gives the LP the cheaper rail and never reverts on a bad MM quote.

MaxIL — cap & unit of risk

Why both premiums share one cap and why quotes are per-market.

Quote signing

Build a SignedQuote for the Path-B escape hatch.

The Φ-sum fair value

The on-chain closed form previewPremium reads.

Degradation

The typed envelopes every read returns instead of throwing.