Skip to main content
LpClient is the LP-facing surface of @inflexion/sdk: it prices, buys, inspects, and settles in-range IL protection on a specific Uniswap v3 position. It is the surface that carries Claim A — LPs are always paid min(realized_IL, MaxIL) from the underwriter’s collateral, with no bad debt in FULL mode — and the only one that holds a signer for LP writes. Access it as sdk.lp from the one-call factory. It is the only surface wired with engineBaseUrl / telemetryUrl / fetchImpl, because it is the only one that talks to the Path-B quote engine and emits the latent-demand telemetry ping.
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.

The read contract: graceful degradation

Every read that needs the live spot price P0 wraps the OracleManager.getPrice call. If that oracle is stale or the L2 sequencer is reported down, the manager reverts, and the SDK catches it and returns a typed NotPriceable envelope rather than throwing:
So the calling pattern is always a discriminated-union check, never a try/catch:
LP writes are the one place a missing signer is a hard error: with no walletClient, buyProtection / settle / poke throw a clear error on call (not at construction), so a read-only SDK still constructs and prices fine. See the full envelope taxonomy in SDK degradation.

Method reference

Decimals are load-bearing. dUSDC is the 6-decimal numéraire — every maxPremium, premium, V0, and maxIL is a 6-dec raw bigint (1 dUSDC = 1_000_000n). dWETH is 18-dec, Chainlink feeds are 8-dec, and ratios (aWad, bWad, σ_ref, loads) are WAD = 1e18. See decimals & units.

getLoadParams

Reads and caches the protocol’s load-stack parameters from InflexionCore.loadParams. These feed the client-side TS-port load math (regime bands, util/disp curves, maxLoadBps = invariant I10).
None.
LoadParams
The on-chain load parameters. Cached after the first read; throws only if there is no RPC client and the cache is empty.

resolveGeometry

Resolves a position NFT + a market into the full settlement geometry — the heart of pricing. It reads the NFT’s ticks, derives sqrtPaX96 / sqrtPbX96 from TickMath, reads the live sqrtP0X96 (from the oracle-derived price), converts to the WAD ratios a/b, computes entry amounts and MaxIL, and stamps the inRange gate.
bigint
required
The Uniswap v3 position NFT id.
0x${string}
required
The bytes32 market id (token0/token1/fee/duration). See markets.
Priceable<{ geometry: LpGeometry }>
On success, { priceable: true, geometry }. On failure, { priceable: false, reason } with reason ∈ 'market-unknown' | 'position-unknown' | 'oracle-degraded'.
inRange:false is informative, not an error. It tells you the on-chain create call will revert because P0 is outside [Pa, Pb]. Why: Inflexion hedges in-range convexity only — out-of-range there is no in-range IL to transfer, so the in-range invariant is enforced at creation. Mitigation: surface “out of range — not eligible” in the UI and offer the LP a position whose range still brackets spot.

listEligiblePositions

Enumerates an owner’s Uniswap v3 NFTs and prices each one across the requested durations. It reads balanceOf / tokenOfOwnerByIndex / positions from the NPM, then runs resolveGeometry per (position × duration) pair.
0x${string}
required
The wallet whose positions to enumerate.
number[]
Durations (seconds) to price each position against. Defaults to the market grid’s durations.
EligiblePosition[]
One row per (position × duration). Each row carries the resolved geometry plus eligibility flags. Returns an empty array if the NPM is unreachable. Per row, inRange is undefined when the oracle is degraded for that price (the position can’t be priced this block).

previewPremium

The load-bearing method: “what dollar premium will I be charged on THIS position if I sign in the next block?” It is the best-of {pool, MM} quote, both capped at the same MaxIL.
bigint
required
0x${string}
required
boolean
If true, sends a VolOracle.poke(token) write before reading to refresh the σ_ref EWMA (otherwise pure read).
string
Override the Path-B quote engine base for this call.
Priceable<PreviewResult>
On success the full pricing breakdown; on oracle degradation, NotPriceable.
How the premium is built:
1

Read the on-chain fair value

FairValueOracle.fairPremium(token, aWad, bWad, durationSeconds, maxIL) → (premium, fairRateWad, sigmaRefWad). The Φ-sum is never reimplemented off-chain.
2

Read pool inventory + params

ConvexityVault.inventory() → (total, locked, free, util, conc) and loadParams().
3

Finish the load stack client-side (the single permitted duplication)

totalLoad = cvamm.totalLoadWad(sigmaRef, util, conc, params), then premiumA = cvamm.premiumFromLoad(fairPremium, totalLoad) capped at MaxIL — byte-equal to _pricePathAFromFair on-chain because the deployed CvammPricing lib is delegatecall-only.
4

Fetch the best MM quote (best-effort)

GET ${engineBaseUrl}/quote?marketId=… for the cheapest live signed quote; premiumB = cvamm.pathBPremium(fairPremium, loadBps, maxIL) capped at the same MaxIL. An unreachable engine leaves premiumB undefined and routes Path A — never an error.
5

Pick the cheaper, then fire telemetry

best/path = min(premiumA, premiumB). A fire-and-forget telemetry ping (latent-demand Signal 4) is sent — never awaited, never throws.
previewPremium performs no approvals and sends no transaction (unless poke:true). Treat it as the quote you snapshot to compute maxPremium immediately before calling buyProtection.

getPayoffCurve

Computes the LP’s settlement payoff across a grid of terminal prices, with the cap applied. Drives the payoff chart in the protect flow.
bigint
required
0x${string}
required
number
Number of grid points across the sqrt-price range. Higher = smoother curve.
Priceable<PayoffCurve>
On success, the curve of (price, il, payout) where payout = min(il, MaxIL), evaluated via ILMath.computeIL over a sqrt-price grid. NotPriceable if the oracle is degraded.
The cap is applied here, so the curve flattens at MaxIL — the visual proof that no bad debt is structural: the LP never claims more than the underwriter’s locked collateral.

buyProtection

The LP write. Sends approvals (by default) and fires the chosen create call on InflexionCore.
bigint
required
The position NFT to protect.
0x${string}
required
Required for the Path-A force path; for routed/Path-B the marketId travels inside the quote.
bigint
required
The LP’s slippage guard (numéraire raw, 6-dec). The on-chain create reverts if the routed premium would exceed it.
SignedQuote
An MM quote envelope. Required when escapeHatch:'B'; optional for routed (omit → the EMPTY_QUOTE sentinel → router falls back to the pool).
0x${string}
The EIP-712 signature for quote.
'A' | 'B'
Force a specific rail. Omit for routed best-of (recommended). See the create-call matrix below.
boolean
default:"true"
When not explicitly false, sends NPM + dUSDC approvals to InflexionCore before the create call.
Hex
The create transaction hash. Throws a clear error if no wallet client is configured.

Which create call fires

createSwapRouted never reverts on a bad MM quote. A malformed or stale quote falls back to the pool on-chain. Only the executed rail mutates nonce / capacity / lock — see routing.

Approvals — the spender is InflexionCore

When buyProtection runs with approve not explicitly false, the SDK sends two approvals before the create call:
  • NPM approve(core.inflexionCore, tokenId) — the position NFT to the core.
  • dUSDC — read allowance(account, core.inflexionCore); if < maxPremium, send approve(core.inflexionCore, maxPremium) — spender is InflexionCore.

getProtectionStatus

Reads the live state of an existing protection (swap): status enum, which rail underwrote it, and the IL-to-date with the cap applied.
bigint
required
The id returned in the SwapCreated event from buyProtection.
ProtectionStatus | {available:false, reason}
available:false if the swap id is unknown.
ilToDate.payout is computed from settlePreview via simulateContract against the oracle-derived spot, so it mirrors exactly what settle would pay this block.

getClaimableFees

Estimates the Uniswap fees the LP can still collect on the underlying position, from the NFT’s checkpointed tokensOwed0/1.
bigint
required
ClaimableFees | {available:false}
{ available:false } if the swap is unknown; otherwise the checkpointed owed amounts.
This figure UNDER-states actual claimable fees. Why: tokensOwed0/1 only reflects fees checkpointed at the last NFT interaction, not fees accrued since. Mitigation: the precise figure needs the subgraph’s fee accounting, which is pending; until then treat this as a conservative lower bound and label it as such in the UI.
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

The LP (or anyone) settlement write. Recovers the swap’s expiry, finds the bracketing Chainlink round (round-walk, or your hint), then calls Core.settle to pay the LP min(realized_IL, MaxIL) from the underwriter’s collateral.
bigint
required
bigint
A Chainlink round id that brackets expiry. If omitted, the SDK walks Chainlink rounds backwards to find it (findHintRoundId).
Hex
The settlement transaction hash. Throws a clear error if no wallet client is configured.
settle always succeeds within the liveness window (invariant I8): expiry + LIVENESS_WINDOW + MAX_STALENESS + GRACE_PERIOD. The round-walk + grace period is the oracle-deadlock fork resolution — a late or briefly-stale feed cannot strand a settlement.

autoProtect

Orchestrates the whole flow — list → preview → buy — across an owner’s eligible positions, with ACTIVE-protection dedupe. Never throws: every row is a structured success / skipped / error result.
0x${string}
required
The wallet whose positions to auto-protect.
bigint
Per-position premium ceiling (numéraire raw). Rows whose best premium exceeds it are skipped.
number[]
Durations to consider per position.
boolean
If true, prices and plans but sends no transaction — pure read.
AutoProtectResult[]
One row per candidate. Each row is a success (with txHash), a skipped row (already ACTIVE, out of range, or over maxPremium), or an error row. The method scans swaps / nextSwapId for existing ACTIVE protection to avoid double-buying.
W (unless dryRun)
Writes one create tx per actionable row. Requires a wallet unless dryRun:true.

poke

Refreshes the σ_ref EWMA for a token before pricing, by calling VolOracle.poke(token).
0x${string}
required
The volatile token whose σ_ref to refresh.
Hex
The poke transaction hash. Throws if no wallet client is configured.
Use this (or previewPremium({ poke:true })) when σ_ref may be stale and you want the freshest fair value before signing.

findHintRoundId

A read helper that walks Chainlink rounds backwards from latest to find the round that brackets a target timestamp — the hintRoundId settle needs.
0x${string}
required
The token whose Chainlink feed to walk.
bigint
required
The target timestamp (the swap’s expiry).
number
Cap on how many rounds to walk backwards.
bigint
The bracketing round id. On an un-fetchable phase boundary it returns the best available round rather than throwing.

Worked example — preview, buy (routed), settle

To route a specific MM quote (Path B), pass { quote, signature, escapeHatch: 'B' } where quote is the SignedQuote and signature the EIP-712 sig.

Approvals & decimals at a glance

Framing that must stay exact

  • In-range convexity hedge, not “IL insurance”. Entry requires Pa ≤ P0 ≤ Pb; the inRange gate rejects out-of-range at creation. See the hedge.
  • Payout = min(realized_IL, MaxIL); the cap is load-bearing for no bad debt. getPayoffCurve / ilToDate apply it.
  • The Φ-sum is read on-chain, never reimplemented — only the deterministic load stack is mirrored off-chain (parity-locked), because the deployed lib is delegatecall-only.

LpClient guide

The narrative walkthrough of the LP surface, with the routing flow explained.

Degradation envelopes

Priceable / Degraded / WriteResult — the full envelope taxonomy.

Lifecycle

Create → active → settle, end to end.

Routing

How createSwapRouted picks pool vs MM and never reverts on a bad quote.