Transaction states
One state machine drives every write button:
signing → pending → success / error.Three approval spenders
The recurring gotcha — premium, deposit, and collateral approve three different contracts.
Pending is not error
A missing data source is a typed, self-describing state — never a crash, never a fake number.
Pool pricing is a TS port
CvammPricing is delegatecall-only on-chain, so the SDK mirrors it with a parity-locked port.Transaction states
Every write in the app — buy protection, settle, deposit, request/withdraw, post or withdraw MM collateral, cancel nonces — goes through one hook:useTx. It drives the button label, waits for the receipt, and normalizes the two different shapes a write can return.
The state machine
TxStatus is a five-state enum:
lib/use-tx.ts
1
idle
The resting state. The button is live and clickable.
reset() returns here after a success or error.2
signing
Set the instant you click, before the wallet popup resolves. You are being asked to sign in your wallet. The button is busy.
3
pending
Set once a tx hash exists and the app is waiting on the receipt —
publicClient.waitForTransactionReceipt({ hash }). The tx is in the mempool / being mined.4
success
The receipt landed. The page’s queries invalidate so the new state appears — e.g. on
/protect, the eligible-positions, owner-swaps, and preview queries refetch so your new protection shows up.5
error
Anything threw. The message is trimmed to its first line and stored — see readable errors below.
isBusy = status === 'signing' || status === 'pending', which is what disables a button while a write is in flight so you cannot double-submit.
Two return shapes, one normalizer
A write fn returns either atxHash (a Hex string, from LpClient / MmClient) or a WriteResult-like object (from DepositorClient). useTx collapses both:
- a raw
Hexstring is taken as the hash directly; - a
WriteResultis unpacked by itsstatusfield — and crucially, two statuses are converted into thrown errors so the UI never silently no-ops:
lib/use-tx.ts
deferred-no-wallet branch is how the depositor flow’s graceful write degradation surfaces to a human: the DepositorClient returns WriteResult { status: 'deferred-no-wallet' } (carrying the unsigned tx for an external signer) instead of crashing at construction, and useTx turns it into a clear “Connect a wallet to sign.” on click — see the SDK degradation contract. LP and MM writes throw their own clear error on call for the same reason.
Readable errors
viem’s revert messages are notoriously verbose — multi-line dumps with ABI snippets and request bodies.useTx keeps only the first line:
lib/use-tx.ts
JuniorBelowLocked, or an MM quote is rejected for a bad signature, or a routed premium would exceed your maxPremium slippage guard, you see a single readable line — not a wall of stack trace.
Because
isBusy and the trimmed error come from the same hook everywhere, the feel of a transaction is identical on every page. Learn it once on Protect and you already know it on Earn and Underwrite.The three approval spenders
This is the single most common integration mistake, so it gets its own section. dUSDC is the numéraire for everything — premiums, deposits, and MM collateral — but the three flows approve three different contracts. Approve the wrong one and the subsequent call simply reverts on insufficient allowance.
The exact addresses live in the registry, never inline — see the deployment addresses:
Network: Arbitrum Sepolia · chainId
421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05
Core protocol
Stylus & libraries
Oracles
Uniswap
Chainlink feeds
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.
Why they differ — and the design behind it
This is not an oversight; it reflects three genuinely different custody relationships.Depositor deposit → ConvexityVault (not Core)
Depositor deposit → ConvexityVault (not Core)
A depositor’s capital goes straight into the pooled cvAMM vault (
ConvexityVault), the Path-A underwriter — it never touches Core. With autoApprove: true the SDK checks allowance and, if short, sends dUSDC approve(core.convexityVault, amount) before deposit(tranche, amount). The /earn hint text says this explicitly, and the success toast restates the risk: “Deposit confirmed. Capital is NOT guaranteed.” This is the gotcha most people hit — approving Core for a deposit will not work.MM collateral → UnderwriterVault (a separate flow)
MM collateral → UnderwriterVault (a separate flow)
An MM posts collateral to the per-MM
UnderwriterVault before its signed quotes can be filled. This is deliberately not part of signQuote and there is no MmClient helper for it — the SDK exposes the ABI + address and the /underwrite page calls the documented functions directly via the wallet client rather than inventing a method. Deposit is two txs: ERC-20 approve(underwriterVault, amount) (only if the allowance is short), then UnderwriterVault.deposit(amount). On a Path-B fill, the MM’s own collateral is locked at MaxIL and pays the LP at settlement.Pending is not error
The app’s resilience model is best understood through how it handles a missing data source. Rich on-chain events (SwapPriced, QuoteFilled, the lifecycle events) have been emitted since deploy block 274081134, and the subgraph now indexes them — so historical, aggregate, and time-series surfaces are served live from the subgraph via the hosted API. But the same surfaces degrade gracefully if a backing (the subgraph URL, an RPC, or a telemetry sink) is ever absent.
What the app does with that is the point. It does not throw, freeze, or fabricate a number. Every such surface degrades into a typed, honest, self-describing state.
1
Reads return a typed envelope, never a throw
Any read whose backing is absent — a reverting oracle, an absent rich event, or (if
SUBGRAPH_URL is unset) the subgraph — returns a typed { available: false } / { priceable: false } envelope and renders a calm pending / degraded state — never an error. See the degradation cheat-sheet.2
The pending note names its route
When a surface does degrade, a
PendingNote prints the reason and, where applicable, the exact API route + query the surface is served from — straight off the typed ApiPending envelope. It is a promise with an address, not a placeholder.3
Live-but-degraded oracle states are honest too
If the live oracle reverts (stale feed / sequencer down), a position renders “not priceable now” — the protection still exists and becomes quotable when the oracle recovers — and Buy is disabled rather than producing a wrong number. The IL-to-date on an active protection degrades the same way: “The protection is intact — payout is still computed at settlement from the price-at-expiry”.
What each surface is backed by
The honest split, identical wherever a surface appears:- Live from RPC
- Live from the subgraph (via the API)
- Premium preview and the payoff-with-cap chart
- Buy and settle on Path A (the cvAMM pool)
- Vault deposit / withdraw (depositor)
- Market pricing and the current clearing-load surface
- σ_ref (the reference vol) and the regime badge
- The book (on-chain swap scan) and coarse fills
Why it is built this way. An app that shows fabricated history looks polished but is dishonest. An app that crashes on a missing backend looks broken. Inflexion does neither: a missing data source becomes a typed, self-describing pending state that says exactly what is missing and where it will come from. Treating “pending” as a first-class, honest state — not a bug and not a fake number — is itself the trust signal the product is built around.
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
pendingstate — never an error.
CvammPricing is delegatecall-only
When you see a pool premium or a clearing load anywhere in the app — the premium preview on Protect, the “pool load to beat” on Underwrite, the heat-matrix on Data — that number is computed client-side, by a TypeScript port, not by calling the deployed pricing library. This is intentional and worth understanding, because it is the one place the architecture permits the same logic to live in two languages.The split: Φ-sum on-chain, load stack ported
The pricing stack has two layers, and they are treated very differently:- FairValue (the Φ-sum) — the exact closed-form fair premium. It is read on-chain from the Stylus
FairValueOracle(fairPremium/fairRate) and is never reimplemented off-chain. - The load stack — the deterministic,
puremarkup over fair value (base load by σ_ref regime + utilization skew + dispersion skew, clamped tomaxLoadBps). This is mirrored off-chain, by the SDK’scvammmath port, because the deployedCvammPricinglibrary cannot be read directly.
libs.cvammPricing (0x4a053d29…) exposes public functions intended for delegatecall from Core during pricing. A direct eth_call to it reverts — Solidity guards deployed libraries against being called as standalone contracts; this was confirmed on the 2026-06-05 deploy. So there is no way to ask the deployed library “what’s the load?” from the client.
The answer is a parity-locked TypeScript port: the cvamm namespace in the SDK is a permanent, byte-equal mirror of the deployed Solidity load math, held in lockstep by math.parity.test.ts. It is the only duplication the architecture allows — the load math, never the FairValue Φ-sum.
Computing the pool premium client-side
previewPremium, getMarketPricing, and getCurrentLoadSurface — you never call cvamm.* yourself unless you are building a custom surface. See the cvAMM port reference.
The framing that travels with every page
Two claims appear on every risk surface in the app, and they are never merged. If you are an LP, Claim A is yours; if you are a depositor or market maker, Claim B is yours. The/earn disclosure puts it bluntly: the no-bad-debt guarantee “is for the protected LP, not for you”. See no bad debt and the tranches for the full treatment.
Where to go next
Before you start
Wallet, network, demo tokens, and an eligible in-range position.
Protect an LP position
Buy in-range IL protection — the full transaction state machine in action.
Earn in the cvAMM
Deposit into the pooled vault — and meet the ConvexityVault spender.
Underwrite as a market maker
Post collateral to the UnderwriterVault and sign firm per-market quotes.
The SDK degradation contract
The typed envelopes behind every “pending” state, in full.
Status & caveats
The complete honest ledger of what’s live, pending, and why.