Smile

TL;DR: Standard options market potentially as popular as Robinhood, decentralized as Polymarket.

A non-custodial, parametric options marketplace that solves three interlocking problems in DeFi options: thin liquidity at each strike, yield-killing collateral lock-up, and the absence of emergent market makers. By combining 1inch Aqua, Uniswap v4 Hooks, and Chainlink CRE, LPs can quote an entire strike range from one capital pool — while their collateral keeps earning DeFi yield until a buyer actually matches.


Table of Contents

  1. The Thesis
  2. Architecture
  3. Mathematical Specification
  4. Flow Diagrams
  5. Deployed Addresses (Sepolia)
  6. How to Run the Project
  7. End-to-End Demo Walkthrough
  8. Glossary
  9. Project Structure
  10. EthOnline 2026 — Continuation Track
  11. Technical Stack

Live: smile-frontend-omega.vercel.app — the full app with the AI copilot (server build, Sepolia + Arc) · oslinin.github.io/Smile — static build (no copilot) · help site

More docs: the build notes & war stories · the verified CRE simulation transcript · known limitations · solutions & phased roadmap · reference table — every L/R/S/P, one-liner + status · integration pages — 1inch Aqua, Chainlink, Uniswap, The Graph, Circle · Arc, Frontend

This README is also published as a wiki-style help page — with a sidebar linking Overview (this doc), Limitations, Solutions, and the Reference Table — at /help.html in the deployed frontend, generated from these same files by frontend/scripts/gen-help.mjs.


🚀 The Thesis

While prediction markets — binary options on event outcomes — have been widely successful in DeFi (Polymarket, Augur), standard options have not. Prediction markets do not offer many strategies retail traders have been increasingly investing in: selling covered calls to generate yield on held ETH, selling cash-secured puts to acquire ETH at a discount, buying butterflies to express a range-bound view on volatility, etc. The building blocks for this popular market requires a functioning options market with real liquidity across strikes and expiries for standard options (buys and sells of puts and calls). That market has never materialized on-chain: Ribbon and Friktion pioneered DeFi Options Vaults (DOVs) but suffer from trapped liquidity: collateral is locked per strike chosen by the vault manager, leaving the rest of the chain empty. Premia introduced RFQ-based pricing that relies on institutional market makers for quotes, creating a dependency on off-chain liquidity.

Smile attempts to overcome these limitations to on-chain standard opions trading by using Aqua's non-custodial LP to remediate:

  1. Liquidity fragmentation across strikes and expiries, until a buyer is matched. Makers can offer liquidity across a range of strikes and expiries, increasing net liquidity.
  2. Collateral lockup in LPs, and forfeited dividend yield — which is not a limitation of standard options writers — is also removed by Aqua's non-custodial LP.
  3. Standard options markets work because broker-dealers delta-hedge their books against the spot market. Smile attempts to use the trading and settlement functionality provided by Uniswap and Chainlink to allow clever LPs and arbitrageurs to continuously arbitraging away mispricings between options and the underlying. Specifically:
    • Fast trading and premium transfer via Uniswap Trading API
    • Vol surface repricing post-trade via Uniswap v4 Hooks across strikes and expiries
    • Options payoff settlement and redemption via Chainlink CRE

Competitive positioning: why not Panoptic?

Panoptic is the strongest live on-chain options design and deserves a direct answer. It synthesizes perpetual options out of Uniswap v3 LP positions and prices them off the pool's realized fee flow ("streamia") — which elegantly deletes the pricing-oracle problem altogether. But that design does not target — and structurally cannot target — the user Smile is built for: the OptionStrat/thetagang-style premium seller and the Deribit-style vanilla trader.

  • No expiries. Panoptic options are perpetual — there is no "March 3,500 call," no expire-worthless endgame where the seller keeps the credit, and no calendar or diagonal spreads (there is no term structure to spread across). Smile's fixed European expiries and per-tenor vol buckets quote all of these natively.
  • No upfront credit. A Panoptic "put credit spread" or "iron condor" draws the same payoff diagram but streams income only while spot sits near the short strikes — the premium is path-dependent and unknowable at entry. A Smile seller collects a known premium at fill, priced off implied vol, which charges for jump risk upfront; realized fee flow does not.
  • Liquidations exist. Panoptic runs partial collateral with margin and forced liquidations (plus forced exercise of far-OTM longs). Smile is fully collateralized by construction — an option, once written, can always pay.

Smile makes the opposite bet: keep the instrument traders already know — fixed-expiry, cash-settled European vanillas with known premiums — and rebuild the market-making stack around it to compete with Deribit and tradfi desks on their own terms:

  • 1inch Aqua removes the market maker's largest on-chain cost. Collateral stays in the LP's wallet, earning yield, until the moment of sale — so quoting an entire strike chain is nearly free, a yield double-dip no tradfi desk or locked vault gets.
  • SwapVM pricing + LP-quoted vol lets each LP express their own σ per range — quoting in vol, exactly how professional desks quote — and best-quote routing turns overlapping ranges into an order book in vol space, so the touch is the discovered market vol.
  • Uniswap v4 hooks reprice the surface with flow, and tradfi-grade spread mechanics — staleness-scaled spreads, size-convex price impact, per-block notional caps, a spread floor calibrated to the oracle's blind window — price adverse selection the way a desk does instead of pretending it away (see Limitations and Solutions).
  • Chainlink settlement is permissionless and round-verified, and LPs who want the full tradfi profile can delta-hedge from the same wallet that backs their quotes — the collateral never left it.

In short: Panoptic reinvents the option to fit Uniswap; Smile keeps the option traders already trade and uses Aqua, SwapVM, Uniswap hooks, and Chainlink to rebuild how it's made.


🏗️ Architecture

Layer Component Functionality
Pricing SmileSwapVMRouter + OptionPricingEngine Custom instruction (opcode 33) on the official 1inch SwapVM pricing off a multiparameter vol surface: σ per tenor bucket + skew, σstrike=σtenor(1+αln(K/S)2+βln(K/S))\sigma_{strike} = \sigma_{tenor} \cdot (1 + \alpha \cdot \ln(K/S)^2 + \beta \cdot \ln(K/S)), time-value =SσstrikeT= S \cdot \sigma_{strike} \cdot \sqrt{T}. The instruction is two-sided: forward direction prices the Ask, reverse the Bid. Oracle reads enforce Chainlink freshness.
Liquidity official 1inch Aqua + AquaCollateralVault LP calls authorizeRange(K_{min}, K_{max}, \text{DTE}, \text{maxCollateral}), then ships the strategy with the official Aqua.ship(). On buy(), the SwapVM swap Aqua.push()es the premium into the LP wallet and Aqua.pull()s collateral JIT into escrow. OptionToken deployed lazily per strike.
Market OptionPricingHook + Uniswap Trading API The vol surface lives in a Uniswap v4 hook contract: every vault reads sigmaFor() to price and calls bumpSigma() after each fill (live on every chain). Its v4 entrance — beforeSwap vetoes mispriced secondary-market trades, afterSwap shifts the whole surface — awaits a live OptionToken pool (L14). Trading API used for (1) live ETH/USD spot price and (2) — when NEXT_PUBLIC_UNISWAP_API_KEY is set — quoting the buyer's ETH→USDC premium swap (Universal Router, mainnet route) ahead of the vault call; without a key, or on Sepolia/Arc, the buyer pays premium from USDC directly.
Settlement AquaOptionSettlement + Chainlink CRE Every minted series is registered at buy time. At expiry, settlement is permissionless: anyone supplies the Chainlink roundId covering expiry and the contract verifies on-chain that it is the first post-expiry round (settleWithChainlinkRound) — no trusted writer. The scheduled CRE DON path (settleSeries) remains as a keeper. Holders redeem() the cash-settled intrinsic from the vault; LPs reclaimCollateral() for the exact remainder.
Asset OptionToken ERC-20 option position. Vault is owner, so can burn without allowance. Tradeable on any DEX for secondary-market price discovery.

Official 1inch Aqua + SwapVM integration

The liquidity layer runs on the official contracts1inch/aqua and 1inch/swap-vm (release/1.2), vendored under lib/ and compiled unmodified:

  • SmileSwapVMRouter (src/swapvm/SmileSwapVMRouter.sol) inherits the official SwapVM core + AquaOpcodes instruction set and registers one custom instruction at opcode 33: _optionPremiumXD. This router is the Aqua app LPs ship to.
  • The strategy is a real SwapVM program: salt(authId) → deadline(expiry) → optionPremium(oracle, σ-source, tokens, K-range, expiry, α) — composed from two official Controls instructions plus the custom pricing opcode.
  • The taker picks the strike per swap via SwapVM taker instruction args, so one shipped Aqua balance quotes the entire option chain in [Kmin,Kmax][K_{min}, K_{max}] — displayed depth is a function of wallet balance, not per-strike pre-allocation.
  • Two-sided market from swap direction: the forward direction (premium → collateral) prices at Ask (rounds against the taker, up); the reverse direction (collateral → premium) prices at Bid (rounds down). close() executes the reverse swap: the holder is paid the live Bid, the escrowed collateral is Aqua.push()ed back into the LP wallet, and the range's JIT capacity self-restores — the buyback funded by premiums the LP already earned.
  • Covered calls execute as official SwapVM swaps (premium Aqua.push()ed to the LP wallet, collateral Aqua.pull()ed JIT). Cash-secured puts use the vault itself as an official AquaApp (same JIT pull(), under the official per-strategy reentrancy lock) since premium and collateral share one token (USDC).
  • Capacity is enforced by Aqua itself: over-buying a range underflows the maker's virtual balance inside the official Aqua.pull() — the vault keeps no parallel accounting. On a mainnet fork the deploy script reuses the production Aqua deployment (0x4999…6D31).

Revenue model (protocol fee via the official fee opcode)

Every option buy carries a protocol fee (default 1%, capped at 5%) that accrues to a fee recipient — e.g. the 1inch DAO treasury — routed through the official SwapVM fee instruction, not custom plumbing:

  • The call-strategy program grows to five instructions: salt → deadline → jumpIfTokenIn → aquaProtocolFee → optionPremium. The official Fee._aquaProtocolFeeAmountInXD (opcode 28) grosses the fee up on top of the Ask — the buyer pays ask + fee, the fee recipient is paid through the official Aqua.pull(), and the LP always nets the full premium.
  • The official Controls._jumpIfTokenIn (opcode 11) makes fees direction-aware in bytecode: sellbacks (collateral-in) jump past the fee instruction, so closing a position is fee-free and never double-charges.
  • Puts (the vault-as-AquaApp leg) apply the identical gross-up vault-side.
  • Fee terms are snapshotted per authorization — an LP sees the exact fee at ship time and it can never change under them; governance changes apply only to new ranges. Fee-enabled ranges ship with $25 of premium-token virtual headroom (an allowance number, no tokens move) because the official opcode pulls the fee before the buyer's premium push lands.

Collateralization Model

V1 (current) — Cash-Secured / Covered (Fully Collateralized)

The simplest and safest model. To mint an ETH Call at a 3,500 strike, the LP backs it with 1 WETH (Covered Call). To mint a Put, the LP backs it with 3,500 USDC (Cash-Secured Put). The collateral is authorized JIT via Aqua — it never leaves the LP's wallet until a buyer matches — but it is always fully present and earmarked.

Solvency is trivially guaranteed: if the option expires in-the-money, the locked assets are delivered to the buyer. No price oracle is needed for margining and no liquidation engine exists — there is nothing to liquidate.

What V1 can do — because the premium is a deterministic on-chain function of (spot, strike, T, σ):

Capability Why the mechanism allows it
Known upfront premium, fixed European expiry Computable at click time from the surface — the instrument OptionStrat sellers actually trade
Sell covered calls and cash-secured puts Full collateral makes writing safe with zero margin machinery
Buy calls & puts at any strike in a range Taker picks the strike per swap; one Aqua balance quotes the whole chain
Exit anytime at a live Bid The same strategy quotes both sides — no counterparty search to close
Calendars & diagonals Fixed expiries + per-tenor σ buckets give a real term structure
Vol competition LPs quote their own σ multiplier; bestQuote routes to the touch (S5/S6)
Trustless settlement, ERC-20 positions Chainlink-round-verified expiry price; options compose anywhere
Free quoting The entire pricing path is view — scanning every range costs nothing

What V1 cannot do — and the mechanism's root cause for each:

Gap Root cause
Know its vol is right without trades σ only moves on fills; an untraded range quotes yesterday's vol (L6/L7, S7 is the fix)
Avoid paying informed flow Quotes derive from a lagging oracle; adverse selection is priced (R1–R5), never eliminated (L1/L2/L5)
Capital-efficient short legs A spread's short leg posts full collateral as if naked until S12 netting — spreads and (on a cash-settled vault) the single-structure iron condor now escrow only the true max loss
Naked writing No mark, no liquidations — that is the entire V2 ladder below
Assets without a price feed The mechanism needs external spot; long-tail listings are feed-constrained (S11)
Exact Black-Scholes prices The on-chain formula deliberately omits N(d₁)/N(d₂) — gas-cheap, roughest deep-ITM and near expiry

Versus the Uniswap mechanism (Panoptic) — really oracle-quoted implied vol vs pool-realized fee flow:

Smile: surface + oracle Panoptic: Uniswap LP synthesis
Premium known at entry ✔ fixed, upfront ✘ streams while spot sits near strike; path-dependent
Seller paid for jump risk ✔ implied vol charges upfront ✘ paid realized fees; gaps deliver loss with no premium
Expiries / calendars / expire-worthless ✔ native ✘ structurally impossible (perpetual)
Pricing-oracle risk ✘ inherent — priced by spreads ✔ none — its genuine win
Vol staleness in quiet markets ✘ σ waits for a trade ✔ n/a — no vol model at all
Asset universe Needs a feed Any Uniswap v3 pool
Solvency Guaranteed, no liquidations Margined: liquidations + forced exercise
Capital efficiency today Low until S12 Higher (partial collateral on spreads)

One line: Smile's mechanism trades oracle risk for a real options contract; Uniswap's trades the contract for freedom from oracles. Smile can state a price and a date and guarantee payment, at the cost of defending a lagging oracle and a trade-gated vol surface; Panoptic never has a wrong oracle price, at the cost of never telling you what your hedge costs or when it ends.

V2 (designed, deliberately deferred) — The capital-efficiency ladder

The obvious V2 question is "when do we allow uncovered calls?" — and the answer starts by untangling two things that usually get conflated: margin is what protects the protocol; delta hedging is what protects the maker. No exchange mandates hedging — Deribit and the CME require margin and mark to market, full stop — but at the 5–20× leverage naked writing implies, an unhedged directional book has near-certain ruin, so in practice every surviving naked writer hedges. A margined V2 therefore implies a delta-hedged maker base even though the contracts never enforce it. The right design lever is to recognize hedges rather than require them: portfolio margin that nets a short call against long WETH (rediscovering today's covered call as the zero-extra-margin case), spreads against each other, and — the step that quietly forces a perp integration, as Derive's cross-margin account shows — an on-venue hedge leg the vault can actually see. A hedge on a CEX is invisible to the contract and can reduce nothing.

Full margin is also not one feature but a five-part machine, and the price list deserves to be stated in advance:

  1. A mark for every open option — margin is collateral ≥ k × current liability, which needs a continuously updated fair value, i.e. an IV oracle. Circular for a venue whose purpose is to discover IV; dependent if imported from Deribit.
  2. A liquidation engine + keeper network — bots that buy back or auction positions when maintenance margin breaks.
  3. A liquid market to liquidate into — the forgotten constraint. Liquidating a short call means buying that call back at the worst moment; on a thin book the engine has no counterparty. Naked margin is only safe after the venue is liquid — it can never be what bootstraps liquidity.
  4. An insurance fund — crypto gaps faster than liquidations land; bad debt is a when, not an if, and someone must eat it.
  5. Sub-second-grade price feeds for the margin marks.

Because each part is expensive and the last three import exactly the risks V1 was built to exclude, V2 is sequenced as a ladder — each rung captures capital efficiency without paying for machinery the rung below didn't need:

Rung Mechanism Liquidation machinery Who it serves
1 Yield-bearing collateral (S4) — escrowed wstETH/sDAI keeps earning while backing quotes None Every LP: makes full collateral cheap instead of smaller
2 Defined-risk netting (S12) — implemented at EthOnline 2026 as SpreadVault (see the Continuation Track section below): a call spread margined at its true max loss (K₂−K₁)/K₂ WETH, not naked-per-leg None — pure position accounting The spread/condor seller (the core Smile user)
3 Partial-collateral putsimplemented at EthOnline 2026 as MarginVault (S13, opt-in, puts only): initial margin min(K, intrinsic + 50% of the worst-of-hour Chainlink mark), maintenance at 30%, margin call → takeover auction → backstop pool → insurance → (haircut, loudly) Light — bounded bad debt, see L13 Yield-focused put writers
4 Naked calls + cross-margin — unbounded liability, the full five-part machine; MarginVault's waterfall is the machine, calls follow once a WETH shortfall can be paid All of it Professional delta-hedging desks

Rungs 1–3 preserve the property that is Smile's one absolute differentiator against Derive, Panoptic, and Deribit alike: an option, once written, can always pay. Rung 4 breaks it — so rung 4 stays gated behind evidence that rungs 1–3 left real demand unmet, and its natural constituency (professional makers who hedge in milliseconds) may be better served by the signed-quote RFQ tier (Limitations, R6), where pros manage their own leverage off-chain and the trustless vault never underwrites it.


📐 Mathematical Specification

1. Multiparameter Volatility Surface

σstrike(T)=σtenor(T)max ⁣(0.1,  1+αln(K/S)2+βln(K/S))\sigma_{strike}(T) = \sigma_{tenor}(T) \cdot \max\!\big(0.1,\; 1 + \alpha \cdot \ln(K/S)^2 + \beta \cdot \ln(K/S)\big)

  • σtenor(T)\sigma_{tenor}(T): demand-driven IV stored per tenor bucket[0,7d)[0,7d), [7,30d)[7,30d), [30,90d)[30,90d), [90d,)[90d,\infty) — the term structure of the surface (OptionPricingHook.sigmaFor).
  • α\alpha: smile curvature (default 2.0). OTM/ITM strikes price above the tenor σ; ATM returns it exactly.
  • β\beta: signed skew tilt (default 0; negative = downside/put skew, matching empirical crypto markets).
  • The multiplier is floored at 0.1 so deep wings can never collapse σ to zero.

Reading the surface like a trader. The (σ, α, β) triple is exactly the level / skew / curvature decomposition options desks have always used, so it translates one-for-one into the three numbers vol traders quote each other — no new model, no new Greeks, just the standard dictionary:

Parameter Trader's name Plain English Approximate conversion*
σtenor\sigma_{tenor} ATM vol The price of movement itself, regardless of direction. Multiply by T\sqrt{T} for the expected move by expiry — the drift the premium is charging for. identical
β\beta (skew) 25Δ risk reversal (RR) Which direction costs more. Negative = crash insurance is pricier than upside (typical for equities/crypto). RR2σtenorβk25RR \approx 2\,\sigma_{tenor}\,\beta\,k_{25}
α\alpha (curvature) 25Δ butterfly (BF) How much extra a big move costs vs a small one — the market's fat-tails charge over a perfect bell curve. BFσtenorαk252BF \approx \sigma_{tenor}\,\alpha\,k_{25}^2

*where k25=ln(K25Δ/S)k_{25} = \lvert\ln(K_{25\Delta}/S)\rvert, the log-moneyness of the "25-delta" reference strikes — the OTM call and put with ~25% probability of finishing in the money, the near-universal convention for measuring the wings. The frontend computes RR/BF exactly (evaluating the smile at the true 25Δ strikes — surfaceQuotes in frontend/lib/options.ts) and shows them in the One-Click Income panel with plain-language captions.

Two things this framing buys: (1) takers get a sanity check in familiar units — an expected-move band instead of an abstract α; (2) LPs see their L6 surface-parameter risk in the same terms a Deribit market-maker manages daily — vega against the ATM level, RR-sensitivity against the skew, fly against the curvature — rather than as bespoke protocol exposures. (Client-facing greeks are untouched: takers always see plain Black-Scholes delta/gamma/theta/vega evaluated at the smile σ, whatever parameterization produces it.)

2. Premium Calculation

P=max(±(SK),0)intrinsic (call/put)+SσstrikeTmin(S,K)max(S,K)moneyness-damped time-valueP = \underbrace{\max(\pm(S - K),\, 0)}_{\text{intrinsic (call/put)}} + \underbrace{S \cdot \sigma_{strike} \cdot \sqrt{T} \cdot \tfrac{\min(S,K)}{\max(S,K)}}_{\text{moneyness-damped time-value}}

  • σ is a parametric level, not a Black-Scholes implied vol. The time-value term has no 1/√(2π) factor and its wing damping is linear, so at σ = 80% this formula charges roughly 2.5× Black-Scholes at the money and more in the wings (S = 2,524, 30 days: $597 vs $242 at K = 2,500; $457 vs $50 at K = 3,200). The model is self-consistent — the hook's feedback and the α/β skew act on this σ — but a Black-Scholes IV back-solved from a fill (as the Trade tab's price chart does) is a different, larger number. Replacing the term with S·N(d₁) − K·N(d₂) costs about 5k more gas per evaluation (measured: 9.4k → 14.7k) and is the planned pricing change for the next router deployment.
  • Ask (forward swap direction, opening): rounds against the taker (up).
  • Bid (reverse direction, sellback): rounds down. One strategy quotes both sides; the rounding asymmetry is the spread engine.
  • A protocol fee (default 1%) is grossed up on top of the Ask via the official SwapVM fee opcode — the LP always nets the full premium. Sellbacks are fee-free.

Gas-efficient on-chain approximation — omits N(d1)N(d_1) and N(d2)N(d_2) to avoid square-root-heavy distributions.

3. σ Feedback Loop (tenor-aware)

σtenor,t+1=σtenor,t+γsign(trade)\sigma_{tenor,\,t+1} = \sigma_{tenor,\,t} + \gamma \cdot \text{sign}(\text{trade})

  • +γ+\gamma on every buy() — bumps only the traded tenor bucket.
  • γ-\gamma on every close() sellback — decays the same bucket.
  • Uniswap v4 afterSwap (no tenor info) shifts the whole surface.
  • γ=0.5%\gamma = 0.5\% per trade. This creates a price-impact-like mechanism: heavy buying steepens the surface and raises premiums, attracting arbitrageurs who sell back to earn the spread.

Design note — why pricing is on-chain, and why σtenor\sigma_{tenor} is a step function. OptionPricingHook.sigmaFor is read atomically inside the same swap that buys or sells the option, so the price a trader gets is exactly whatever the bucket lookup returns at that block — no off-chain quote to go stale or be front-run. This isn't architecturally required: AquaOptionSettlement already sources its expiry price off-chain via the Chainlink CRE forwarder (§5), so an RFQ-style premium quote (a signed off-chain price, verified on-chain much like a CRE report) is possible in principle. The tradeoff:

  • On-chain step lookup (current): fully permissionless and atomic, no quoting service to keep live — but σtenor(T)\sigma_{tenor}(T) is discontinuous at the 7d/30d/90d bucket edges (visible as terraces on the Vol Surface tab), and each trade can only afford to move the one bucket it landed in.
  • Off-chain quoted pricing: could interpolate σtenor(T)\sigma_{tenor}(T) smoothly across tenors, but reintroduces a liveness/trust dependency on the quoter, and blending a trade's demand feedback across neighboring buckets (instead of bumping one bucket) opens a manipulation surface — trading right at a bucket edge could nudge a bucket nothing actually traded in.

V1 keeps pricing on-chain and discrete; smooth interpolation is left for a future RFQ-style quoting layer.

4. Black-Scholes Delta (Frontend)

Delta (Δ\Delta) is computed client-side for the matrix display. Not used in on-chain pricing.

Δ=N(d1),d1=ln(S/K)+12σstrike2TσstrikeT\Delta = N(d_1), \qquad d_1 = \frac{\ln(S/K) + \frac{1}{2}\sigma_{strike}^2 \cdot T}{\sigma_{strike} \cdot \sqrt{T}}

N()N(\cdot) is approximated via Abramowitz & Stegun 26.2.17 (max error 1.5×1071.5 \times 10^{-7}, no lookup tables). σstrike\sigma_{strike} from §1 is used — ensuring delta reflects the vol surface curvature, not flat vol.

Delta ranges 0–1 for calls (0 = deep OTM, 1 = deep ITM). A 0.5-delta call is approximately ATM.

A parametric option is only as trustworthy as the price it settles against. At expiry every open series needs one final spot price SfinalS_{final} written on-chain, because that single number decides every payout: holders redeem the in-the-money intrinsic and the LP reclaims the remainder (see §6 flow). Every series is registered with AquaOptionSettlement at first mint, and can then be settled by either of two paths:

Path A — permissionless Chainlink-round settlement (trustless). Anyone — a keeper, the holder, the LP — calls settleWithChainlinkRound(seriesId, roundId) with the Chainlink ETH/USD round covering expiry. The contract verifies on-chain that the round was updated at/after expiry and that its predecessor was updated before expiry (i.e. it is the first post-expiry round), so nobody can cherry-pick a later, more favorable price:

Sfinal=getRoundData(roundId).answer    (8-dec)    WAD 18-decS_{final} = \mathtt{getRoundData(roundId).answer} \;\; (\text{8-dec}) \;\rightarrow\; \text{WAD 18-dec}

No trusted writer exists on this path — settlement liveness reduces to the feed's.

Path B — Chainlink CRE (scheduled keeper). A CRE cron trigger fires the settlement workflow at expiry: the DON reads the same aggregator at the last finalized block (every node observes an identical value), reaches consensus, and the DON-signed report calls settleSeries(seriesId, S_final) through the CRE forwarder — so a series settles on schedule even if nobody races to call Path A.

In short: 1inch Aqua holds the collateral, Uniswap prices and routes the trade, and settlement is available trustlessly to anyone with Chainlink CRE as the scheduled closer.

Design note. Because both paths resolve to the on-chain Chainlink feed, the DON's role is a scheduled, trust-minimized keeper (deterministic read + signed write) rather than novel off-chain data sourcing.

Settlement (§5 above) always resolves through Chainlink's on-chain round history — that's what makes permissionless expiry-bracketing verifiable, and it never changes. Quoting (pricing a live buy()/close()) is a separate concern and reads whatever IPriceOracle the vault was deployed with:

  • Default — Chainlink Data Feeds. OptionPremiumInstruction and the vault's put-pricing path call latestRoundData() directly, gated by a maxStalenessSec freshness check. This is a push oracle: the price is only as fresh as Chainlink's last heartbeat/deviation-triggered update, which is the root cause of the oracle-latency gap documented as L1/L2 in Limitations (stale-quote sniping, and an "invisible window" of sub-threshold drift with no on-chain signal at all).
  • Optional — PythSpotAdapter pull-oracle. Pyth is a first-party oracle: 100+ trading firms and exchanges submit price + confidence directly, aggregated into an update roughly every 400ms. It's a pull oracle — the taker fetches a signed update off-chain and posts it in their own transaction (PythSpotAdapter.refresh()), so the very next read in that transaction prices against a near-live spot instead of Chainlink's last published round. PythSpotAdapter.sol wraps this behind the same latestRoundData() shape the pricing path already expects (round ids are meaningless for a pull oracle and returned as zero; updatedAt maps to Pyth's publishTime), so swapping it in requires no changes to OptionPremiumInstruction or the vault. Scope is quoting only — settlement is untouched and still reads Chainlink rounds. See R5 in Solutions for the full design rationale and Limitations for what it does and doesn't fix.

To enable it at deploy time, set PYTH (the Pyth contract address on your target chain) and PYTH_PRICE_ID (the feed id, e.g. ETH/USD) in .envscript/Deploy.s.sol then deploys PythSpotAdapter and wires it in as the quoting oracle in place of the raw Chainlink feed. Leave both unset to use Chainlink Data Feeds for quoting (the default, and what the Sepolia addresses above run).


🔄 Flow Diagrams

Color key: 🟢 1inch Aqua · 🩷 Uniswap · 🔵 Chainlink

0. System Overview

sequenceDiagram
    participant Trader
    participant Frontend
    participant SwapVM as 🟢 SwapVM (OptionPricingEngine)
    participant Aqua as 🟢 1inch Aqua (AquaCollateralVault)
    participant Maker as 🟢 Maker (LP Wallet)
    participant CRE as 🔵 Chainlink CRE DON
    participant Settle as 🟢 AquaOptionSettlement

    rect rgba(60,80,120,0.12)
    Note over Trader,Maker: Trade (pre-expiry)
    Trader->>Frontend: Select strike & expiry (or compose a strategy)
    Frontend->>SwapVM: quote(order, tokens, amount)
    SwapVM-->>Frontend: Ask (fee-inclusive)
    Frontend-->>Trader: Option matrix / strategy builder
    Trader->>Aqua: buy(authId, K, amount, maxPremium)
    Aqua->>SwapVM: swap — custom optionPremium opcode prices at Ask
    SwapVM->>Maker: Aqua.push premium → LP wallet
    SwapVM->>Maker: Aqua.pull collateral JIT → vault escrow
    Note over SwapVM: Aqua.pull fee → DAO treasury (1%)
    Aqua-->>Trader: OptionToken minted
    end

    rect rgba(40,90,140,0.18)
    Note over CRE,Settle: Expiry settlement (DTE = 0) — two paths
    Trader->>Settle: settleWithChainlinkRound(seriesId, roundId) — ANYONE, trustless
    Note over Settle: verifies first post-expiry round on-chain
    CRE->>Settle: …or settleSeries(S_final) via scheduled DON (onlyCRE)
    Note over Settle: series settled=true → redemption unlocked
    Trader->>Aqua: redeem(optionToken, amount) → ITM intrinsic from escrow
    Maker->>Aqua: reclaimCollateral(optionToken) → exact remainder
    end

1. Range Authorization + Ship (LP)

The LP authorizes a strike range from one collateral pool, then ships it on the official Aqua registry. No collateral moves at any stage — it stays in the LP's wallet earning yield; Aqua.ship() only records virtual balances.

sequenceDiagram
    participant LP
    participant Frontend
    participant Vault as 🟢 AquaCollateralVault
    participant Aqua as 🟢 official 1inch Aqua

    LP->>Frontend: K_min, K_max, DTE, maxCollateral
    Frontend->>Aqua: ERC20.approve(Aqua) — collateral + premium token
    Note over LP: collateral stays in wallet — earns yield until matched
    Frontend->>Vault: authorizeRange(...) → authId
    Vault-->>Frontend: getShipParams(authId) — exact official calldata
    Frontend->>Aqua: ship(app, strategy, tokens, amounts)
    Note over Aqua: strategy = real SwapVM bytecode (salt → deadline → jumpIfTokenIn → fee → optionPremium)
    Aqua-->>LP: ✓ range live — one balance quotes the whole chain

2. Primary Market Buy (Trader)

With a Trading API key configured, premium payment is routed through the Uniswap Trading API (EXACT_OUTPUT ETH→USDC), giving an on-chain Uniswap tx before the vault call; otherwise the buyer's USDC is pulled directly.

sequenceDiagram
    participant Buyer
    participant UniAPI as 🩷 Uniswap Trading API
    participant Vault as 🟢 AquaCollateralVault
    participant Router as 🟢 SmileSwapVMRouter (official SwapVM)
    participant LP as 🟢 LP Wallet
    participant DAO as 🟢 Fee Recipient (DAO)

    Buyer->>UniAPI: EXACT_OUTPUT ETH→USDC (premium funding)
    UniAPI-->>Buyer: Universal Router calldata
    Buyer->>Vault: buy(authId, K, amount, maxPremium)
    Vault->>Router: quote + swap — taker picks K via instruction args
    Note over Router: custom opcode 33 prices Ask off the live vol surface
    Router->>LP: Aqua.push premium (ask + fee) → LP wallet
    Router->>DAO: Aqua.pull fee (1% gross-up) → treasury
    Router->>Vault: Aqua.pull collateral JIT → escrow
    Vault->>Buyer: mint OptionToken
    Note over Vault: Hook bumps the traded tenor bucket σ += γ

3. Close Position = Sellback at Bid (Holder)

A holder exits before expiry by selling the option back at the live Bid — a reverse swap through the same shipped SwapVM strategy. Escrowed collateral returns to the LP wallet via Aqua.push(), which restores the range's JIT capacity; the Bid premium is Aqua.pull()ed from the LP wallet straight to the holder (funded by premiums the LP already earned). Sellbacks are fee-free — the strategy bytecode jumps past the fee opcode in the reverse direction. This is also the path arbitrageurs use to monetize σ corrections.

sequenceDiagram
    participant Holder
    participant Vault as 🟢 AquaCollateralVault
    participant Router as 🟢 SmileSwapVMRouter (official SwapVM)
    participant LP as 🟢 LP Wallet
    participant Hook as 🩷 OptionPricingHook

    Holder->>Vault: close(optionToken, lp, amount, minPayout)
    Vault->>Vault: OptionToken.burn(holder, amount)
    Vault->>Router: reverse swap (collateral in → premium out)
    Note over Router: jumpIfTokenIn skips the fee — Bid priced by opcode 33
    Router->>LP: Aqua.push collateral → wallet (capacity self-restores)
    Router->>Holder: Aqua.pull Bid premium → holder
    Vault->>Hook: bumpSigma(false, timeToExpiry)
    Note over Hook: traded tenor bucket σ -= γ
    Vault-->>Holder: ✓ sold back at Bid

4. Arbitrageur as Emergent Market Maker

When on-chain σ diverges from market IV, arbitrageurs capture the spread by buying at the Ask, delta-hedging on spot, and selling back at the Bid once their own demand re-rates σ. The sellback mechanism is what makes the round trip monetizable — their activity is the correction. This is the emergent market-making loop.

sequenceDiagram
    participant Arb as Arbitrageur
    participant Vault as 🟢 AquaCollateralVault
    participant UniPool as 🩷 Uniswap v4 ETH/USDC
    participant Hook as 🩷 OptionPricingHook

    Note over Arb: σ_tenor < market IV — options underpriced
    Arb->>Vault: buy(authId, K, amount, maxPremium) — at Ask
    Arb->>UniPool: short ETH delta hedge
    Note over Arb: σ_tenor rises toward market IV
    Arb->>Vault: close(optionToken, lp, amount, minPayout) — at Bid
    Arb->>UniPool: unwind delta hedge
    Note over Arb: profit = (σ_market - σ_entry) * vega

5. Secondary Market Swap (Uniswap v4)

Existing OptionTokens can be resold. The hook vetoes mispriced swaps and adjusts σ. Secondary market only — ERC-20 ownership transfers, no minting.

sequenceDiagram
    participant Seller
    participant Pool as 🩷 Uniswap v4 Pool
    participant Hook as 🩷 OptionPricingHook
    participant Oracle as 🔵 Chainlink Price Feed

    Seller->>Pool: swap(OptionToken → USDC)
    Pool->>Hook: beforeSwap(params, hookData)
    Hook->>Oracle: fetch S
    Note over Hook: Veto if |P_exec - P_fair| > 5%
    Hook-->>Pool: ✓ OK
    Pool->>Pool: OptionToken transfers to buyer
    Pool->>Hook: afterSwap(params)
    Note over Hook: exactIn (sell) → σ -= γ
    Hook-->>Pool: ✓ σ updated

6. Settlement & Redemption

Every series is registered at first mint. At expiry it settles through either path — permissionlessly with the Chainlink round covering expiry (verified on-chain, no trusted writer), or via the scheduled CRE DON. Holders then redeem the cash-settled intrinsic from the vault's escrow; the LP reclaims the exact remainder — in any order, with full conservation.

sequenceDiagram
    participant Anyone
    participant CRE as 🔵 Chainlink CRE DON
    participant Feed as 🔵 Chainlink ETH/USD Feed
    participant Settlement as 🟢 AquaOptionSettlement (price registry)
    participant Vault as 🟢 AquaCollateralVault (escrow)
    participant Maker as 🟢 Maker (LP)
    participant Holder as Trader (Holder)

    alt Path A — permissionless (trustless)
        Anyone->>Settlement: settleWithChainlinkRound(seriesId, roundId)
        Settlement->>Feed: getRoundData(roundId) + predecessor
        Note over Settlement: verify FIRST round at/after expiry → S_final (WAD)
    else Path B — scheduled CRE keeper
        CRE->>Feed: latestRoundData() @ finalized block
        Note over CRE: DON consensus → S_final (WAD)
        CRE->>Settlement: settleSeries(seriesId, S_final) · onlyCRE forwarder
    end
    Note over Settlement: settled=true — price written exactly once

    Holder->>Vault: redeem(optionToken, amount)
    Note over Vault: call → (S−K)/S of collateral per unit · put → (K−S) USDC
    Vault->>Holder: transfer intrinsic from escrow
    Maker->>Vault: reclaimCollateral(optionToken)
    Vault->>Maker: everything not owed to outstanding holders

MarginVault lifecycle (opt-in margin + liquidation waterfall)

The six stages of script/margin-lifecycle.sh:

flowchart TD
    S1["1 · Writer opens a margined put range<br/>authorizes IM capacity, ships to Aqua — nothing locked yet"]
    S2["2 · Holder buys 1 put, $3,000 strike<br/>only initial margin ~$1,500 pulled JIT through Aqua<br/>(the main vault would cash-secure the full $3,000)"]
    S3["3 · ETH crashes to $2,000<br/>put is $1,000 in-the-money · maintenance $1,600 > $1,500 locked<br/>keeper flags the position"]
    S4["4 · One hour grace to add margin<br/>writer does not, so the auction opens"]
    S5{"5 · Waterfall — who covers the gap?<br/>writer margin, then bidder, then backstop, then insurance"}
    T["Takeover: a bidder assumes the short,<br/>posts full margin, earns a 1 to 10% bonus<br/>out of the liquidated writer's margin"]
    B["Absorb: no bidder in 30 min, so the backstop pool<br/>takes the short, drawing only the shortfall<br/>then the insurance fund, then a holder haircut"]
    S6["6 · Expiry at $2,000<br/>settle off Chainlink, finalize,<br/>holder redeems $1,000 intrinsic in cash"]
    S1 --> S2 --> S3 --> S4 --> S5
    S5 -->|a bidder appears| T
    S5 -->|nobody bids| B
    T --> S6
    B --> S6

📍 Deployed Addresses (Sepolia)

Redeployed 2026-09-10 (EthOnline 2026) — the full current stack, on real Circle USDC, canonical WETH and the real Chainlink ETH/USD feed. Every address, deploy hash and demo transaction: the Sepolia deployment notes; .env.sepolia.example points the app at it. The Graph Studio subgraph smile-sepolia indexes this vault (see the subgraph notes).

Contract Address
Aqua (official registry) 0x915Bc53936Ecb14A18dB8270A4a648E8dE248749
SmileSwapVMRouter 0x44E2213838913aeC52410ec815b07D15Fcf0a72c
OptionPricingEngine 0x681Bd7583B6612FFf1539781e8d5d7Db565994B3
OptionPricingHook 0xCa84Df6F9317FABDE1fD21f4bee25Cb2a8ba1676
AquaCollateralVault 0x82AcBBFE5E03510d5407d8C50435B08e6d2d0a4D
AquaOptionSettlement 0x17aAAf612cB5b7b3749Cf22b0b2e0CB1AdA77ca1
SmileQuoteLens 0xad1cE2065f1588caFB6BA6176D1b87cf4Ec7B8D6
SpreadVault (S12) 0x94eE3E1747e96fd643f464ae42db5899Ce878391
MarginVault (S13) 0x23F9a08F44fBBCABe9Fdf0d458f226ABb3A84742
MarginBackstop 0x6eEE1ec5F1AFA7Fb8353016a50fBA9C50791FdA2
USDC (Circle Sepolia) 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
WETH (canonical Sepolia) 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9
Chainlink ETH/USD 0x694AA1769357215DE4FAC081bf1f309aDC325306

Frontend deployed at https://oslinin.github.io/Smile (WalletConnect enabled). The live site still targets the pre-event v1 contracts (AquaCollateralVault 0x5115…f887, incompatible ABI); point a build at .env.sepolia.example to use this deployment.


🛠️ How to Run the Project

1. View the Live Sites

Two builds of the same code:

URL What it carries
Vercel (server build) smile-frontend-omega.vercel.app The full app: Sepolia + Arc Testnet, the AI copilot (/api/copilot), the subgraph proxy (/api/subgraph, gateway key stays server-side), the help site at /help.html
GitHub Pages (static export) oslinin.github.io/Smile The same app without the server routes — no copilot, subgraph read directly from Studio

Deploying to Vercel. The Vercel project points at this repository with root directory frontend and the continuation-track branch as its production branch; every push builds and promotes automatically. frontend/next.config.ts produces a server build whenever NEXT_PUBLIC_BASE_PATH is unset (the Pages workflow is the only thing that sets it), so no Vercel-specific configuration file exists. Environment variables set in the Vercel project (Production):

Variable Purpose
COPILOT_PROVIDER, COPILOT_MODEL Which LLM backs the copilot (anthropic / openai / google / openrouter) and the model id
ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENROUTER_API_KEY The one key matching the provider
NEXT_PUBLIC_COPILOT=1 Shows the copilot button (unset on the static build)
SUBGRAPH_URL_11155111, SUBGRAPH_URL_5042002 (or one SUBGRAPH_URL for all chains) The Graph gateway URL per chain, carrying the API key; used only by /api/subgraph
NEXT_PUBLIC_WC_PROJECT_ID WalletConnect project id for phone wallets
COPILOT_RPC_SEPOLIA, COPILOT_RPC_ARC (optional) RPCs the copilot's server-side reads use
COPILOT_MCP_SERVERS (optional) JSON seed of MCP servers offered in the copilot's ⚙ menu

Contract addresses are not environment variables on either live site: frontend/config/wagmi.ts carries the Sepolia and Arc address maps and resolves them from the connected chain. To deploy your own copy: import the repo in Vercel, set the root directory to frontend, add the variables above, push.

2. Local Frontend Development

cd frontend
pnpm install
pnpm run dev

Or, from the repo root (no cd needed — pnpm targets the workspace by name):

pnpm --filter frontend dev

Open http://localhost:3000. The UI includes the option-chain matrix, the LP range-authorization flow, and an OptionStrat-style strategy builder — 20 named strategies (spreads, condors, butterflies, straddles, backspreads, calendars) grouped by market outlook, with up to 6 custom legs, per-leg expiry, a T+0 value curve, breakevens, probability of profit, and net greeks. Entry premiums are quoted with the same smile the on-chain instruction charges, so what you see is what vault.buy() costs.

The Vol Surface · Python tab renders the live 3-D volatility surface σstrike(K,T)\sigma_{strike}(K,T) with matplotlib (a Flask service in volsurface/). Every confirmed buy/sell POSTs to the renderer, which bumps the traded tenor bucket by ±γ\pm\gamma — the same feedback loop the on-chain OptionPricingHook.bumpSigma applies — so the surface visibly re-rates as order flow arrives. Start it standalone with ./volsurface/run.sh (it also comes up automatically with ./local.sh); the tab shows a hint instead of a broken image when the service is offline.

3. Smart Contract Development (Foundry)

forge build   # compile all contracts (incl. the vendored official 1inch stack)
forge test    # run the 82-test suite

4. Deploy to Anvil (Local)

The quickest path: ./local.sh starts Anvil, deploys all contracts, writes frontend/.env.local, launches the vol-surface renderer, and starts the dev server in one step.

When you're done, stop everything it started:

fuser -k 8545/tcp 3000/tcp 8000/tcp

(This is the same port-based cleanup local.sh runs on every invocation, so it's safe even if a previous run didn't finish cleanly — unlike kill $(cat /tmp/*-options.pid), it won't error out on a missing PID file.)

Re-running ./local.sh also stops any previous instances automatically before starting fresh.

To deploy manually (e.g., to iterate on the script):

# Terminal 1 — start Anvil
anvil --chain-id 31337 --block-time 1 --port 8545

# Terminal 2 — deploy (uses Anvil's pre-funded account 0)
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  forge script script/Deploy.s.sol:Deploy \
  --rpc-url http://localhost:8545 \
  --broadcast \
  --skip-simulation

5. Deploy to Sepolia

Copy .env.example.env, fill in PRIVATE_KEY and RPC_SEPOLIA, then:

source .env
PRIVATE_KEY="$PRIVATE_KEY" \
FEE_RECIPIENT="$FEE_RECIPIENT" \
forge script script/Deploy.s.sol:Deploy \
  --rpc-url "$RPC_SEPOLIA" \
  --broadcast

FEE_RECIPIENT is where the 1% protocol fee accrues (e.g. a DAO treasury); it defaults to the deployer if unset. The script outputs NEXT_PUBLIC_* addresses; copy them into frontend/.env.local (or set NEXT_PUBLIC_CHAIN_ID=11155111).

To verify contracts on Etherscan at the same time:

source .env
PRIVATE_KEY="$PRIVATE_KEY" forge script script/Deploy.s.sol:Deploy \
  --rpc-url "$RPC_SEPOLIA" \
  --broadcast \
  --verify \
  --etherscan-api-key "$ETHERSCAN_API_KEY"

The required Chainlink integration is a CRE workflow that performs an on-chain state change: on a cron schedule the DON reads the Chainlink ETH/USD feed at the last finalized block, reaches consensus, and the DON-signed report calls AquaOptionSettlement.settleSeries(seriesId, spotPrice) on-chain.

File Role
cre-workflow/settlement/workflow.ts The workflow itself — cron trigger → callContract(latestRoundData) on the Chainlink feed → DON consensus → DON-signed writeReportsettleSeries(). Compiles to WASM.
cre-workflow/settlement/config.json Runtime config: schedule, seriesId, target settlementAddress, priceFeedAddress (Chainlink ETH/USD), gasLimit, and chain selector.
cre-workflow/settlement/package.json Deps + typecheck script. Build + simulate run via the cre CLI (cre workflow build|simulate settlement).

Access control. settleSeries() carries an onlyCRE modifier (AquaOptionSettlement.sol:38) — only the CRE forwarder address passed to the constructor at deploy time can write the settlement price. The simulator uses a local forwarder; the live path requires the contract to be deployed with your registered CRE forwarder address.

Prerequisites

The current CRE CLI (v1.20.x) reworks several commands; this workflow is pinned to v1.11.0 to match @chainlink/cre-sdk@^1.11.0. The CLI is a binary installed via Chainlink's official script (it is not an npm package).

# 1. CRE CLI v1.11.0 — installs to ~/.cre/bin and appends it to PATH in ~/.bashrc.
curl -sSL https://app.chain.link/cre/install.sh | bash -s -- v1.11.0
source ~/.bashrc          # or open a new shell, so `cre` is on PATH
cre version               # → CRE CLI version v1.11.0

# 2. Bun ≥ 1.0 — cre-compile uses it to build the WASM target.
curl -fsSL https://bun.sh/install | bash

# 3. Authenticate — required even for local simulation.
cre login                 # opens a browser; or, non-interactively:
# echo 'CRE_API_KEY=<key from Account Settings at https://app.chain.link>' >> cre-workflow/.env

# 4. Install workflow + contract-binding deps (each folder has its own package.json).
cd cre-workflow
( cd settlement && bun install )
( cd contracts  && bun install )

# 5. (Optional) Regenerate the typed contract binding from the Foundry ABI.
#    Already committed under contracts/evm/ts/generated/; only needed if the ABI changes.
cp ../out/AquaOptionSettlement.sol/AquaOptionSettlement.json contracts/evm/src/abi/
cre generate-bindings evm --language typescript

Edit cre-workflow/settlement/config.json so seriesId matches the series you registered on-chain (copy it from the On-Chain Proof tab or from the forge script deploy logs), evm.settlementAddress points at your deployed AquaOptionSettlement, and evm.priceFeedAddress is the Chainlink ETH/USD feed for the chain (Sepolia: 0x694AA1769357215DE4FAC081bf1f309aDC325306). The active CRE target is read from CRE_TARGET in cre-workflow/.env (staging-settings → Sepolia RPC in project.yaml).

Build needs no auth; simulate does. cre workflow build compiles the WASM locally. cre workflow simulate gates on auth — run cre login (or set CRE_API_KEY) first.


6a. Demonstrate a successful CRE CLI simulation (the verified path)

First confirm it compiles (no auth needed):

cd cre-workflow
cre workflow build settlement
# ✓ Workflow compiled successfully
# ✓ Build output written to settlement/binary.wasm

Then run the simulation. The simulator spins up a local CRE runtime, fires the cron trigger, runs the workflow's on-chain feed read + DON consensus, and signs the report — all against the Sepolia RPC from project.yaml:

cre workflow simulate settlement --non-interactive --trigger-index 0
# `--non-interactive --trigger-index 0` selects the single cron trigger;
# omit both to pick it from an interactive menu.

Verified: CLI v1.11.0 reads the live Sepolia ETH/USD feed, runs DON consensus and signs the report, exiting 0. Full annotated transcript: the CRE simulation transcript.

Note — live broadcast is out of scope here. CRE delivers DON-signed reports through a KeystoneForwarder that calls onReport(bytes,bytes) on the receiver, whereas AquaOptionSettlement exposes a plain settleSeries(bytes32,uint256) guarded by onlyCRE. Wiring the live on-chain write (an onReport entrypoint + registered forwarder) is a follow-up; the CRE CLI simulation above is the demonstrated path.

7. Explore the Codebase — Knowledge Graph

The repo ships a generated map of itself — .understand-anything/knowledge-graph.json — 166 nodes (files, functions, contracts) and 219 edges (imports, calls, contains) grouped into 5 architecture layers, plus a 13-step guided tour, produced by an understand-anything-style codebase analyzer.

To browse it interactively:

./view-knowledge-graph.sh

This serves .understand-anything/ on http://localhost:4321 (override with PORT=...) and opens viewer.html — a force-directed graph you can filter by layer, search by file/symbol name, and click through the guided tour, with each node's summary and tags shown in a side panel. It must be served over HTTP (not opened as a local file:// page) so the browser can fetch() the JSON; the script handles that. Stop it with Ctrl+C.


End-to-End Demo Walkthrough

Step Actor Action Contract call
1 LP Connect wallet → Authorize Strike Range → approve official Aqua → register → ship ERC20.approve(Aqua) + AquaCollateralVault.authorizeRange() + Aqua.ship()
2 Trader Click Buy on a strike within LP's range → approve USDC → buy ERC20.approve(USDC) + AquaCollateralVault.buy(authId, K, amount, maxPremium) (SwapVM-priced, 1% fee → DAO)
2a Trader Compose a multi-leg position in the Strategy Builder (20 named strategies or custom legs) — each buy leg is a vault.buy() frontend only — payoff, T+0 curve, greeks, POP
3 Trader Click Close to sell back at the live Bid (reverse SwapVM swap; LP capacity restores) AquaCollateralVault.close(optionToken, lp, amount, minPayout)
4 Settle at expiry — anyone may supply the Chainlink round covering expiry; or run cre workflow simulate settlement AquaOptionSettlement.settleWithChainlinkRound() / settleSeries() via CRE
5 Trader Call redeem() to collect the cash-settled intrinsic (ITM only) AquaCollateralVault.redeem(optionToken, amount)
6 LP Call reclaimCollateral() to recover everything not owed to holders AquaCollateralVault.reclaimCollateral(optionToken)

📖 Glossary

Ethereum / Blockchain Terms

  • EOA (Externally Owned Account): A standard Ethereum wallet controlled by a private key (e.g., MetaMask). LPs and traders use EOAs; the protocol never takes custody of their funds.
  • ERC-20: Standard interface for fungible tokens. OptionToken follows this standard so positions can be resold on any DEX.
  • Non-custodial: The protocol never holds user assets. Collateral stays in LP wallets until a buyer matches; the vault only moves funds atomically on match.

DeFi Terms

  • LP (Liquidity Provider): A participant who backs trades. Here, LPs authorize the vault to pull collateral JIT — they are yield-seeking covered-option writers, not market makers.
  • Maker: The option writer (LP). Authorizes a strike range, provides collateral JIT, receives premiums, reclaims collateral at expiry.
  • Trader: The option buyer. Pays premium, receives an OptionToken representing the long position, redeems ITM payout at settlement.
  • CEX (Centralized Exchange): Off-chain exchange (Binance, Coinbase, Kraken). Referenced as real-world ETH/USD price sources; the live frontend spot is sourced via the Uniswap Trading API, while settlement reads the on-chain Chainlink feed.
  • DEX (Decentralized Exchange): On-chain exchange. Uniswap v4 provides secondary-market trading for OptionTokens.
  • DON (Decentralized Oracle Network): A tamper-resistant network of node operators that securely delivers external data to smart contracts (Chainlink).
  • CRE (Chainlink Runtime Environment): Off-chain computation environment for custom DON workflows (successor to Chainlink Functions).
  • JIT (Just-In-Time) Liquidity: Capital pulled from an LP's wallet only at trade execution — never locked idle. Enabled by 1inch Aqua.

Options Terms

  • Delta (Δ): Rate of change of premium per $1 move in spot. 0 = deep OTM, 1 = deep ITM for calls. Computed frontend-only via N(d1)N(d_1) using smile-adjusted σstrike\sigma_{strike}.
  • Strike Price (K): Price at which the option holder has the right to buy (call) or sell (put) at expiry.
  • Spot Price (S): Current market price of ETH/USDC, sourced from Chainlink.
  • DTE (Days to Expiry): Time remaining until settlement, in days.
  • K_min / K_max: The lower and upper bounds of an LP's authorized strike range.
  • OTM (Out-of-The-Money): No intrinsic value at expiry; LP reclaims 100% of collateral.
  • ITM (In-The-Money): Intrinsic value at expiry; holder receives payout, LP gets remainder.
  • IV (Implied Volatility / σ): Market's forecast of price movement. Stored per tenor bucket (sigmaFor), demand-weighted, adjusting with every trade.
  • Volatility Smile: OTM/ITM options trade at higher IV than ATM; modeled by αln(K/S)2\alpha \cdot \ln(K/S)^2 curvature.
  • Black-Scholes: Mathematical option pricing model. This protocol uses a parametric approximation (gas-efficient, no N(d1)N(d_1) on-chain).

Protocol-Specific Terms

  • Range Authorization: LP's single on-chain commitment to write options at any strike K[Kmin,Kmax]K \in [K_{min}, K_{max}] from one collateral pool. First buy at a new strike deploys an OptionToken lazily.
  • Yield Double-Dip: LP earns staking/lending yield on collateral (because it stays in their wallet via Aqua JIT) and option premium from buyers. Impossible in vault-locking designs.
  • Vol Surface / σ Feedback Loop: IV is stored per tenor bucket with a skew tilt. Every buy bumps the traded bucket up; every sellback decays it. Creates on-chain price discovery that arbitrageurs can trade against.
  • Emergent Market Maker: An arbitrageur who buys underpriced options (low σtenor\sigma_{tenor}) at the Ask, delta-hedges on Uniswap, and sells back at the Bid when σ corrects — capturing the spread while enforcing IV consistency.
  • Covered Call / Cash-Secured Put: Fully collateralized option: WETH backs calls (LP delivers ETH if exercised), USDC backs puts (LP purchases ETH if exercised). No naked writing; collateral IS the hedge.
  • SwapVM: 1inch highly-optimized VM for custom matching and pricing logic.
  • 1inch Aqua: 1inch primitive for JIT transfer of assets from LP self-custodial wallets.
  • Uniswap v4 Hooks: Smart contracts at swap lifecycle points: beforeSwap vetoes mispriced trades, afterSwap adjusts IV.
  • latestRoundData(): The Chainlink aggregator read returning the latest ETH/USD answer (8-decimal). CRE reads it at the last finalized block so every DON node agrees on the settlement price.

🏗️ Project Structure

├── lib/                      # Vendored official contracts (compiled unmodified)
│   ├── aqua/                 # 1inch/aqua — registry, AquaApp base, IAqua
│   ├── swap-vm/              # 1inch/swap-vm release/1.2 — VM core, opcodes, routers
│   └── forge-std/            # forge-std v1.11.0
├── src/                      # Smile contracts (Solidity 0.8.30)
│   ├── swapvm/               # SmileSwapVMRouter (opcode 33) + OptionPremiumInstruction
│   │                         #   + SmileMath + OptionPricingEngine (quoting facade)
│   ├── vaults/               # AquaCollateralVault (escrow/lifecycle)
│   │                         #   + AquaOptionSettlement (expiry-price registry)
│   ├── hooks/                # OptionPricingHook (Uniswap v4 + the vol surface)
│   ├── periphery/            # EthOnline 2026: SpreadVault (S12 netting AquaApp)
│   │                         #   + SmilePremiumLib + SpreadToken
│   │                         #   + MarginVault + MarginBackstop (S13 opt-in margin tier)
│   ├── mocks/                # MockV3Aggregator (local Chainlink feed)
│   └── OptionToken.sol       # ERC-20 option position
├── subgraph/                 # EthOnline 2026: The Graph subgraph (authorizations + fills)
├── frontend/                 # Next.js app
│   ├── components/           # OptionMatrix, AuthorizeRange, PayoffBuilder, LPDashboard, VolSurface, SpreadDesk
│   ├── lib/                  # options engine + 20-strategy catalog + subgraph client + AI copilot
│   └── config/               # Wagmi + contract addresses (Anvil / Sepolia / Arc) + official Aqua ABI
├── volsurface/               # Python (Flask + matplotlib) 3-D vol-surface renderer
│                             #   — evolves with each trade via the σ feedback loop
├── cre-workflow/             # Chainlink CRE workflow (TypeScript → WASM)
├── script/                   # Deploy.s.sol + DemoTrade.s.sol (live-node demo)
│                             #   + SpreadDemo.s.sol, spread-lifecycle.sh, arc-smoke.sh (EthOnline 2026)
├── docs/                     # grant proposal, build notes, CRE transcript, docs/plans/ (feature plans)
├── test/                     # Foundry tests (192 passing)
├── .understand-anything/     # Generated codebase knowledge graph (nodes, edges,
│                             #   layers, guided tour) + viewer.html — see §7 above
├── view-knowledge-graph.sh   # Serves and opens the knowledge graph viewer
└── foundry.toml              # solc 0.8.30, via_ir

🧭 EthOnline 2026 — Continuation Track

Everything above this section is the pre-existing protocol. This section is what was built during EthOnline 2026 (September 5–13, 2026) on branch EthOnline2026_continuation_track, cut from main at 5b4cc63. Plans and the honest scope decisions behind them are in the plans (feature overview · SpreadVault / MarginVault · The Graph subgraph · Arc). The task-by-task status page is Help → Continuation Track in the app.

Feature · reason · integration

Feature Reason it exists Integration
SpreadVault — credit spreads escrow their true max loss (0.0625 WETH not 1; 200 USDC not 3,200) Smile's core user sells spreads; margining each leg as naked wasted 16× the capital. Netting is a collateral-accounting problem, not a liquidation problem, so it was the safest efficiency win 1inch · Aqua App
MarginVault + Backstop — opt-in margined puts (IM 1,500 not 3,000), margin calls, takeover auction, backstop pool, insurance, haircut-as-last-resort Rung 3 of the ladder: yield writers want to post a fraction of the strike. Aqua's JIT pull applied to margin itself — collateral stays in the wallet until a real margin call — which no other margin system does 1inch · Aqua App
RfqVault — LP-signed EIP-712 quotes over the formula floor, same Aqua pull Tradfi's NBBO + price improvement: sophisticated makers bring their own models and win flow with tighter quotes while the formula tier stays the public fallback 1inch · Aqua App
The Graph subgraph smile-sepolia + smile-arc-testnet, the copilot's tape The LP dashboard and the copilot were brute-force-scanning logs and went blind past 50 ranges (L12a). The subgraph is now the only position source on public networks — no RPC path — and the copilot trades off it: opportunities vs Deribit, liquidity map, portfolio greeks, hedging, LP/RFQ preparation The Graph · AI tooling / agents on live chain data
Trader skills, Skills menu, MCP servers — eight skill files, user-added skills, The Graph Subgraph MCP preset, a subgraph skill file The tooling half: the copilot's know-how is packaged as skills any AI environment can read, and it can query any indexed subgraph through The Graph's MCP The Graph · AI tooling
Option premium + IV over time on the price chart, 100-trade Anvil tape An options venue has no public tape; the subgraph is Smile's — and a chart needs trades to draw The Graph (data) · UI
Arc testnet deployment — every vault on Circle's native USDC, real fills An options venue whose premium, collateral, margin, backstop and gas are all the chain's native dollar is the cleanest stablecoin-native DeFi story Circle · Arc
Sepolia redeploy on Circle USDC, canonical WETH, Chainlink ETH/USD The pre-event Sepolia contracts were a stale v1; the subgraph needed the current stack with real feeds The Graph (prerequisite) · Circle USDC
Overview, Risk Monitor, TradingView chart, OptionStrat-grade builder, User Guide A first-time visitor gives it three minutes; the numbers that matter (16×, 1,500 vs 3,000, holders whole after a crash) had to be on screen, live, not in a README UI/UX for all three
OpenRouter copilot provider, copilot help page Free-model access without an API key; the AI surface was undocumented The Graph · AI tooling (supporting)
Chainlink feed + CRE, Pyth adapter, Uniswap v4 hook, SwapVM opcode (pre-existing, blue in the map) The oracle, settlement, vol surface and pricing engine every new vault reuses infrastructure

Integration & feature map

Blue is what Smile already was on September 5; green is what the event added. Integrations and infrastructure alike.

flowchart TB
  classDef old fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
  classDef new fill:#dcfce7,stroke:#16a34a,color:#14532d

  subgraph Infra["Infrastructure & integrations"]
    Aqua["1inch Aqua<br/>JIT-pull liquidity registry"]:::old
    SwapVM["1inch SwapVM<br/>custom opcode 33: OptionPremium"]:::old
    Uni["Uniswap v4 hook<br/>demand-driven vol surface"]:::old
    CL["Chainlink ETH/USD<br/>spot + permissionless settlement"]:::old
    CRE["Chainlink CRE<br/>scheduled settlement keeper"]:::old
    Pyth["Pyth pull oracle<br/>PythSpotAdapter (R5, opt-in)"]:::old
    USDC["Circle USDC<br/>premium · fee · put collateral"]:::old
    Graph["The Graph<br/>Studio subgraph smile-sepolia"]:::new
    Arc["Circle Arc testnet<br/>USDC as the gas + quote token"]:::new
    Sepolia["Sepolia redeploy<br/>full stack, real feeds"]:::new
    OR["OpenRouter<br/>4th copilot provider"]:::new
    TV["TradingView Lightweight Charts<br/>open-source engine, Apache-2.0"]:::new
    MD["Coinbase / Kraken public candles<br/>market context for the chart"]:::new
  end

  subgraph Protocol["Smile contracts"]
    Vault["AquaCollateralVault<br/>single-leg calls & puts"]:::old
    Settle["AquaOptionSettlement<br/>round-verified expiry price"]:::old
    Lens["SmileQuoteLens + FirmEscrow<br/>best quote, firm depth"]:::old
    Spread["SpreadVault (S12)<br/>defined-risk netting"]:::new
    Margin["MarginVault + Backstop (S13)<br/>opt-in margined puts"]:::new
    Rfq["RfqVault (R6)<br/>EIP-712 signed quotes"]:::new
  end

  subgraph App["App & tooling"]
    UI["Next.js app<br/>chain · LP · payoff builder"]:::old
    Copilot["AI copilot<br/>reads live chain state"]:::old
    Tabs["Overview · Spreads · Margin · RFQ · Risk Monitor tabs"]:::new
    Chart["Price chart + OptionStrat-grade builder"]:::new
    Keepers["Lifecycle scripts + margin keeper"]:::new
  end

  Aqua --> Vault & Spread & Margin & Rfq
  SwapVM --> Vault
  Uni --> SwapVM
  CL --> Vault & Settle & Margin
  Pyth -.-> Vault
  CRE --> Settle
  USDC --> Vault & Spread & Margin & Rfq
  Arc --> Vault & Spread
  Sepolia --> Vault & Spread & Margin
  Graph --> Copilot & UI
  OR --> Copilot
  Vault --> Lens & UI
  Spread & Margin & Rfq --> Tabs
  Margin --> Keepers
  TV & MD --> Chart
  Chart --> UI

What the event added

Piece What it is Feature Where
SpreadVault — S12 defined-risk netting Rung 2 of the V2 ladder above, now implemented: a sibling AquaApp where a credit spread escrows only its true max loss — 0.0625 WETH instead of 1 WETH for a 3000/3200 call credit spread (16×), 200 USDC instead of 3,200 for the put-credit twin. Same JIT model (collateral stays in the writer's wallet until a buyer matches), same settlement contract, AquaCollateralVault untouched. Deployed without WETH (Arc, a chain with no ether) the vault is cash-settled: a call credit escrows K₂−K₁ USDC per unit and settles in USDC. 1inch · Aqua App src/periphery/SpreadVault.sol, SmilePremiumLib.sol, SpreadToken.sol · test/SpreadVault.t.sol, test/SpreadSettlement.t.sol · the Spreads · Defined Risk tab · script/SpreadDemo.s.sol, script/spread-lifecycle.sh
MarginVault — S13 opt-in margin Rung 3 of the ladder: a second sibling AquaApp where a put writer locks initial margin — 1,500 USDC for an ATM 3000 put, not 3,000 — off the lowest Chainlink answer of the last hour (never the vol hook). Behind the holder, in order: the writer's margin and free balance, an opt-in Aqua credit line, a 30-min writer-takeover auction, a share-based backstop pool (naked notional capped at 7× it), the insurance fund, and only then a loud haircut. Two-step settlement; a gap-40 solvency test; AquaCollateralVault still untouched. 1inch · Aqua App src/periphery/MarginVault.sol, MarginBackstop.sol · test/Margin*.t.sol (54 tests) · the Margin · Opt-in Puts tab · script/margin-lifecycle.sh · keeper/margin.mjs
RfqVault — R6 hybrid RFQ The "NBBO + price improvement" tier from the limitations doc: an LP ships a range to a third sibling AquaApp, then signs EIP-712 quotes off-chain (no gas) — (authId, strike, maxAmount, premiumPerUnit, ttl, nonce) — and a taker fills one; the vault recovers the signer and pulls the collateral JIT through Aqua exactly as tier 1. formulaQuote() shows the tier-1 Ask the quote is beating. Single-use nonces, cancellable; no close() (sellbacks stay on tier 1). 1inch · Aqua App src/periphery/RfqVault.sol · test/RfqVault.t.sol (8) · the RFQ · Signed Quotes tab · script/rfq-lifecycle.sh
The Graph subgraph + trading copilot Authorization, Fill, Instrument (open interest, last trade) and Position (holder balance) entities, live on Studio for Sepolia and Arc. On public networks the app and the copilot read only The Graph — the capped brute-force scan (MAX_AUTHS = 50, L12a) is gone. The copilot trades off the tape: find_opportunities (Smile IV vs the nearest Deribit instrument and vs the last fill), liquidity_map (capacity, used %, open interest, stale/scarce/empty flags, per-strike heat map), portfolio_greeks (long side + written side), hedge_suggestion, reference_market, macro_calendar, and prepare_lp_range / prepare_rfq_quote cards that prefill the forms — the user signs. Eight trader skills (SKILL.md), a Skills menu with user-added skills, MCP servers with a Subgraph MCP preset, a subgraph skill file for AI environments. The price chart draws premium and IV per instrument from the tape; ./local.sh seeds 100 trades. The Graph · AI tooling / agent on live chain data subgraph/ · Studio: smile-sepolia, smile-arc-testnet · frontend/lib/tape.ts, lib/subgraph.ts, lib/copilot/graphTools.ts, frontend/skills/, lib/copilot/mcp.ts · components/PriceChart.tsx · script/seed-tape.sh · Copilot
Arc testnet deployment The whole stack — main vault, SpreadVault, MarginVault + backstop, RfqVault — on Circle's Arc, with Circle's real Arc USDC as premium, fee, put collateral, margin, backstop pool, insurance fund — and gas. Real fills on every vault, recorded: a USDC-margined put locking 1.50 instead of 3.00, an RFQ quote filled inside the formula. Arc the Arc deployment notes · script/arc-smoke.sh, script/arc-siblings-smoke.sh · .env.arc.example · Arc in the app's network picker
App: Overview, Risk Monitor, builder A default Overview tab — the capital-efficiency ladder as live bars from the connected chain, live counters across all vaults, the recorded testnet receipts; one build serves Anvil / Sepolia / Arc (addresses follow the connected chain); a Risk Monitor with per-position health bars and the liquidation timeline rebuilt from MarginVault events (+ "explain with the copilot"); the strategy builder gains today/halfway/expiry curves, a price × date P&L heat map, breakevens, and a per-leg "what the writer locks on each vault" panel; a TradingView Lightweight Charts price chart with the strategy overlaid; tabs in user language. — (UI/UX for all three) frontend/components/Story.tsx, RiskMonitor.tsx, PayoffBuilder.tsx, PriceChart.tsx, lib/deployments.ts, config/wagmi.ts
Copilot & docs OpenRouter as a fourth copilot provider; the copilot documented as a help page; a User Guide (User Guide, in the help sidebar and in the copilot's knowledge) so the copilot walks people through buying, building strategies and providing liquidity step by step; reference-table rows cite the code that implements each solution; the LP Dashboard bug that started the whole indexer thread, fixed. The Graph · AI tooling frontend/lib/copilot/provider.ts · Copilot, User Guide, Reference Table
flowchart LR
  subgraph Chain["EVM chain — Anvil · Sepolia · Arc testnet"]
    Aqua["1inch Aqua registry<br/>(official, self-deployed)"]
    Vault["AquaCollateralVault<br/>single-leg options · unchanged"]
    Spread["SpreadVault (new)<br/>S12 netted spreads"]
    S1["AquaOptionSettlement"]
    S2["AquaOptionSettlement<br/>(the spread's own)"]
    Aqua --- Vault
    Aqua --- Spread
    Vault --- S1
    Spread --- S2
  end
  App["Next.js app<br/>Spreads tab · LP Dashboard · Copilot"]
  Graph[("The Graph subgraph (new)<br/>Authorization · Fill")]
  App -- "RPC / getLogs (fallback)" --> Chain
  App -- "NEXT_PUBLIC_SUBGRAPH_URL" --> Graph
  Graph -- "events + bound authorizations() calls" --> Vault

SpreadVault in one table

A taker buys the structure; the writer's escrow is the S12 true max loss, pulled JIT through the SpreadVault's own Aqua strategy, and settlement is one price through one formula per structure:

Call credit (short K₁, long K₂) Put credit (short K₂, long K₁)
Escrow per unit (K₂−K₁)/K₂ WETH — 0.0625 for 3000/3200 K₂−K₁ USDC — 200 for 3000/3200
vs. the main vault (naked short leg) 1 WETH 3,200 USDC
Taker pays Ask(K₁ call) − Bid(K₂ call), floored at 1 USDC, + fee Ask(K₂ put) − Bid(K₁ put), same
Payout at settlement price S units·(clamp(S,K₁,K₂)−K₁)/S WETH units·(K₂−clamp(S,K₁,K₂))/1e30 USDC
Max payout over S exactly the escrow (at S = K₂) exactly the escrow (at S ≤ K₁)

Pricing is not a new model: SmilePremiumLib is the vault's own put-side premium math lifted into a library with an isCall flag, and test_quote_putLegMatchesVaultPutQuote proves it reproduces vault.putQuote to the wei with the staleness spread and fee gross-up live. test/SpreadSettlement.t.sol fuzzes the settlement price across OTM, between the strikes, and far ITM: holder payout + writer reclaim == escrow to the wei in either order, and the escrow cap never binds.

sequenceDiagram
    participant W as Writer (LP)
    participant SV as SpreadVault
    participant AQ as Aqua
    participant T as Taker
    participant ST as AquaOptionSettlement
    W->>SV: openStructure(CallCredit, [K1,K2], expiry, escrowCapacity)
    W->>AQ: ship(app=SpreadVault, strategy, [WETH], [escrowCapacity])
    Note over W,AQ: WETH stays in the writer's wallet
    T->>SV: buy(authId, units, maxPremium)
    SV->>SV: quote — Ask(long leg) − Bid(short leg), fee, escrow
    SV->>AQ: pull(writer, hash, WETH, escrow) — the S12 max loss, not a full leg
    SV->>T: mint SpreadToken
    Note over ST: expiry — anyone supplies the first Chainlink round after it
    T->>SV: redeem(authId, units) → net intrinsic, capped by escrow
    W->>SV: reclaim(authId) → escrow − what outstanding holders are owed

The app, as a visitor sees it

./local.sh opens on Overview: which chain you are on and what is real there, the ladder — naked put $3,000 → credit spread $200 → margined put $1,500 (read live from MarginVault's mark) → signed quote — with a jump button per rung, live counters, and on Sepolia / Arc the real receipts. Trade opens on a TradingView-engine ETH/USD chart with the strategy you are building drawn on it (strikes, breakevens, the protocol's spot), then the option chain and the strategy builder (three P&L curves, a price × date heat map, breakevens, greeks, and what a writer locks per sell leg on each vault). Earn writes ranges (one-click or by hand); Spreads, Margin and RFQ are the three new vaults; Risk Monitor shows every margined position's health and the liquidation timeline as it happens; My Positions reads The Graph where it exists; Receipts lists every deployment. The copilot (bottom-right) has read the User Guide and can drive any of it.

Running the new pieces

./local.sh                          # Anvil + all contracts incl. SpreadVault + the app (Spreads tab)
./script/spread-lifecycle.sh        # open → ship → buy → expiry → settle → redeem → reclaim, conservation-checked
./script/margin-lifecycle.sh        # margined put: fill (IM only) → crash → flag → auction → backstop absorbs → settle → redeem
MODE=takeover ./script/margin-lifecycle.sh   # …or a second writer takes the position over at auction
cd keeper && npm install && MARGIN_VAULT=… MARGIN_SETTLEMENT=… ORACLE=… PRIVATE_KEY=… npm run margin   # permissionless keeper
./script/rfq-lifecycle.sh           # LP signs an EIP-712 quote 1% inside the formula (no gas) → taker fills → JIT pull → replay rejected

cd subgraph && pnpm install && pnpm codegen && pnpm build   # The Graph subgraph (see the subgraph notes)

cp .env.sepolia.example frontend/.env.local                  # point the app (+ subgraph URL) at the Sepolia deployment
cp .env.arc.example frontend/.env.local                      # point the app at the Arc testnet deployment
PRIVATE_KEY=0x… ./script/arc-smoke.sh                       # real-USDC fills on Arc, as plain cast sends

Honest status of each track

  • SpreadVault: A1–A4 shipped and demoed on Anvil and on Arc, for call and put credit spreads. The iron condor is now a real single structure on a cash-settled vault — escrow = max(putWidth, callWidth) USDC (the wider wing, not the sum), one fill, one settlement (test/SpreadCondor.t.sol); it lands live at the next Arc SpreadVault redeploy. The optional SpreadPremiumInstruction SwapVM opcode for the call-credit leg was not attempted.
  • MarginVault: B1–B8 shipped — puts only, USDC only, whole-position takeover only; per-range maxBlockNotional not ported (the global backstop-coupled ceiling bounds exposure instead); no close() by design (a sigma-priced buyback paid from margin is L7's attack). Full lifecycle on Anvil via script/margin-lifecycle.sh and the keeper; deployed to Sepolia and Arc, with a real-USDC margined fill on Arc (1.50 locked instead of 3.00). L13 is the honest list of what it does not promise.
  • RfqVault: built as a sibling vault rather than a SwapVM opcode — a signed quote changes the price, never the custody model, and nonces need state an instruction doesn't have. On Arc testnet with a real-USDC signed fill (not on Sepolia); no close() by design.
  • Subgraph: live on Graph Studio as smile-sepolia v0.0.2 (https://api.studio.thegraph.com/query/44448/smile-sepolia/v0.0.2), indexing the Sepolia deployment above — Authorization #0 was queryable within a minute of Aqua.ship, and the Fill for a real 0.01-unit $2,500 call one block after the buy. The local graph-node compose is x86-64-only (no arm64 image; emulation crashes). The two stretch items landed in a different shape: holder positions come from a Position entity fed by the vault's own events (no per-token data-source template, so wallet-to-wallet OptionToken transfers are not indexed), and the Subgraph MCP is documented (.mcp.json.example, The Graph page) with a preset in the app copilot.
  • Arc: every vault deployed with real USDC and traded (main, Spread, Margin + backstop, RFQ). FX options (USDC/EURC) were cut after the oracle check found no EUR/USD feed on Arc testnet at all (Stork's pull contract has none and its ETH/USD is stale); Circle Gateway and a Circle developer-controlled wallet fund the insurance fund and backstop pool on Arc (keeper/insurance-gateway.mjs, keeper/backstop-wallet.mjs, real runs recorded in the Arc deployment notes). Arc mainnet launches Sept 16; the mainnet deployment is a follow-up.

Technical Stack

  • Smart Contracts: Solidity 0.8.30 (Foundry, via_ir), on the official 1inch Aqua + SwapVM contracts (vendored, unmodified)
  • Frontend: Next.js 16, Tailwind CSS, Wagmi/Viem, recharts; strategy engine built on the MIT black-scholes + greeks packages; price chart on TradingView Lightweight Charts (TradingView's open-source engine, Apache-2.0) fed by Coinbase's public ETH-USD candles (Kraken fallback) — market context only, the protocol prices off its oracle
  • Indexing & AI: The Graph (Studio subgraph smile-sepolia); copilot via the Vercel AI SDK with Anthropic / OpenAI / Google / OpenRouter as providers
  • Oracle/Settlement: Chainlink price feeds (permissionless round-verified settlement) + Chainlink CRE SDK (scheduled keeper)
  • DEX Infrastructure: Uniswap v4 Hooks, Uniswap Trading API

User Guide — trading and providing liquidity on Smile

This is the hands-on guide: what each tab does, how to build a trade, how to earn as a liquidity provider, and what you are risking on each rung. The Overview explains why the protocol is built this way; this page explains how to use it. The AI copilot (bottom-right) has read this guide and can walk you through any step or explain a position you already hold.

The one idea to understand first

On Smile a writer (liquidity provider, LP) never deposits collateral up front. They authorize a range of strikes and expiries and ship that allowance to 1inch Aqua; the collateral stays in their wallet. When a taker buys an option, the exact collateral for that fill is pulled just-in-time (JIT) through Aqua into the vault, and an ERC-20 OptionToken is minted to the taker. Premiums are priced by the protocol's own vol surface (a SwapVM opcode; a Uniswap v4 hook nudges the surface with demand). At expiry, a Chainlink round fixes the settlement price permissionlessly; the holder redeems intrinsic value from the escrowed collateral, and the writer reclaims the rest.

Everything below is a variation on where that collateral comes from and how much of it is needed.

Networks

Network What is real How to use it
Anvil (local) mock USDC / WETH, a settable oracle ./local.sh; MetaMask on chain 31337; time can be warped, so expiry, settlement and liquidation demos run in minutes
Sepolia Circle USDC, canonical WETH, Chainlink ETH/USD; The Graph subgraph pick Sepolia in the network menu — the app carries the deployed addresses
Arc Testnet Circle's native USDC for premium, collateral, margin and gas pick Arc Testnet (MetaMask adds chain 5042002)

The Overview tab shows which chain you are on and what is real there. The Receipts tab lists every deployed contract and recorded demo transaction with explorer links.

Buying an option (Trade tab)

The tab opens on an ETH/USD candlestick chart (TradingView's open-source Lightweight Charts engine, fed by Coinbase's public hourly candles with Kraken as a fallback). The protocol's own spot is the dotted blue line; as you build a strategy below, each leg's strike appears on the chart — green for long, red for short — with breakevens dashed in yellow. The market data is context; every price the protocol charges comes from its oracle and surface.

  1. The option chain shows strikes around spot with the live Ask (buy) and Bid (sell-back) for calls and puts, quoted from the surface.
  2. Pick a strike and size; approve USDC for the premium once.
  3. Buy: the premium goes to the writer, the fee to the protocol, the collateral is pulled from the writer's wallet at that moment, and you receive the OptionToken.
  4. Close early: sell the option back at the live Bid — the vault pays you from the writer's escrow and premium; there is always a formula price to exit at.
  5. At expiry: anyone can settle the series with the first Chainlink round after expiry; then Redeem pays intrinsic value (calls in WETH as (S−K)/S, puts in USDC as K−S).

The Strategy Builder under the chain lets you compose multi-leg strategies (20 presets by outlook, or custom legs) and shows the P&L curve today / halfway / at expiry, a price × date P&L heat map, breakevens, probability of profit, net greeks, and — Smile-specific — what a writer would have to lock for each sell leg on each vault.

Earning as a liquidity provider

One-Click (Earn · One-Click)

The simplest way in: choose covered calls (hold WETH) or cash-secured puts (hold USDC), a delta band and a tenor; the app authorizes a range at those deltas and ships it to Aqua. keeper/roll.mjs can roll it every expiry with your own key.

Write a Range (Earn · Write a Range)

The full form: strike range, expiry, capacity, optional per-block cap and your own vol multiplier (your quote versus the surface — competing ranges are the vol discovery). Three steps: approve Aqua for the collateral, authorizeRange, Aqua.ship. Nothing leaves your wallet until a fill. Your positions and fills are on My Positions (read from The Graph on Sepolia, from RPC elsewhere).

Spreads (SpreadVault)

Write a credit spread — call credit (short K1, long K2) or put credit (short K2, long K1) — and only the structure's true maximum loss is escrowed: (K2−K1)/K2 WETH per unit for calls, K2−K1 USDC for puts. For a 3000/3200 call credit that is 0.0625 WETH instead of 1 WETH. The taker buys the whole structure as one SpreadToken and pays Ask(long leg) − Bid(short leg). Settlement is one price through one formula; the payout can never exceed the escrow.

Margin (MarginVault, opt-in)

Write puts posting initial margin instead of the strike: intrinsic plus a 50% buffer of the worst-of-hour Chainlink mark, capped at the strike — 1,500 USDC for an at-the-money 3,000 put. This is the one tier where "the option always pays" can break, so read the waterfall:

  • Fall below the 30% maintenance floor and anyone may flag you. The vault first sweeps your free balance and, if you opted in, pulls a top-up from your Aqua allowance; only if that is not enough are you flagged. You have 1 hour to top up to initial margin.
  • Then a 30-minute takeover auction: another writer can take your position with a rising bonus; what travels is min(locked, MM + bonus + penalty). The holder's option is untouched.
  • Unsold, the backstop pool (USDC deposited by anyone, 24 h withdrawal delay) adopts the position.
  • At expiry each writer settles through locked → free → bad debt; the series then draws the backstop, then the insurance fund, and only then do holders take a haircut — announced by an event, after which the initial-margin buffer ratchets up.

The Risk Monitor tab shows every position's health bar, the mark, the backstop and insurance, and a live timeline of flags, auctions, absorbs, settlements and haircuts. "Explain with the copilot" turns the timeline into a paragraph.

Deposits and withdrawals of free balance go through deposit / withdraw; a withdrawal is refused while you are flagged, in debt, or if it would take any position below initial margin.

RFQ (RfqVault)

Ship a range as above, then sign quotes in your wallet — no gas — for a strike, size cap, price, and time-to-live. Price them off any model you like; the tab shows the formula Ask next to your quote. A taker fills the quote and the collateral is pulled exactly as on the other rungs. Quotes are single-use (nonce) and cancellable; there is no sell-back on this vault — holders exit through the formula tier.

Risks, plainly

  • Main vault, spreads: fully collateralized at the true maximum loss; the risk is the option's own P&L and the oracle at settlement.
  • Margin: liquidation risk for writers, and a bounded bad-debt risk for holders after the backstop and insurance are exhausted (L13). Naked notional is capped at 7× the backstop.
  • Everywhere: premiums come from a model with a demand-driven sigma; displayed depth is only as firm as the writer's wallet — the firm-escrow tier and the S1 firmness checks exist for that (Limitations).

Asking the copilot

Try: "walk me through selling a 30-day cash-secured put", "what does my range look like right now?", "build me an iron condor around spot" (it loads the builder), "what happens to my margined put if ETH drops 20%?", "explain the last liquidation" (from the Risk Monitor), "how do I sign an RFQ quote?". It reads the connected chain, The Graph (on Sepolia and Arc), Deribit for reference vol, and these docs.

Trading with the tape

On Sepolia and Arc every range, fill and position is indexed by The Graph (subgraph/), and the copilot's trading tools read that tape — with no cap on how many ranges exist. Ask it:

  • "What's cheap right now?" — it screens every live strike, compares Smile's implied vol with the nearest listed Deribit instrument and with the last fill, and ranks the edge; then it prices the trade and proposes it as a card you can load into the builder.
  • "Where is liquidity thin?" / "where should I write a range?" — the liquidity map: every range's capacity, how full it is, open interest, when it last traded, and the strikes near spot nobody quotes. The card it proposes opens Earn · Write a Range with the band, expiry and size filled in; you review and sign.
  • "What are my greeks?" / "hedge my short puts with short calls" — the whole book (long positions and the ranges you wrote) as net delta, gamma, theta and vega, then the exact quantity of spot or options that flattens the delta.
  • "Quote the 3,000 call for me on RFQ" — recent fills and reference IV for that instrument, a premium inside the formula ask, and a card that opens the RFQ desk with the quote ready to sign (EIP-712, in your wallet — the copilot never holds a key).
  • "Anything on the calendar this week?" — FOMC, CPI and listed expiries with the usual vol behaviour around each.

The Skills button in the copilot lists what it knows how to do (opportunities, risk management, delta hedging, margin, market making, RFQ quoting, macro context, calendar spreads) with a starter prompt each, and lets you add your own skill as a markdown note. The gear lets you add MCP servers, including The Graph's Subgraph MCP, so the copilot can query any indexed subgraph in natural language.

On the local Anvil chain there is no indexer; the app rebuilds the same tape from the vault's events, and ./local.sh seeds 100 trades so the chart and the screens have something to show. The Trade tab's price chart draws the traded premium and implied vol of any instrument next to the ETH candles.

Screens: what every number means

This page walks through the Smile application one tab at a time and explains every figure, label, badge, bar and button on each tab: what it shows, and where the value comes from (which contract read, which formula, which data source). It is written for a reader who knows what an option is but has not read the code. The Help link in the app opens this page at the section for the tab that is currently on screen.

Terms are defined where they first appear. A few recur everywhere:

  • Ask is the price a buyer pays to open a position; Bid is the price a holder receives to sell it back. One on-chain strategy quotes both sides; the gap between them is the spread.
  • Spot is the current ETH/USD price the app is working from. Where it comes from is explained under the header.
  • WAD means an 18-decimal fixed-point integer, the on-chain representation of prices and unit counts. USDC amounts are 6-decimal integers. Every display in the app divides by the right power of ten; this page states the units as they are shown.
  • Anvil is the local development chain started by ./local.sh; Sepolia and Arc Testnet are the two public testnets the app is deployed on. Where a number behaves differently per chain, the section says so.

The mathematics behind the quotes is specified in the Overview's Mathematical Specification and is not repeated here; the User Guide is the step-by-step walkthrough of the same screens.

Header strip (every tab)

The header is the same on every tab: the wallet controls on the top row and the spot price bar beneath it.

Element What it shows Where it comes from
Help ↗ Opens this documentation site in a new tab, at the section for the current tab. app/page.tsx, static link to /help.html.
Network dropdown Before a wallet is connected the button reads "Network"; once connected it shows the connected chain's name (for example "Sepolia", "Arc Testnet") or "Chain N" for an unknown id, and "Switching…" while a switch is pending. The list marks the active chain with "✓ active". useChainId() and useChains() from wagmi; switching goes through useSwitchChain on the connected connector, or a raw EIP-3326 request when no wallet is connected.
Connect Wallet Opens the wallet picker when more than one connector is available, or connects directly when there is exactly one. Reads "Connecting…" while pending. Any connector error appears in red beneath it. useConnect(); connectors are the injected wallet (MetaMask etc.) and WalletConnect.
Open in MetaMask Shown only when no injected wallet exists (a phone browser). A universal link that opens the page inside MetaMask's own browser. https://metamask.app.link/dapp/<host><path>.
Balance, e.g. 0.1436 ETH The connected wallet's native-token balance on the connected chain, four decimals. On Arc the native token is USDC, so this reads in USDC. useBalance({ address }).
Address, e.g. 0x2816…6bf1 The first six and last four characters of the connected address. useAccount().
Disconnect Disconnects the wallet. useDisconnect().
Large price, e.g. $2,536 The spot price the app uses for every quote, chart and ladder on every tab. Shown as "$—" while loading. hooks/useUniswapSpot.ts, see the three sources below.
ETH/USD · 30d expiry Static caption: the underlying pair and the default tenor the option chain opens on. Literal text.
Source badge: ● Uniswap API (pink), ● Chainlink (blue), ● mock feed (amber) or ● static (grey) Which source produced the spot. useUniswapSpot(chainId). On a chain whose vaults price from a settable mock (Arc testnet, Anvil) it reads that mock first (latestRoundData on the deployed MockV3Aggregator), so the spot shown is the spot the contracts use; on Arc that mock is refreshed from Sepolia's Chainlink every 30 minutes by a keeper. Otherwise, in order: (1) the Uniswap Trading API EXACT_INPUT quote for 1 WETH → USDC on mainnet, only when NEXT_PUBLIC_UNISWAP_API_KEY is set; (2) the Chainlink ETH/USD feed on Sepolia (0x694A…5306, 8 decimals); (3) a static 3420. Refreshes every 60 seconds and on chain switch.

Notes. The spot shown here is a display and quoting convenience for the browser. Contracts price against their own oracle: the Chainlink feed on Sepolia, a settable mock aggregator on Anvil and Arc (fixed at $3,000 on Arc unless someone posts a new round). Rounded to whole dollars.

Overview

The landing tab. It states the thesis, names the chain you are on, shows the capital-efficiency ladder as live numbers from the connected chain, the protocol's live counters across every vault, and the recorded testnet receipts.

Element What it shows Where it comes from
You are on The connected chain's name ("Sepolia", "Arc Testnet", "Anvil (local)" or "chain N"). DEPLOYMENTS[chainId].name in lib/deployments.ts; the Anvil fallback for chain ids 31337 and 1337.
Line under the chain name What is real money on this chain: on Sepolia "Circle USDC · canonical WETH · Chainlink ETH/USD"; on Arc "Circle's native USDC — premium, collateral, margin, backstop, and gas"; on Anvil "mock USDC / WETH, settable oracle — the full lifecycle runs here in minutes". DEPLOYMENTS[chainId].realMoney, or the literal Anvil string.
● indexed by The Graph — no range cap, the copilot trades off it Present when the chain has a subgraph (Sepolia, Arc). DEPLOYMENTS[chainId].subgraph is set.
connect a wallet to trade; reading works without one Shown while no wallet is connected. useAccount().isConnected.
What a writer locks for one $K put The ladder's reference strike K is the spot rounded to the nearest $50 (the "at-the-money" strike). k = round(spot / 50) × 50.
live from the connected chain · spot $S · margin mark $M The spot in use and, when MarginVault is deployed, its margin mark (the lowest Chainlink answer of the last hour, explained under Margin). MarginVault.markSpot(); the mark is omitted when the read returns zero.
Rung Naked put$K USDC locked per unit What the main vault locks per put unit: the full strike, cash-secured. Full-width bar (this is the 100% reference). k.
Rung Credit spread$(K₂−K) USDC — the true max loss, N× less What SpreadVault locks for a put credit spread with the long strike K₂ = K + 200: the strike gap in USDC. N = K ÷ (K₂ − K), rounded to a whole number. Formula from SpreadVault's escrow rule, computed in the browser; K₂ is K + 200 by construction of the ladder.
Rung Margined put$IM USDC initial margin — N× less, liquidation-backed What MarginVault would lock as initial margin for one put at K, off its live mark. N = K ÷ IM to one decimal. MarginVault.marginRequirement(K, 1 unit, markSpot, initial = true), refreshed every 15 seconds; if the read is unavailable the browser estimate min(K, max(K − spot, 0) + 0.5 × spot) is used.
Rung Signed quoteany price the LP signs — the custody model never changes The RFQ tier: the price changes, the collateral rule does not. Bar width equals the naked rung. Literal.
new badge Marks the three rungs built at EthOnline 2026. Literal.
Trade →, Spreads →, Margin →, RFQ → Jump to the tab that implements that rung. Tab switch.
Ranges shipped — value and vault a · spread b · margin c · rfq d The total number of ranges ever authorised across the four vaults, with the per-vault breakdown. Sum of nextAuthId() on AquaCollateralVault, SpreadVault, MarginVault, RfqVault; refreshed every 15 seconds. nextAuthId counts authorisations, so revoked and expired ranges are included.
Backstop poolN USDC Total USDC held by the backstop pool that absorbs margined positions nobody buys at auction. MarginBackstop.totalAssets().
Naked notional / ceilingA USDC / B USDC A is the notional of margined puts currently open; B is the most the vault will allow, "ceiling = 7 × backstop" (the lower of the owner's cap and seven times the pool). MarginVault.nakedNotional(), MarginVault.effectiveCeiling().
Insurance fundN USDC The insurance fund drawn after the backstop and before any haircut; fed by "50% of margin fees + penalties". MarginVault.insuranceFund().
Real transactions on The recorded demo receipts for this chain: hash (link to the explorer), label, note. Below them the deployed contract addresses and the subgraph URL. On Anvil this block is replaced by a note pointing at the lifecycle scripts. DEPLOYMENTS[chainId].demo, .contracts, .subgraph; links via lib/explorer.ts. The two "buy" receipts are the deployer buying from its own range (limitations L15); the three "Treasury ·" receipts are the Circle App Kits runs.

Units and rounding. Ladder amounts are whole dollars; counters are whole USDC. Bar widths are value ÷ k as a percentage with a 2% floor so a rung is never invisible.

Trade

The buyer's tab: the price chart with the strategy drawn on it, the option chain (the matrix of quotes by strike), and the strategy payoff builder.

Price chart

Element What it shows Where it comes from
ETH/USD · Coinbase ETH-USD · 1h · last N Hourly candles and the last close. api.exchange.coinbase.com public candles, granularity=3600; on failure api.kraken.com OHLC at 60-minute interval and the caption reads "Kraken ETH/USD · 1h". Market context only; the protocol prices off its oracle.
Dotted blue line Smile spot $S The app's spot (the header figure), as a horizontal price line. The spot prop.
Solid green / red lines long call 1×, short put 2× One line per leg in the payoff builder, at the leg's strike; green for a bought leg, red for a written leg, with the quantity. The builder's legs.
Dashed yellow breakeven lines The underlying prices at which the strategy's expiry P&L crosses zero. breakevens(pnlSeries(legs, spot)) in lib/options.ts.
nearest expiry in Nd The smallest days-to-expiry among the legs. Leg expiryDays, default 30.
Tape selector, e.g. C 2500 · 8 Oct (12) The instruments that have traded on the main vault, most fills first; the label is C/P, strike and expiry date, with the fill count in parentheses. lib/tape.ts readTape: the subgraph on Sepolia and Arc, the vault's event log on Anvil. Re-polled every minute.
· The Graph or · Anvil event log Which source the tape came from. tape.source.
Purple line premium / unit $P The premium paid per unit at each fill of the selected instrument, on the left price scale. TapeFill.premiumPerUnit.
Pink line implied vol N% The Black-Scholes volatility that reproduces each fill's premium, given the candle close at that hour, the strike and the time to expiry. Absent for a fill whose premium is below intrinsic value (no volatility explains it). impliedVol() in PriceChart.tsx: bisection on sigma in [1%, 500%], r = 0; spot at the fill is the last candle at or before the fill, or the prop spot when no candle covers it.
Tape · no trades yet No fills on this chain. Empty tape.

Option chain

Element What it shows Where it comes from
Expiry pills 1d 7d 30d 90d The tenor the matrix quotes. When an LP range is active the pills are locked and the pill nearest the range's expiry is highlighted, with Nd (LP auth) showing the exact days left. selectedDays, or activeAuth.expiry.
Bid↓ to sell · Ask↑ to buy Reminder of which column does what. Literal.
Range strip: Covered Calls / Cash-Secured Puts, $K₁ – $K₂ or single strike $K The active authorisation (the market-wide latest active range on the main vault): side and strike band. AquaCollateralVault.authorizations(latest), polled every 10 seconds in page.tsx.
used / max WETH (or USDC), progress bar, N% used How much of the range's collateral capacity has been consumed by fills. Calls are capped in WETH, puts in USDC. usedCollateral and maxCollateral from the same read, refreshed every 6 seconds.
Nd left (red when 3 or fewer) Days until the range expires. (expiry − now) / 86400, rounded.
firm depth N WETH and ⚠ soft The size a fill can actually clear right now: the minimum of the authorised remainder, the LP wallet's token balance, and the LP's allowance to Aqua. "soft" appears when the wallet backs less than the authorised remainder — the rest of the displayed depth is phantom (limitations L11). hooks/useFirmDepth.ts: ERC-20 balanceOf(lp) and allowance(lp, Aqua) every 10 seconds.
Rows: strikes at −20%, −10%, −5%, 0, +5%, +10%, +20% of spot, rounded to $50 The strike grid. STRIKES_OFFSETS in OptionMatrix.tsx.
IV e.g. 82.4% The implied volatility the smile assigns to that strike: σ = σ_tenor × max(0.1, 1 + α × ln(K/S)² + β × ln(K/S)), with σ_tenor and β read live from the on-chain hook for the selected expiry (α is the range default 2.0). A trailing * means the live read has not landed and the pre-event constants (0.80, 2.0, 0) are showing. useLiveSurface()OptionPricingHook.sigmaFor(timeToExpiry) and beta(); smileSigma() in lib/options.ts. This σ is the surface's parametric level, not a Black-Scholes implied vol: the vault's formula has no 1/√(2π) factor, so the same fill back-solves to a larger BS IV on the price chart (Overview §2 explains the gap).
Δ e.g. 0.53 (calls) / −0.47 (puts) Black-Scholes delta at the live smile volatility: the option's price sensitivity to a $1 move in spot, and roughly the probability of expiring in the money. Put delta is call delta minus one. callDelta() with the Abramowitz–Stegun normal CDF.
Bid↓sell (green) and Ask↑buy (red) e.g. $121.30 Per-unit premium from the same formula the vault charges: intrinsic + spot × σ_strike × √T × min(S,K)/max(S,K) (Overview, Mathematical Specification §2), at the live sigma. Ask rounds up to the cent, Bid rounds down; the on-chain spread adds the staleness-scaled and size terms at fill time, and the 1% protocol fee is grossed up on top of the Ask. Ask cells are dimmed and unclickable unless the strike is inside the active range on the matching side. priceWAD()protocolPremium() in lib/options.ts.
Strike column, highlighted row The at-the-money row is the strike within 1% of spot. isATM.

Clicking an Ask opens the buy panel; clicking a Bid opens the sell (write) panel; a strike where you hold OptionTokens offers a close panel.

Buy panel. Amount (contracts) is the number of units. Call · K $2,500 · 12.34 USDC premium is Ask × amount in USDC (the Ask WAD scaled to 6 decimals). The three steps are: 1. Swap 0.00512 ETH → 12.34 USDC (only when a Uniswap API key is configured; an EXACT_OUTPUT quote from the Trading API for exactly the premium, executed through the Universal Router and shown afterwards as ✓ Swapped via Uniswap (0x…) ↗), 2. Approve USDC (an ERC-20 approval for twice the premium, to the vault), and 3. Buy N Call which calls AquaCollateralVault.buy(authId, strike, amount, maxPremium). The yellow line "Size exceeds the LP's firm depth…" appears when the collateral the fill would pull (amount WETH for calls, amount × strike USDC for puts) exceeds the firm depth, and the buy button is disabled.

Sell panel (writing from the matrix). Call/Put, K_min, K_max, × size, receive ~$N USDC (Bid × size), lock N WETH for calls or lock $N USDC (size × K_max) for puts; expiry 1d 7d 30d; "range write · collateral splits across N strikes" counts $50 steps between the bounds. It runs approve → authorizeRangeAqua.ship, reporting Confirm approval… / Approving… / Registering… / Shipping to Aqua… / ✓ Written.

Close panel. Close position · 0.0100 contracts is your OptionToken balance (18 decimals); "Burns OptionToken · releases LP collateral · decrements σ" describes AquaCollateralVault.close(optionToken, lp, balance, minPayout = 0): the payout is the on-chain Bid, accepted as is.

Strategy Payoff Builder

Element What it shows Where it comes from
Outlook tabs and strategy chips Preset multi-leg strategies grouped by market view; hovering a chip shows its description. STRATEGIES in PayoffBuilder.tsx.
Leg row: side, K, ×, DTE, ~$N Each leg's strike, quantity, days to expiry and the protocol's per-unit premium for it. protocolPremium(spot, K, isCall, DTE/365): intrinsic + spot × σ_smile × √T × min(S,K)/max(S,K), the same formula the on-chain instruction uses.
Payoff chart: solid, purple dashed, pink dotted lines; S marker; $N breakeven labels P&L versus underlying price at the nearest expiry (solid), today (T+0, purple dashed) and halfway to expiry (pink dotted); the vertical marker is spot; yellow labels are breakevens. pnlSeries(): 200 points from 60% to 140% of spot; each leg valued by Black-Scholes at the smile σ with r = 0, collapsing to intrinsic at expiry; P&L = value − entry cost.
Net debit / Net credit $N Entry cost at the protocol's premiums: positive is paid, negative is received. entryCost().
Max profit, Max loss ( when unbounded) Extremes of the expiry P&L over the plotted range; "∞" when the curve is still rising or falling at the edge. strategyStats().
Breakeven $N · $M Zero crossings of the expiry P&L. breakevens().
Prob. profit N% Probability of finishing profitable at the nearest expiry under a lognormal terminal distribution centred on spot with the at-the-money smile σ. probabilityOfProfit().
Δ, Γ, Θ/day $N, Vega $N Net Greeks of the strategy at spot: delta (per $1 of spot), gamma (change in delta per $1), theta (P&L per calendar day), vega (P&L per 1 volatility point). The greeks package at the smile σ per leg.
P&L by price and date heat map A 15 × 8 grid: rows are underlying prices from +30% to −30% of spot, columns are calendar days from today to the nearest expiry; each cell is the strategy P&L, green for gain, red for loss; hovering shows the exact value. pnlMatrix().
What the writer locks on Smile, per unitmain vault, SpreadVault, MarginVault For each written leg: the naked collateral (1 ETH per call, the strike in USDC per put); the netted escrow when a bought leg of the same type caps the loss ((K₂ − K₁)/K₂ ETH for calls, K₂ − K₁ USDC for puts), otherwise "— add a long call above/put below"; the margined amount for puts, min(K, max(K − S, 0) + 0.5 × S), otherwise "puts only". writerCollateral() in lib/options.ts.

Per chain. The chart's candles are public exchange data on every chain. The tape is the subgraph on Sepolia and Arc and the event log on Anvil; on a public chain without a subgraph the tape stays empty rather than falling back to an RPC scan. Quotes in the matrix depend only on the header spot and the smile constants, so they look the same on every chain; the on-chain price at fill time depends on that chain's oracle.

Earn · One-Click

The income writer's front door: pick a side and a risk band, and the app turns a delta target into a strike range, estimates the yield, and runs approve → authorise → ship as one flow.

Element What it shows Where it comes from
Side: Covered calls / Cash-secured puts Which side you write. Calls lock WETH, puts lock USDC. Local state.
Presets Conservative 10–20Δ · far OTM, high win rate, Balanced 20–30Δ · the classic income band, Aggressive 30–40Δ · richer premium, more assignments The delta band. Delta approximates the probability of expiring in the money, so a lower band is further out of the money. PRESETS in IncomeOneClick.tsx.
Tenor 7 days / 30 days Days to expiry. TENORS.
Strike range $K₁–$K₂ The strikes whose Black-Scholes delta
Size input (WETH for calls, USDC for puts) The collateral you commit. Local state; becomes maxCollateral.
Est. premium $N Premium per unit at the mid-strike times the units the collateral backs: units = size for calls, size ÷ mid-strike for puts. protocolPremium(spot, midStrike, isCall, T).
Est. APR N% (estimated premium ÷ collateral value) × (365 ÷ tenor days) × 100, where collateral value is size × spot for calls and size for puts. An annualised rate from one period's premium; not compounded, not net of assignments. Computed in the component.
Expected move by expiry ±N% (~$M) ATM vol × √T, the size of move the premium is charging for, in percent and dollars. surfaceQuotes(spot, T).expectedMovePct.
ATM volatility (N%/yr here) The at-the-money smile σ. With the current constants this is 80%. smileSigma(spot, spot).
Which direction costs more ±N vol pts — the risk reversal σ at the 25-delta call strike minus σ at the 25-delta put strike. With β = 0 this is close to zero. surfaceQuotes().rr25.
Extra charge for big moves +N vol pts — the butterfly Mean of the two 25-delta wing vols minus the ATM vol: the fat-tails premium. surfaceQuotes().bf25.
($K_put / $K_call today) The 25-delta reference strikes the wings were measured at. surfaceQuotes().k25put, .k25call.
Progress Confirm approval… / Approving… / Registering… / Shipping to Aqua… and the green … range is live card with $K₁ – $K₂ · N DTE · est. N% APR in premium The three transactions: ERC-20 approve to Aqua (skipped when the allowance already suffices), AquaCollateralVault.authorizeRange, Aqua.ship with the vault's ship parameters. useWriteContract chain in the component; getShipParams(authId) supplies the ship arguments.
node keeper/roll.mjs The keeper that settles, reclaims and re-ships at the new spot at expiry. Literal pointer.

Notes. Every estimate here uses the browser's smile constants and the header spot; the premium a buyer actually pays is set on-chain at fill time. The range only earns when someone buys against it; nothing leaves the wallet until then.

Earn · Write a Range

The manual version of the previous tab: choose strikes, collateral and expiry directly. The copilot's range prepare card ("Open in Write a Range → review & sign") prefills this form.

Element What it shows Where it comes from
Covered Calls / Cash-Secured Puts Side; sets the collateral token (WETH or USDC). Local state.
K_min (USD), K_max (USD) The strike band buyers may choose from. Equal values write a single strike. Inputs.
Collateral input, N WETH or N USDC summary The most collateral the range may pull in total. Calls: whole WETH units; puts: USDC. maxCollateral, sent as 18- or 6-decimal integers.
Expiry (DTE) 1 day / 7 days / 30 days Days to expiry. EXPIRY_PRESETS.
LP: 0x… The connected address that will own the range. useAccount().
Allowance indicator and the approve / authorise / ship progress Whether the Aqua allowance already covers the collateral, then the same three-step flow as One-Click. ERC-20 allowance(lp, Aqua); authorizeRange; Aqua.ship.
Green Range shipped to Aqua card with $K₁ – $K₂ Confirmation. The new range becomes the matrix's active range. onAuthorized callback into page.tsx.

Per chain. Identical on every chain; on Arc the USDC is Circle's native USDC (18-decimal native asset, 6-decimal ERC-20 view), which the app already accounts for.

Spreads

SpreadVault: write a credit spread that escrows only its true maximum loss, and buy one.

Element What it shows Where it comes from
Call / Put Structure type. Call credit spread: short K₁, long K₂ (collateral WETH, or USDC on a cash-settled vault such as Arc's). Put credit spread: short K₂, long K₁ (collateral USDC). Local state; strikes reset to spot rounded to $50 and +$200.
K1 (USD, lower), K2 (USD, higher) The two strikes. Inputs.
Capacity (units) Units the range may fill. Input, as WAD.
Expiry 7 / 30 / 90 days Days to expiry. EXPIRY_PRESETS.
Escrow, netted (S12) N WETH or N USDC The collateral the range pulls in total: calls units × (K₂ − K₁) / K₂ WETH; puts units × (K₂ − K₁) USDC, both rounded up. On a cash-settled vault (SpreadVault.cashSettledCalls() is true — Arc) calls use the USDC formula too. escrowFor() in SpreadDesk.tsx, mirroring SpreadVault.quote()'s escrow arithmetic; cashSettledCalls read once from the vault.
Main vault, naked short leg N WETH or N USDC What the main vault would lock for the same short leg: 1 WETH per unit, or K₂ USDC per unit. On a cash-settled vault the call figure is shown in USDC at spot (1 ETH per unit at spot). units, units × K₂, or units × spot.
Capital efficiency N× tighter Naked ÷ netted per unit: K₂/(K₂ − K₁) for calls, K₂/(K₂ − K₁) for puts (the example 3000/3200 gives 16×). "K2 must exceed K1" when the strikes are invalid. Computed in the component.
Progress and Spread #N shipped — E WETH backing it, still in your wallet. approve → SpreadVault.openStructureAqua.ship. useWriteContract chain.
Buy the SpreadStructure #N · call/put credit · $K₁ / $K₂, Expires, Status active/closed The latest structure on the vault. SpreadVault.structures(latest).
Units How many spread units to buy. Input.
Net premium (Ask long − Bid short) N USDC What the buyer pays before fees: the on-chain Ask of the long leg minus the Bid of the short leg, floored at 1 USDC. SpreadVault.quote(structureId, units), first output, refreshed every 10 seconds.
Protocol fee N USDC The 1% fee on the net premium. Second output of quote.
You pay N USDC Premium plus fee. The transaction passes 1% above this as the slippage cap. Sum; maxPremium = total × 1.01.
Writer's escrow pulled on fill N WETH / N USDC The netted escrow for this many units, pulled just in time from the writer's wallet at the fill. Third output of quote.
SpreadToken: 0x… and You hold N units The ERC-20 minted per structure and your balance. SpreadVault.buy return value; balanceOf.

Per chain. Sepolia's SpreadVault collateralizes call credits in WETH. Arc's is cash-settled (no WETH on the chain): the Arc receipt shows exactly 2.00 USDC pulled for 0.01 units of a 2600/2800 call credit spread, K₂−K₁ per unit, where a naked call would need 0.01 ETH.

Margin

MarginVault: write puts against initial margin instead of the full strike, buy one, and watch the health of the resulting position and of the vault as a whole. Two definitions first. Initial margin (IM) is what a writer must lock at the fill: min(K, intrinsic + IM buffer × mark) with the buffer at 50%. Maintenance margin (MM) is the floor below which the position can be flagged: the same formula with the 30% buffer. The mark is the lowest Chainlink answer in the last hour, never the vol surface.

Element What it shows Where it comes from
Write Margined PutsStrike min, Strike max, Margin capacity (USDC), Expiry 7 / 30 / 90 days The range and the total margin it may pull. Inputs.
Credit-line checkbox Opt in to an Aqua credit line that auto-tops-up margin from free balance before a flag. openRange's autoTopUp flag.
Range #N shipped — C USDC of margin capacity, still in your wallet. approve → MarginVault.openRangeAqua.ship. useWriteContract chain.
Buy a Put from ItRange #N · $K₁ – $K₂ · credit line on/no credit line, Expires, Status The latest range. MarginVault.ranges(latest).
Strike (USD), Units The put to buy. Inputs.
Writer locks (initial margin) N USDC IM for this strike and size off the live mark. MarginVault.initialMargin(rangeId, strike, units).
Main vault, cash-secured N USDC The full strike × units the main vault would lock, struck through. strike × units.
Capital efficiency N× tighter Full strike ÷ IM. Ratio.
Premium (Ask) N USDC The on-chain Ask for the put. MarginVault.quote(rangeId, strike, units).
Protocol fee (50% insurance · 30% backstop · 20% DAO) N USDC The fee and its split. Second output of quote.
You pay N USDC Premium plus fee; 1% slippage headroom in the transaction. Sum.
Buy N put @ $K and the two-step progress Check wallet — approve USDC… / Buying… Approve USDC to the vault, then MarginVault.buy(rangeId, strike, units, maxPremium). useWriteContract chain.
OptionToken (shared by every writer of this strike/expiry): 0x…, You hold N units The per-series token and your balance. seriesId(strike, expiry), seriesOf(sid).token, balanceOf.
Margin Health · $K put · date The health panel for the range's writer in this series. MarginVault.health(sid, lp), positions(sid, lp).
mark = lowest Chainlink answer in the last hour · $M over N rounds The mark and how many feed rounds the one-hour walk covered. MarginVault.markSpot() every 10 seconds.
Units short, Locked N USDC, Maintenance / Initial MM / IM The writer's open units, locked margin, and the two thresholds at the live mark. positions and health.
State: no position, at IM (green), above MM (yellow), below MM, flagged, in auction (red) Locked versus IM and MM, or the position's flag/auction timestamps. Derived in the component from positions[3] (flaggedAt), positions[4] (auctionStart) and health.
Vault exposureNaked notional, Ceiling (min of owner cap, 7× backstop), Buffers IM / MM 50% / 30% Open notional, its ceiling, and the two buffers as percentages of the mark over intrinsic. nakedNotional(), effectiveCeiling(), imBufferBps(), mmBufferBps() (basis points ÷ 100).
Behind the holdersBackstop pool, Insurance fund The two safety funds. MarginBackstop.totalAssets(), MarginVault.insuranceFund().
Funded through Circle App Kits — three receipts On Arc: the Wallets-kit deposit into the backstop and the Gateway mint and fundInsurance transactions, linking to arcscan. DEPLOYMENTS[chainId].demo entries whose label starts with "Treasury ·".
Footer: ./script/margin-lifecycle.sh, keeper/margin.mjs Liquidation is driven by the script on Anvil and by the keeper on a live chain, not by this page. Literal.

Per chain. On Arc the recorded fill locked 1.50 USDC for a 0.001-unit $3,000 put against the $3,000 mock mark (intrinsic 0 + 50% × 3,000 × 0.001). On Sepolia the mark comes from the real Chainlink feed. MarginVault.buy reverts when the mark is more than 90 minutes stale, which on Arc happens when nobody has posted a new mock round for a while.

Risk Monitor

Every margined position on this chain with its health against the live mark, the vault's risk dials, and the liquidation timeline rebuilt from MarginVault events.

Element What it shows Where it comes from
Margin mark $Mworst of N rounds · T min old The mark, the number of feed rounds inspected, and the age of the latest round. markSpot() every 5 seconds.
Naked notional N USDCceiling N USDC Open notional and its ceiling. nakedNotional(), effectiveCeiling().
Backstop pooladopts unsold positions Pool assets. MarginBackstop.totalAssets().
Insurance fundafter the backstop, before a haircut Fund balance. insuranceFund().
Buffers IM / MM 50% / 30%of spot, over intrinsic The margin buffers. imBufferBps(), mmBufferBps().
Positions list: 0x… · $K put · date or backstop pool · …, state label, bar, N units short, locked L · MM M · IM I One card per (series, writer) that ever held a short here. The bar is the locked margin; the white tick is MM and the faint tick is IM (the bar's full width is 115% of IM). Green when locked ≥ IM, yellow between MM and IM, red below MM. The state reads healthy, above maintenance, below maintenance, FLAGGED, IN AUCTION, closed or settled · finalized. Keys collected from MarginLocked, TakenOver (the bidder) and Absorbed (the pool) events; positions, health, seriesOf reads every 5 seconds.
Liquidation timelinelive · N events, scanning…, or an error The vault's events oldest-first, newest at the top, each with an icon, a plain-language line and the block number. Events covered: OptionBought, MarginLocked, ToppedUp, Flagged, FlagCleared, AuctionStarted, TakenOver, Absorbed, PositionSettled, SeriesFinalized, HolderHaircut, Redeemed. getLogs on MarginVault from its deploy block (Sepolia 11,677,124; Arc 61,470,464; Anvil 0), every 5 seconds. This is an RPC log scan, not the subgraph.
Explain with the copilot → Sends the last twelve events to the copilot with a request to explain who gained or lost what. smile:ask event; shown only when NEXT_PUBLIC_COPILOT=1.

Per chain. On Anvil, ./script/margin-lifecycle.sh fills this screen in real time (fill → crash → flag → auction → absorb → settle → finalize → redeem). On Sepolia and Arc only the recorded fill exists, because public chains cannot be time-warped through the grace and auction windows. Arc's RPC caps log ranges; a failure is shown in the caption rather than hidden.

RFQ

RfqVault: an LP ships a range, then signs price quotes off-chain in the wallet; a taker fills one. The formula tier stays the public floor; a signed quote can only improve on it.

Element What it shows Where it comes from
Strike min, Strike max, Capacity (WETH or USDC), Expiry 7 / 30 / 90 days The range to ship. Inputs; approve → RfqVault.openRangeAqua.ship.
Latest range #N · calls/puts $K₁–$K₂, Status active/revoked The latest range on the vault. RfqVault.ranges(latest) every 10 seconds.
Strike, Max size (units), Inside the formula (bps), Valid for (min) The quote you are about to sign: strike, maximum fill size, how far below the formula Ask to price (basis points), time to live in minutes (default 10). Inputs; the copilot's RFQ prepare card prefills them.
Tier-1 formula Ask N USDC The formula price for that strike and size, struck through. RfqVault.formulaQuote(rangeId, strike, size).
Your quote N USDC Formula Ask reduced by the improvement. formula × (1 − bps/10000).
Per unit N USDC Your quote divided by the size: the premiumPerUnit field of the signed message. Computed.
Sign Quote and the JSON that appears An EIP-712 signature over Quote(authId, strike, maxAmount, premiumPerUnit, ttl, nonce) under the domain "Smile RFQ", produced by the wallet with no transaction. The nonce is the current timestamp in milliseconds. "Hand this to a taker (or switch accounts and fill it on the right)." useSignTypedData.
Fill a quote — paste box, Quote range #N · $K · up to N units, Signed by 0x…, Status valid until HH:MM / expired / used / cancelled The pasted quote decoded, the recovered signer, and whether its nonce is still unused and its TTL unexpired. RfqVault.nonceUsed(lp, nonce) every 5 seconds; TTL compared with the clock.
Units to fill How many units to take, up to the quote's maximum. Input.
Tier-1 formula, incl. fee N USDC What the same fill would cost on the formula tier. formulaQuote for the fill size plus the fee.
This quote, incl. fee N USDC What this fill costs. RfqVault.fillCost(quote, units).
Price improvement N USDC (B bps) Formula cost minus quote cost, and as basis points of the formula cost. Difference.
Approve and Fill progress Approve USDC for the total, then RfqVault.fill(quote, signature, units, maxCost), which recovers the signer, checks ttl, size and nonce, and pulls the collateral just in time through Aqua. useWriteContract chain.

Per chain. RfqVault is deployed on Arc (recorded fill: 0.688860 USDC versus a 0.695819 formula Ask) and on Anvil; the Sepolia address map leaves it empty, so the tab reports the vault as unset there.

My Positions

The LP dashboard: the connected wallet's own most recent active range on the main vault and the collateral state behind it.

Element What it shows Where it comes from
Wallet ETH N ETHSelf-custodied — earning until called The wallet's native balance, four decimals. On Arc this is USDC. useBalance.
Locked Collateral N ETH or N USDCPulled JIT by Aqua on match — none locked yet / · released on close Collateral consumed by fills on your range. AquaCollateralVault.authorizations(myAuth).usedCollateral, every 6 seconds.
Active AuthorizationStrike Range $K₁ – $K₂, Max Collateral, Expires (date and days left), used/max progress bar Your latest active range. On Sepolia and Arc: the subgraph query authorizations(where: {lp, active: true}); on Anvil: getLogs on RangeAuthorized filtered by the lp topic. Distinct from the market-wide range the Trade tab uses.
Total Value Backing Quotes N ETHAvailable to back new options The wallet balance again, framed as the capital that can back new ranges. useBalance.
Active Positions e.g. 2Instruments you wrote with open interest A count of the (strike, expiry) instruments this wallet has written that still have open interest. Shows while the tape is loading and falls back to ≥1 only if the tape is unavailable. readTape() instruments filtered by lp and openInterest > 0 (subgraph on Sepolia/Arc, event log on Anvil).

Per chain. On a public chain without a subgraph this tab shows no range; there is no RPC fallback there by design (limitations L12a).

Vol Surface

A three-dimensional rendering of the implied-volatility surface, drawn in the browser with Plotly and updated as trades execute. No server: it works on the static, Vercel and local builds alike.

Element What it shows Where it comes from
Formula caption σ(K,T) = σ_tenor(T) · max(0.1, 1 + α·ln(K/S)² + β·ln(K/S)) The multiparameter smile: a per-tenor level, a curvature α and a skew β. Literal; α = 2.0 and β = 0.
The surface Volatility (height) over strike and tenor at the current spot, with the ATM term-structure ridge highlighted; drag to rotate. Computed in VolSurface.tsx from spot and the σ buckets, plotted by Plotly; re-renders when spot or the buckets change.
σ tenor: 0–7d N% · 7–30d N% · 30–90d N% · 90d+ N% The per-tenor sigma levels currently held (in component state). VolSurface.tsx state.
γ=N%/trade The bump applied to the traded tenor bucket per trade: up on a buy, down on a sellback. Constant (0.5%).
N trades How many trades have moved the surface since the last reset. VolSurface.tsx state.
Reset σ Resets the sigma buckets to their initial term structure. Local state.

Notes. The surface mirrors the feedback loop of the on-chain OptionPricingHook (each confirmed buy or sell on the Trade tab bumps the leg's tenor bucket by ±γ); it does not read the hook's state. The bucket math (SmileMath.sol's smile, the tenor edges, the ±γ step) is reproduced in TypeScript, so the surface renders identically everywhere with no Python service. (volsurface/ — the former Flask/matplotlib renderer — is retained for reference but no longer used by the app.)

Receipts

The recorded on-chain proof for the connected chain: demo transactions, contract addresses and the subgraph endpoint.

Element What it shows Where it comes from
On-Chain Proof · with the real-money caption One card per known deployment: only the connected chain when it is a known deployment, all known deployments otherwise (for example on Anvil). DEPLOYMENTS in lib/deployments.ts.
Transaction rows: 0x… ↗, label, note Each recorded demo transaction, linked to the explorer. .demo.
Contract rows: label and full address Every deployed contract, linked to the explorer's address page. .contracts.
The Graph: <url> The Studio query endpoint for the chain's subgraph. .subgraph.
You are on a local chain (no public explorer)… Shown on Anvil. No matching deployment.
This session's last premium swap 0x… ↗ The hash of the Uniswap swap executed in step 1 of the buy panel during this session, when one happened. onSwapTx from the Trade tab.

Per chain. Sepolia's card lists the range, ship and buy receipts; Arc's lists fills on all four vaults plus the three Circle App Kits treasury transactions.

Copilot panel

The floating Copilot button opens the AI panel on every tab. It is served by /api/copilot (a server route, present on the Vercel deployment and on ./local.sh, absent on the static GitHub Pages build) and reads the same tape, chain and documentation the app does. Its full tool list and configuration are described on the Integrations · The Graph page.

Element What it shows Where it comes from
Starter prompts Three prompts for the current tab plus one general one, shown while the conversation is empty. lib/copilot/tabs.ts, keyed by the active tab id.
Skills The built-in trader procedures and any you add; a starter from a skill sends it as a message. frontend/skills/*.md; user skills in browser storage.
⚙ settings Bring your own model API key and add MCP servers (The Graph Subgraph MCP is a preset). Sent per request in headers; never stored server-side. CopilotSettings.tsx.
Proposal card: table side · type · strike · amount · DTE · premium, mini payoff chart, debit/credit, max profit, max loss, prob. profit, Load into Payoff Builder → A strategy the copilot proposes, priced with the same protocolPremium and strategyStats as the builder. Loading it switches to the Trade tab with the legs in the builder. StrategyCard.tsx.
Smile chart The smile σ against strike with the ATM point and the 25-delta put and call strikes marked. ChatSmileChart.tsx, smileSigma, surfaceQuotes.
Range card: rows strikes $K₁ – $K₂, expiry N days, capacity N WETH/USDC, expected premium / unit $N, rationale, button Open in Write a Range → review & sign A range the copilot prepared from the liquidity map; the button prefills Earn · Write a Range and switches to it. PrepareCard.tsx, prepare_lp_range tool.
Quote card: rows strike $K call/put, max size N units, premium / unit $N (formula ask $M), valid for N min, button Open in RFQ desk → sign in wallet A quote the copilot prepared; the button prefills the RFQ form. The user signs in the wallet; the copilot never holds a key. PrepareCard.tsx, prepare_rfq_quote tool.
Quiz card A multiple-choice question with the answer explained after you pick. QuizCard.tsx, quiz_question tool.
Source citations in answers ("The Graph", "Deribit", document names) Where a number in the answer came from: the subgraph tape, the Deribit reference market, or the documentation pack. The tools' return values.

Notes. Any number the copilot quotes about positions, open interest, liquidity or last trades on a public chain comes from the subgraph; screener prices are computed from the hook's live sigma per expiry with the same smile model as the app, and the copilot states that caveat itself.

Known Limitations, Risks, and Recommendations

This document is the honest record of Smile's design trade-offs. It captures a series of design discussions from development so the reasoning survives the sessions that produced it. It is written for three audiences:

  • LPs deciding whether to ship a range — Part 1 explains, in plain language, the risk you are actually taking.
  • Integrators and auditors — Part 2 enumerates the known limitations with code references.
  • Future contributors — Part 3 is the sequenced roadmap of mitigations, with the rationale for the ordering.

Nothing here is a bug. Every limitation below is a trade the protocol made deliberately, usually exchanging some market-making efficiency for trustlessness. The point of this document is that you should know the price.


Part 1 — The concepts, in plain language

What an LP on Smile actually is

When you authorize a strike range, you are not "depositing into a pool." You are becoming a market maker: someone who posts standing prices and waits for other people to trade against them. The protocol quotes two prices on your behalf for every strike in your range:

  • the Ask — what a buyer pays you for an option;
  • the Bid — what you pay a holder who sells the option back (close()).

The Ask is always higher than the Bid. That gap is the bid-ask spread, and it is your compensation for standing in the market. Every round trip (someone buys at the Ask, later sells at the Bid) pays you the spread.

The natural question is: why does the spread need to exist at all? Why not quote one fair price? The answer is the single most important concept in this document.

Adverse selection, from zero

Start away from finance. You're selling a used car for a fixed, posted, non-negotiable price of $10,000. Two kinds of buyers show up:

  1. People who just need a car. Some think it's worth $9,500 and walk away; some think it's worth $10,500 and buy. Their errors are random — on average, you get a fair price from them.
  2. A mechanic who inspects the engine and knows something you don't. The mechanic only buys if the car is secretly worth more than $10,000. If it's worth less, they walk away.

Notice the asymmetry: the mechanic never loses to you, and you never win against the mechanic. You can't tell the two kinds of buyers apart — they both just hand you $10,000. But the composition of who chooses to trade with you is skewed against you. The trades you actually receive are selected adversely. That is adverse selection: when the other side gets to decide whether to trade at your fixed price, the ones who say yes are disproportionately the ones who know your price is wrong.

Now translate to Smile, with real numbers:

  • ETH trades at $3,000. The on-chain Chainlink feed says $3,000, and the vol surface prices a 3,100-strike call at $68.
  • News hits. On Binance, ETH jumps to $3,020 within one second. Chainlink only publishes a new price when the change exceeds its deviation threshold (0.5% for ETH/USD) or a heartbeat timer expires — so on-chain, ETH is still "$3,000" and the call still costs $68.
  • A fast trader (a sniper) sees both prices. The call is now genuinely worth about $74. They buy as many as your range allows at $68.
  • Chainlink updates. Your quote catches up. The sniper is up ~$6 per option, and that money came out of your collateral's expected value.

You were picked off: filled at a stale quote — a posted price that no longer reflects reality. The sniper is informed flow (they traded because they knew something the price didn't reflect). The ordinary trader who buys a call to hedge or speculate is uninformed flow — not stupid, just not trading against your error specifically; their trades are roughly fair for you and they pay you the spread. Flow that is systematically informed is called toxic flow, because filling it systematically loses money.

The punchline, due to Glosten and Milgrom (1985): since you cannot tell the mechanic from the ordinary buyer, your only defense is to charge everyone a spread wide enough that your winnings from the uninformed cover your losses to the informed. The spread is an insurance premium against your own ignorance of who you're trading with. A market with more snipers needs wider spreads; a market that could magically exclude snipers could quote nearly zero spread. Every limitation in Part 2 is a variation on this theme, and every recommendation in Part 3 is an attempt to shrink the insurance premium honest users have to pay.

The Greeks, briefly

The Greeks measure how an option position's value moves when market conditions move. For a Smile LP the relevant four are:

Greek Measures sensitivity to… A short-option LP has… Plain meaning
Delta (Δ) spot price negative (short calls) You lose as ETH rises past your strikes
Gamma (Γ) speed of spot moves negative Big moves in either direction hurt more than proportionally
Vega implied volatility negative You lose when the market gets more volatile
Theta (Θ) passage of time positive You earn a little every calm day

Short gamma + short vega + long theta is the classic market-maker profile: you are paid steadily for absorbing the risk of sudden moves. Writing options over a whole strike range makes this precise: by a classical result (Carr–Madan static replication), a portfolio of options spread across all strikes replicates a variance swap — a bet on realized volatility itself. An LP whose range fills broadly is therefore, to first order, short volatility as an asset class: profitable in calm markets, hit hardest in turbulent ones. If that risk profile isn't what you want, don't ship a wide range.


Part 2 — The limitations

Status update: since this document was written, the Phase 1–2 mitigations have been implemented: L3 (post-trade-only repricing) is addressed by size-convex intra-trade pricing, L4 (unbounded per-block drain) by per-authorization block caps, L1/L2 partially by the staleness-scaled spread plus the optional Pyth quoting adapter, and L11 (soft liquidity) by the firmness bond, reliability counters, and phantom-depth-aware bestQuote routing. The sections below describe the UNMITIGATED design so the reasoning stays legible; see Solutions for what is now in place.

EthOnline 2026 (September 2026): L8 is partially lifted by two opt-in sibling vaults — SpreadVault (S12) escrows a credit spread's true maximum loss and MarginVault (S13) lets a put writer post initial margin, at the price recorded in L13. L12a is lifted by the subgraph. R6 is built as RfqVault. Note that R1's per-authorization block cap lives in the main vault only; the three sibling vaults have no per-block cap, so L4 applies to them in full. Per-integration pages (Help → Integrations) collect the entries that touch each protocol. L14 records that the Uniswap v4 hook entrance (beforeSwap/afterSwap) has never run outside tests; L15 that self-fills are permitted and why the demo receipts are ones.

L1. Stale-quote sniping — the oracle latency gap

The premium is computed from Chainlink's last published price (OptionPremiumInstruction.sol reads latestRoundData()). Between real-world price changes and the next Chainlink update, every quote is stale, and stale quotes are free money for whoever notices first (Part 1). The maxStalenessSec guard (OptionPremiumInstruction.sol:190-194, and AquaCollateralVault.sol:725-728 for the vault's put pricing) rejects quotes against an old round — but it cannot reject quotes against a fresh round that is already wrong.

L2. The invisible window — sub-threshold drift is undetectable

Chainlink ETH/USD publishes on a 0.5% deviation or a heartbeat. Inside that threshold the off-chain price can drift up to 0.5% with no on-chain signal of any kind. No contract check can detect what the chain has never been told; this window is invisible by definition, not by implementation. The only defense is pricing: the LP's edge per trade must exceed the expected adverse move within the threshold — roughly Δ × 0.5% × spot of premium per option. The current spread (asymmetric Ask/Bid rounding plus the smile markup) is not explicitly calibrated to this floor.

L3. Repricing happens after the trade, not during it

Demand feedback exists — every buy bumps the tenor bucket's σ by GAMMA = 0.005e18 (0.5 vol points) and every sellback bumps it down (OptionPricingHook.sol:90-105, called from AquaCollateralVault.sol:405 and :558) — but the bump lands after the fill. The trader always executes at the pre-bump price. A market maker who repriced only after each fill would be run over; the on-chain analog is that a sniper pays no price impact on the trade where it matters.

L4. One transaction can drain a whole range

There is no per-block or per-trade size limit — a single buy() may consume an authorization's entire remaining maxCollateral at one price. Combined with L1–L3, the worst case is: oracle goes stale → sniper drains the full range in one transaction at one wrong price → σ bump fires too late to matter. The loss per staleness event is bounded by maxCollateral, not by anything smaller.

L5. On-chain rules cannot reject informed traders

It is tempting to ask the contract (or the v4 hook) to "reject malicious trades." It can't, for a structural reason: every on-chain rule is public. A sniper simulates your rejection logic before submitting and only sends transactions that pass. Rules can filter mechanically definable patterns — staleness, size, rate — but never information, because informedness is not observable on-chain. (If you could identify informed traders, Part 1 says you wouldn't need a spread at all.) Rejection is therefore the wrong frame; pricing is the right one — make toxic flow pay for its toxicity (see R1–R4).

L6. Passive LPs inherit the protocol's pricing model

The LP delegates pricing entirely to on-chain state: the σ tenor buckets, the smile curvature α, and the skew β. This creates a second-order exposure beyond the ordinary Greeks — parameter risk: ∂P/∂α ∝ vega·ln²(K/S) and ∂P/∂β ∝ vega·ln(K/S) (the smile-space analogs of volga and vanna). In trader-native terms these are the familiar desk exposures — sensitivity to the 25Δ butterfly (α) and the 25Δ risk reversal (β) — not bespoke protocol Greeks (see the Overview's "Reading the surface like a trader"). If governance moves α/β, or the demand-feedback loop walks a σ bucket away from fair, every open quote in every affected range marks against the LP with no action on their part. There is currently no dashboard surfacing this exposure.

L7. The demand-feedback loop is nudgeable

σ bumps are triggered by trades, and trades can be manufactured. Buying bumps σ up; selling back bumps it down. An attacker who wants a cheaper entry could sell back options to walk σ down before buying size. The attack is costly — each round trip pays the full bid-ask spread, and the protocol fee (1% on the Ask, none on sellbacks) taxes re-entry — and GAMMA is small, so moving σ materially takes many paid round trips. It is a bounded nuisance rather than a free lunch, but the loop is not manipulation-proof.

L8. Full collateralization is capital-inefficient — on purpose

Every call locks 1 WETH per unit regardless of strike (AquaCollateralVault.sol:395); every put locks the full strike value in USDC (:398). A margin system would let the same capital write 5–20× the notional. Smile chose full collateralization because it eliminates liquidation machinery, margin oracles, and insolvency risk — an option, once written, can always pay out. The cost is that LP returns on capital are structurally lower than a margined venue's. Aqua softens this (collateral stays in the LP's wallet, unrehypothecated, until the moment of sale) but does not remove it.

Update (EthOnline 2026): two opt-in rungs now sit above the main vault. SpreadVault (S12) margins a credit spread at its true maximum loss — 0.0625 WETH instead of 1 WETH for a 3000/3200 call credit spread, 200 USDC instead of 3,200 for the put twin — without changing the "always pays" property. MarginVault (S13) locks initial margin (1,500 USDC for an ATM 3000 put instead of 3,000) and does give up that property, which is why it is opt-in and why L13 exists. The main vault itself is unchanged.

L9. Settlement still depends on one oracle

settleWithChainlinkRound is permissionless and verifies that the supplied round actually brackets expiry — nobody can cherry-pick a favorable round — but the value settled is still whatever Chainlink published. A wrong or manipulated feed settles wrong, trustlessly. This is oracle risk, distinct from the latency risk of L1/L2, and it is shared with essentially every oracle-settled derivative on-chain.

On Arc testnet there is no Chainlink ETH/USD feed at all, so the deployment there settles against a MockV3Aggregator that anyone can set; a keeper mirrors Sepolia's Chainlink answer into it every 30 minutes so the price is real, but the trust is not (the Arc deployment notes, "Oracle tick"). Settlement on Arc is a demonstration of the mechanism, not of the trust model.

L10. The off-chain alternative has its own price

The known cure for L1–L5 is moving pricing off-chain (an RFQ model: the LP runs a server that streams live vol, reprices in milliseconds, scores counterparty toxicity, and signs short-lived quotes; the chain only verifies signatures and settles). This works — it is how Hashflow, Paradigm, and professional desks operate — but it trades away exactly what Smile is for:

  • The passive-LP thesis dies. "Ship one balance and walk away" becomes "operate a quoting server 24/7, or trust someone who does."
  • Composability breaks. Other contracts can't atomically request a signed quote; the on-chain surface is a lego, an RFQ endpoint is not.
  • The risk relocates rather than disappearing. A signed quote is frozen for its TTL (time-to-live) while the market moves; snipers attack the TTL window (quote fading), makers respond with shorter TTLs and last look (the right to reject a trade after seeing it) — each a worse version of the fairness problem the chain solved.
  • Toxicity scoring is discrimination. Refusing to quote addresses you dislike is precisely the permission the protocol promised not to require.

See R6 for the hybrid that captures most of the benefit without giving up the on-chain floor.

L11. Aqua liquidity is soft — quoted depth can be phantom

The flip side of unrehypothecation (L8's virtue): because the balance backing a quote sits in the LP's own wallet, it can be spent, transferred, or de-approved at any moment. A displayed quote is an unfunded intention until the block it executes — the JIT Aqua.pull() simply reverts at fill time if the collateral left. At small scale this is a UX annoyance (failed transactions); at scale it is a market-quality problem: takers and integrators cannot rely on displayed depth, and an adversarial LP could display size they never intend to honor, learning takers' intentions for free. Contrast: an order on Deribit's book is firm; an escrowed-vault quote is firm; an Aqua quote is indicative. Mitigations — honest depth display, slashable firmness bonds, fill-reliability scores, and a parallel firm tier with yield-bearing escrowed collateral — are specified in Solutions (S1–S4).

L12a. No indexer — lifted by the subgraph (EthOnline 2026)

Was: there was no subgraph or indexing service. LPDashboard found the connected wallet's active range by scanning RangeAuthorized logs from block 0 and reading each match; the copilot's readAuths looped every authId up to a hard MAX_AUTHS = 50 — past 50 ranges ever created it went blind, including to its own wallet's position — and then walked a 40-strike grid per range with N+1 RPC calls to find option balances. Cost grew with history, only one range per LP was ever shown, and a public RPC rate-limited the whole thing.

Now: subgraph/ indexes the vault into Authorization, Fill, Instrument (open interest, last trade per strike) and Position (holder balance) entities, live on Graph Studio for Sepolia (smile-sepolia) and Arc (smile-arc-testnet). The LP dashboard, the copilot's position tools and its trading tools (opportunities, liquidity map, portfolio greeks) read that tape through frontend/lib/tape.ts; the price chart draws premium and implied vol per instrument from it. On a public network there is no RPC path any more — a missing subgraph is an error, not a capped scan. The 50-range cap is gone.

What remains: the local Anvil chain has no graph-node (no arm64 image), so lib/tape.ts rebuilds the same entities from the vault's events there — gated on chain id 31337/1337 only. ERC-20 transfers of option tokens between wallets are not indexed (a data-source template per OptionToken would do it — plan G6); a transferred position shows on the original buyer until it is closed or redeemed. The Studio endpoints are rate-limited dev endpoints; publishing to the network and querying through the gateway with an API key (SUBGRAPH_URL, proxied by /api/subgraph) is the production path.

L13. Bad debt in the opt-in margin tier

MarginVault (S13) is the one place Smile's "a written option always pays" promise can break, and it is opt-in precisely because of that. What can go wrong, honestly:

  • The haircut path exists. If a writer's margin, their free balance, the takeover auction, the backstop pool, and the insurance fund are all exhausted at finalization, holders of that series are paid pro rata from the pot (payoutPerUnit, haircutBps on the series) and HolderHaircut is emitted. The naked-notional ceiling (7× the backstop) and the gap-40 test bound this, they do not eliminate it — a gap larger than 40% from the maintenance point in one heartbeat can exceed the pool.
  • Settlement is a heartbeat wide. Marks stop at expiry but the settlement round can land up to one Chainlink heartbeat later; the two-step design (per-writer waterfall, then finalizeSeries) is what keeps that gap from becoming an oracle race, and a writer who never settles is finalized around after 6 hours and repays the pool when they do.
  • The credit line is consent, not collateral. A range's autoTopUp pulls from the same Aqua allowance the fill used — bounded by what is still shipped, in the wallet, and approved. The writer can dock or spend it at any time, so a margin call may find nothing there; the vault then flags rather than reverts.
  • Sigma still moves the trade, not the margin. Premiums follow the vol hook (that is the product); margin requirements read the oracle only. A manipulated sigma can make a put expensive, it cannot drain margin.
  • SpreadVault.releaseCollateral-style admin risk does not exist here, but the owner does set the naked-notional ceiling, the fee split, and the vol buffers — the buffers only ratchet up, with a 24 h delay on the maintenance side, so a parameter change cannot liquidate anyone who had no time to answer it.
  • Whole-position liquidation only. Takeover and absorb move a writer's entire position in a series; partial-unit takeover is on the plan's cut list. Per-range maxBlockNotional (R1) is not implemented in this vault; the global ceiling bounds exposure instead.

L14. The Uniswap v4 hook path has never run live

OptionPricingHook has two entrances. The one every trade uses is direct: the vault reads sigmaFor() to price and calls bumpSigma() after each fill and sellback (AquaCollateralVault.sol:553, :830), gated only vault. The other is the Uniswap v4 interface — beforeSwap (a 5% fair-value veto) and afterSwap (a surface-wide sigma bump) — gated msg.sender == poolManager. That path is exercised in test/OptionPricingHook.t.sol by pranking a fake pool-manager address and nowhere else: script/Deploy.s.sol passes address(1) as the pool manager on every network, no script creates a v4 pool, and Arc has no Uniswap deployment at all. Consequences: there is no on-chain secondary market for OptionTokens (holders exit only through close() at the Bid or by holding to expiry), and the "afterSwap shifts the whole surface" feedback described in the Overview's flow diagrams is a design, not a live mechanism. Nothing about pricing or the demand loop depends on it: the vault path carries the whole vol surface on Anvil, Sepolia and Arc alike. The fix is a deployment, not code — a v4 pool per OptionToken on a chain with Uniswap v4, with the hook's poolManager set to the real one (see the Uniswap page).

L15. Self-fills are allowed — and economically null

An LP can buy from its own range: nothing in any vault requires buyer != lp. A broker forbids this (a self-cross is a wash trade: it prints volume and a price no counterparty agreed to), and can enforce it because it sees both sides of one account. A public chain cannot see "the same person" — a second wallet defeats any on-chain guard in seconds — so Smile, like every permissionless venue, does not try (see L5: rejection is the wrong frame, pricing is the right one).

What a self-fill is here: the premium goes from the buyer to the LP, i.e. from one pocket to the other, minus the 1% protocol fee; collateral moves from the LP's wallet into escrow and comes back at expiry as payout plus reclaim. Because every option is fully collateralized, no counterparty risk is created and solvency is never touched — the position nullifies by construction. What it does do is what wash trades do anywhere: print volume and open interest on the tape (the subgraph) that is not real demand, and move the demand-feedback loop (each buy bumps the traded tenor's sigma). That second effect is L7, and it is bounded by the spread plus the fee paid per round trip.

Disclosure: the recorded demo fills on Sepolia and Arc (the Sepolia and Arc deployment notes) are the deployer buying from its own range, so the subgraph had a real trade to index. They are labelled as self-fills. A buyer != lp check was considered for the three sibling vaults as a guard against accidental self-fills and left out on purpose: it would be cosmetic, and the main vault is unchanged.

L12. The per-trade gas floor — and where it actually comes from

Every fill pays a fixed gas overhead, which sets a minimum economical trade size: below it, gas dominates the premium. Measured (test/GasProbe.t.sol, reproduce with forge test --match-contract GasProbeTest -vv):

Operation Gas
First buy() at a (strike, expiry) — lazily deploys the series' ERC-20 ~1,040,000
Repeat buy() in an existing series — full pricing + swap + mint ~198,000
close() sellback at Bid ~140–240k
redeem() / reclaimCollateral() after settlement ~46–98k

The instinctive reading — "computing option premiums on-chain is too expensive" — is wrong here, and measurably so. The pricing arithmetic (lnWad Padé series, one integer sqrtWad, the staleness spread, two size-impact iterations) is a rounding error inside the 198k repeat-fill cost, which is in the range of an ordinary Uniswap v3 swap; the design already avoids exp/N(d₁) entirely (Overview §Mathematical Specification; the price of that shortcut is that σ is not a Black-Scholes IV — a measured real Black-Scholes costs ~5k gas more per evaluation, see Overview §2). What actually dominates is series bootstrapping: ~840k of the first fill is the one-time deployment of that series' plain-ERC-20 OptionToken — contract-creation bytes, not math. Moving the calculation off-chain would therefore save almost nothing while paying L10's full price.

The real consequence: small first trades in a fresh series are uneconomical on expensive blockspace, and the first taker at each strike subsidizes everyone after. Mitigations, in order of value: deploy OptionTokens as EIP-1167 clones (~45k instead of ~840k, costing ~2.6k of delegatecall overhead on later transfers); let LPs or a keeper pre-deploy the series for the strikes they quote, so no taker ever pays the deploy; and cheaper blockspace, which shrinks the whole floor linearly — a deployment choice, not a property of the code.


Part 3 — Recommendations

Note: these recommendations are expanded into a full sequenced build plan — with the soft-liquidity solutions, the LP-quoted-vol competitive pricing design, demand strategy, and phase gates — in Solutions.

Ordered by benefit-to-complexity. Phases 1–2 are contained contract changes; each is testable in isolation. The guiding principle, from L5: stop trying to reject bad flow; price it.

Phase 1 — On-chain hardening (cheap, high value)

R1. Per-block notional cap per authorization. Track (lastTradeBlock, blockNotional) per auth and cap fills per block at an LP-chosen fraction of maxCollateral. Converts the L4 worst case from "lose the whole range at one stale price" to "lose one block's cap," and forces a sniper into multiple blocks — across which σ bumps and oracle updates catch up. Small change to AquaCollateralVault.buy().

R2. Size-convex pricing (intra-trade impact). Apply the σ bump inside the premium calculation, proportional to trade size, so a trade of n units pays the average of a σ that rises as it fills — the options analog of an AMM curve, or of Kyle's lambda (price impact per unit of flow). Fixes L3: large informed trades eat their own impact at execution time rather than gifting the pre-bump price. Change to SmileMath + OptionPremiumInstruction.

R3. Staleness-scaled spread. Widen the Ask−Bid spread as a function of block.timestamp − updatedAt, instead of the current cliff at maxStalenessSec. A quote against a 2-second-old round is tight; against a 50-minute-old round, wide. Prices L1 continuously.

R4. Spread floor calibrated to the deviation threshold. Enforce spread ≥ Δ × deviationThreshold × spot per option (with the 0.5% threshold as an instruction arg). This is the L2 insurance premium made explicit — the minimum edge at which quoting inside the invisible window is positive-expected-value.

Phase 2 — Shrink the staleness window itself

R5. Pyth pull-oracle integration. Pyth delivers ~400ms-fresh prices in the taker's own transaction (the taker submits the signed price update alongside the trade; the contract verifies and prices against it). This closes most of the L1 latency gap and shrinks the L2 window from "0.5% deviation" to "sub-second drift," while remaining fully composable and deterministic — no LP server, no signatures from the LP, no last look. Contained change to the instruction's oracle read plus a freshness check. Phases 1+2 together likely capture ~80% of the RFQ benefit at ~10% of its complexity — ship these before considering R6.

Phase 3 — Hybrid RFQ, only if flow data demands it

R6. Signed-quote tier on top of the on-chain floor. Two tiers, tradfi's "NBBO + price improvement" structure: tier 1 is the existing on-chain surface — permissionless, composable, always live, and the guaranteed fallback for close() so holders are never captive to a server. Tier 2 is a new signedPremium instruction verifying an LP's EIP-712 quote (strike, expiry, premium, maxAmount, ttl, nonce), settling through the identical Aqua pull/push. Takers query both and take the better price. Sophisticated LPs run fast repricing and toxicity models off-chain and win flow with tighter quotes; passive LPs keep the tier-1 spread. Build this only if, after Phases 1–2, realized LP markouts (P&L measured a few minutes after each fill) show flow is still systematically toxic.

Implemented (EthOnline 2026) — as a sibling AquaApp rather than a SwapVM instruction: src/periphery/RfqVault.sol. The LP ships a range, signs Quote(authId, strike, maxAmount, premiumPerUnit, ttl, nonce) under the EIP-712 domain "Smile RFQ", and fill() recovers the signer, checks ttl / size / nonce, then settles like a tier-1 fill (premium + fee in, collateral pulled JIT through Aqua). formulaQuote() is the tier-1 Ask for the same range. Nonces are single-use and cancellable. The markout gate above was skipped; the tier is opt-in per range, so tier 1 is untouched for anyone who does not sign.

Phase 4 — LP risk tooling

R7. Range-Greeks panel in the LP dashboard. Show, per authorization: ex-post Greeks of actually-sold series; ex-ante expected Greeks under an assumed fill distribution over [strikeMin, strikeMax]; worst-case Greeks (full maxCollateral at the most adverse strike); and the parameter sensitivities ∂P/∂α, ∂P/∂β from L6. An LP should be able to see "shipping this range ≈ short X vega, worst case Y" before signing.

R8. Markout monitoring. Off-chain analytics job: for every fill, record the surface's own re-quote 1/5/30 minutes later. Persistent negative markouts are the empirical signature of adverse selection and the trigger condition for R6. Without this measurement, the Phase-3 decision is a guess.


Glossary

Term Meaning
Adverse selection When counterparties choose whether to trade at your posted price, those who accept are disproportionately those who know the price is wrong.
Informed / uninformed flow Trades motivated by knowledge your price doesn't reflect yet / trades motivated by hedging or opinion, fair to you on average.
Toxic flow Order flow that is systematically informed; filling it loses money on average.
Picked off / sniped Filled at a stale quote by a faster, informed trader.
Stale quote A posted price that no longer reflects current information.
Bid-ask spread Gap between the price you sell at (Ask) and buy back at (Bid); the market maker's compensation, and (Glosten–Milgrom) the insurance premium against informed flow.
Glosten–Milgrom (1985) Model showing spreads exist because makers can't distinguish informed from uninformed traders.
Kyle's lambda Price impact per unit of order flow; large trades move the price against themselves.
Markout A fill's P&L measured against the market price some minutes later; the standard empirical test for toxic flow.
Deviation threshold / heartbeat Chainlink publishes only when price moves >0.5% or a timer expires; between updates the chain is blind.
RFQ (request-for-quote) Off-chain model where a maker signs short-lived quotes and the chain only verifies and settles.
TTL / quote fading / last look A signed quote's validity window / attacking the maker within that window / the maker's right to reject after seeing the trade.
Delta, Gamma, Vega, Theta Sensitivity of option value to spot, to speed of spot moves, to implied volatility, to time (see Part 1 table).
Vanna / volga Second-order Greeks: sensitivity of vega to spot / to vol. Here: the LP's exposure to the surface parameters β and α — in trader terms, to the risk reversal and the butterfly.
ATM vol / expected move The at-the-money implied volatility: the market's price for movement itself, direction-blind. ATM vol × √(time to expiry) ≈ the expected move — how far the market thinks spot could drift by expiry, which is what an option premium is fundamentally charging for.
Risk reversal (25Δ RR) The vol of an out-of-the-money call minus a matching out-of-the-money put: which direction costs more. Negative = crash insurance is pricier (the usual state). Maps one-for-one to the smile's β (skew) parameter.
Butterfly (25Δ BF) Average wing vol minus ATM vol: how much extra a big move costs versus a small one — the market's fat-tails charge over a perfect bell curve. Maps one-for-one to the smile's α (curvature) parameter.
25-delta (25Δ) strikes The OTM call and put that each have ~25% probability of finishing in the money — the industry's standard reference points for measuring the wings of the smile (hence "25Δ RR", "25Δ BF").
Variance swap A contract paying realized variance; replicable by holding options across all strikes (Carr–Madan), which is why a broadly-filled range LP is "short volatility."
Static replication (Carr–Madan) Result that any smooth payoff — including a variance exposure — decomposes into a strike-weighted portfolio of vanilla options.
JIT / unrehypothecated Collateral stays in the LP's own wallet, unlent and unreused, and is pulled only at the moment an option is actually sold.
NBBO + price improvement Tradfi structure where a public best price is the floor and competitive makers may beat it; the model for the R6 hybrid.

Solutions Plan: Making Smile Viable

Companion to Limitations. That document diagnoses; this one prescribes. It takes the five hard problems — including the ones Aqua itself introduces — and lays out concrete solutions, what each costs, and the order to build them in, with measurable gates between phases.

The framing question that prompted this document was "we don't have to use Aqua — how do we solve these problems?" The answer reached below is that dropping Aqua is the wrong dichotomy. Aqua's unrehypothecated model is the protocol's genuine novelty and its best LP-acquisition funnel; its weakness (soft, revocable liquidity) is real but priceable. The design that follows keeps Aqua as the soft tier of a two-tier liquidity system, adds a firm tier whose lockup cost is engineered to ~zero with yield-bearing collateral, and lets takers and routers price the difference. Markets already work this way: firm central-limit-order-book quotes coexist with indicative RFQ streams.


The five problems

# Problem Root cause Limitations ref
P1 Soft liquidity / phantom depth — quoted depth can vanish before fill because the backing balance sits in the LP's wallet Aqua's JIT pull model L11
P2 Adverse selection — stale quotes get picked off; repricing lands after the fill; one tx can drain a range Oracle latency + post-trade feedback + no caps L1–L5
P3 Uncompetitive pricing — σ comes from a crude demand-feedback loop, not vol discovery; every LP is a price-taker of one global model No mechanism for market participants to express a vol opinion L6
P4 Thin demand — on-chain vanilla options historically lose to Deribit and perps No structural wedge exploited yet Part 3 discussion
P5 Capital inefficiency — full collateral caps LP return on capital Deliberate no-liquidation design L8

P1 — Soft liquidity: price firmness instead of assuming it

S1. Honest depth display (frontend only — ship first)

Quoted depth for an authorization must be min(maxCollateral − usedCollateral, wallet balance, Aqua allowance), computed live via staticcall before display, not the authorized maximum. Phantom depth becomes visible before it becomes a failed transaction. Zero contract changes; a useFirmDepth hook in the frontend and the same check in any quoting API. This doesn't solve softness — the balance can still move between quote and fill — but it eliminates the routine case and the adversarial "display depth you never intended to honor" case becomes a one-block race instead of a standing lie.

S2. Firmness bond (small contract change)

At authorizeRange, the LP escrows a small slashable bond in the vault (e.g. max($25, 25 bps × maxCollateral), owner-tunable). If a fill fails because the JIT Aqua.pull() reverts — balance moved, allowance revoked — the taker's transaction claims a fixed slice of the bond as compensation for gas and adverse selection (they revealed their trading intention for nothing). Bond returns in full at deauthorization. Economics: quoting stays nearly free, but lying about depth now has a price. The bond also creates the on-chain event needed for S3.

S3. Fill-reliability score

The vault already knows when a pull fails (the S2 path). Persist a per-LP counter (fills, failedPulls) and expose it. Frontends sort and badge LPs by reliability; routers de-prioritize unreliable ones. Reputation is the cheap complement to the bond: the bond prices a single failure, the score prices a pattern.

S4. Firm tier: escrowed ranges with yield-bearing collateral

The structural answer. A parallel authorizeRangeFirm path where collateral is escrowed in the vault at ship time — firm by construction, pull() cannot fail — but denominated in yield-bearing collateral so the lockup costs ~nothing:

  • Calls: wstETH (or other LST) instead of WETH. The escrowed collateral keeps earning staking yield (~3%) while backing quotes.
  • Puts: sDAI / sUSDe / aUSDC instead of USDC. The cash security earns the savings rate (~4–8%) while locked.

This dissolves the historical objection to escrow (dead capital in Ribbon-era vaults) — the opportunity cost of locking yield-bearing collateral is approximately zero, because the collateral does its other job while locked. Settlement math needs an exchange-rate read (wstETH/ETH, sDAI/DAI) at payout, which is a contained change to redeem/reclaimCollateral.

Firm quotes carry a firm flag; the router (S6) prefers firm liquidity at equal price, and soft (Aqua) quotes must be better to win flow — softness gets priced by competition rather than banned. LPs self-select: passive wallets use Aqua's zero-commitment tier; serious LPs escrow yield-bearing collateral for priority. This is the resolution of "do we have to use Aqua": both, tiered, with the market pricing the difference.

New trust surface, stated honestly: the firm tier inherits the yield source's risk (Lido, Maker/Sky, Aave). Cap the accepted collateral list and keep plain WETH/USDC escrow as the conservative option.

MVP scope (implemented). The firm tier shipped early in its minimal form: FirmEscrow (src/periphery/FirmEscrow.sol) — a wrapper contract that becomes the LP's wallet from Aqua's perspective. It holds plain WETH/USDC, is the msg.sender that authorizes and ships the range, and has no code path that moves collateral out except Aqua's own pull(); withdrawals refuse to dip below the total committed to live ranges (revoke first — cancelling a displayed quote is legitimate, keeping it live while unbacking it is not). L11's one-block front-run is impossible by construction. SmileQuoteLens prefers registered firm makers at equal price, so soft quotes must be strictly cheaper to win flow. Deferred to full S4: yield-bearing collateral (wstETH/sDAI + settlement FX reads) and firm Bid depth (premium income stays withdrawable, so sellbacks can still bounce, exactly like the soft tier). Rationale for opening the gate before the S3 data arrived: the fill-failure-rate gate has a censoring blind spot — it cannot count the integrators and size takers who never route because depth is indicative — and the wider 1inch ecosystem's answer to soft maker liquidity (simulation + reputation, i.e. S1/S3) does not serve users who need firmness as a precondition, not a probability.


P2 — Adverse selection: the hardening set

Specified in Limitations Part 3 as R1–R5; summarized here because Phases below reference them:

  • R1 Per-block notional cap per authorization — bounds loss per staleness event to one block's cap instead of maxCollateral.
  • R2 Size-convex intra-trade pricing — the σ bump applies inside the premium integral, so large trades pay their own impact at execution.
  • R3 Staleness-scaled spread — Ask−Bid widens continuously with oracle age instead of cliff-rejecting at maxStalenessSec.
  • R4 Spread floor ≥ Δ × deviationThreshold × spot — the minimum edge at which quoting inside Chainlink's blind window is positive-EV.
  • R5 Pyth (or Chainlink Data Streams) pull-oracle for quoting, with a seconds-tight freshness bound; Chainlink rounds stay for settlement.

P3 — Vol discovery: let LPs quote vol, route to the best

This is the deepest change and the one that most directly answers "can the pricing ever be competitive."

S5. Per-range LP-quoted vol

Today every LP is a price-taker of one global surface (σ buckets + α + β set by the protocol). Add an LP-chosen sigmaBps multiplier (or absolute σ override) to each authorization, serialized into the strategy's instruction args like the existing parameters. The protocol surface becomes the default for passive LPs; opinionated LPs quote their own vol — exactly how professional options markets quote (in vol, not price). An LP who thinks the surface is rich undercuts it and wins the flow; one who thinks it's cheap quotes higher and only fills when they're happy to.

S6. Best-quote routing across ranges

buy(authId, …) currently targets one explicit authorization (AquaCollateralVault.sol:381) — the frontend auto-picks the latest one. Add a router view bestQuote(strike, expiry, isCall, amount) that scans active authorizations covering the strike (an enumerable index per (asset, isCall) is needed), applies the S1 firmness check, and returns the best executable Ask/Bid; and buyBest(...) that routes to it. S5 + S6 together create competitive vol discovery: overlapping ranges with different σ opinions form an order book in vol space, and the touch — the best bid/ask across LPs — is the discovered market vol. No oracle for IV needed; discovery emerges from the same mechanism as every other market: competition.

S7. Optional external IV anchor

If range competition stays thin early on, anchor the default surface's σ buckets to an external implied-vol reference (e.g. a curated feed of Deribit ATM IV, or an on-chain vol index) with the demand-feedback loop reduced to a bounded, mean-reverting deviation around the anchor. Passive LPs then inherit approximately-fair vol instead of a random walk. This is a pragmatic bridge, not the destination — S5/S6 is the trust-minimized endgame — and it adds an oracle dependency, so gate it on evidence that the default surface is drifting (persistent negative markouts on the passive tier).

S8. Markout instrumentation (R8 — decision infrastructure)

Off-chain job: for every fill, record the surface's own re-quote at +1/+5/+30 minutes. Every phase gate below reads this data. Build it first; it is the protocol's profit-and-loss telescope and every argument about "is pricing fair" is a guess without it.


P4 — Demand: sell the wedge, not the option

S9. One-click covered-call / cash-secured-put product

The LP-first product: pick an asset and a yield target → the app ships a sensible OTM range (e.g. 10-delta to 25-delta calls, 30 days) → auto-rolls at expiry, harvesting premium. Ribbon proved retail wants this UX and locked nine figures for it with custody risk; Smile's version is self-custodial (Aqua tier) or yield-stacked (S4 firm tier: staking yield + premium yield). Pure frontend + a keeper for rolls; no new contract surface beyond what exists.

Implemented (MVP). The One-Click Income tab (frontend/components/IncomeOneClick.tsx) turns a side + risk preset (10–20Δ / 20–30Δ / 30–40Δ, delta ≈ P(ITM) — the knob thetagang actually reasons with) into a strike range via the same smile-adjusted Black-Scholes the chain quotes, shows estimated premium APR, and runs approve → authorizeRange → Aqua.ship as one auto-chained flow. Auto-roll is keeper/roll.mjs — deliberately LP-RUN and self-custodial (rolling needs the LP's signature; nobody else can touch the position): at expiry it settles each series permissionlessly, reclaims the remainder, revokes, and re-ships a fresh range at the same delta band against TODAY's spot. Verified live on Anvil: open → fill → expiry → settle → reclaim → roll to the new spot, one command.

S10. Distribution through the 1inch ecosystem

The Aqua/SwapVM integration isn't just plumbing — it's a distribution channel. Options premiums are quoted by a SwapVM strategy, so 1inch aggregation/Fusion can route into them like any other liquidity source, and option tokens are plain ERC-20s tradable anywhere. Pursue: listing in the 1inch ecosystem registry, the Aqua Revenue Stream Incubator grant, and resolver integrations. Demand-side flow arrives through integrators, not a standalone venue's UI.

S11. Long-tail listings

Deribit lists three assets. Smile can permissionlessly list options on anything with a reliable price feed — LST/LRT tokens, majors' L2 variants, blue-chip DeFi tokens. Long-tail is where an on-chain venue has no centralized competition, and where covered-call yield on treasury/DAO holdings (S9) is a genuinely unserved market. Constraint to respect: thin feeds are easier to manipulate, so long-tail listings need conservative parameters (wider spread floors, lower caps, longer staleness bounds — all per-authorization args that already exist or arrive in Phase 1).


P5 — Capital efficiency

S4 (again). Yield-bearing collateral

Solves the largest chunk: collateral earns its native yield while escrowed, so "full collateral" stops meaning "dead capital."

S12. Defined-risk netting (later)

Full collateralization is per-option today: a call spread (long K₁, short K₂) locks collateral for the short leg as if naked, though the structure's true worst case is K₂ − K₁. Vault-level netting for recognized two-leg structures held by the same LP releases the difference — substantial efficiency for spread writers with no liquidation machinery, because defined-risk structures stay fully covered at their true maximum loss. This is real design work (position accounting, early-exercise-free European payoffs make it tractable) and belongs after product-market signal, not before.

Designed — full specification in the plan: exact collateral requirements per structure (call credit spread (K₂−K₁)/K₂ WETH ≈ 16× tighter; condors need max-not-sum of the two sides since one terminal price can't breach both), the dominance result that makes debit spreads need ZERO extra collateral, and the recommended architecture (a sibling SpreadVault AquaApp — the main vault has no EIP-170 room and is never touched).

Implemented (EthOnline 2026)src/periphery/SpreadVault.sol: call credit and put credit spreads, quoted off the shared surface via SmilePremiumLib, escrowing (K₂−K₁)/K₂ WETH / K₂−K₁ USDC through the vault's own Aqua strategy, settled at one price through one formula with wei-exact conservation (test/SpreadSettlement.t.sol). The iron condor is now a real single structure on a cash-settled (USDC-native) vault: one openStructure with all four strikes escrows max(putWidth, callWidth) USDC — the wider wing, not the sum — since one terminal price can't breach both sides; premium is both wings, one SpreadToken, one settlement (test/SpreadCondor.t.sol). It goes live at the Arc SpreadVault redeploy.

S13. MarginVault — opt-in true margin (rung 4)

Rung 4 of the ladder is the one that can break "a written option always pays", so it is a separate, opt-in tier — src/periphery/MarginVault.sol, puts only, USDC only, its own settlement and its own backstop pool; the main vault is untouched. A put writer posts initial margin instead of the strike: min(K·u, intrinsic + 50% of spot per unit) off a worst-of-hour Chainlink mark (the lowest answer in the last hour), so an ATM 3000 put locks 1,500 USDC, not 3,000. Margin reads the oracle only — never the vol hook — so trading cannot move what anyone has to post (L7), and the test suite proves it bit for bit after 400 sigma bumps.

What stands behind the holder, in order: the writer's locked margin, the writer's free balance and (opt-in) an Aqua credit line swept at the margin call; a 30-minute writer-takeover auction with a 1→10% bonus; the backstop pool, which adopts unsold positions and pays the residual shortfall at finalization; the insurance fund (half the fee plus liquidation penalties); and only then a holder haircut — emitted loudly, with the IM buffer ratcheting up 500 bps. Exposure is capped Maker-style: naked notional can never exceed 7× the backstop pool, and the pool's withdrawals are delayed, floored, and frozen while an expired series is unfinalized.

Implemented (EthOnline 2026) — B1–B8 of the plan: MarginVault.sol (23.5 KB, under EIP-170 without a split), MarginBackstop.sol, 54 tests across test/Margin*.t.sol including the gap-40 solvency test and a book-balance invariant, script/margin-lifecycle.sh (fill → crash → flag → auction → absorb/takeover → settle → finalize → redeem, on Anvil) and keeper/margin.mjs. See L13 for what it deliberately does not promise.


The plan

Executable task-level plan for Phases 0–1 (exact files, signatures, tests, commands, written for mechanical execution): see the plan.

Status: the contract side of Phases 1–2 is IMPLEMENTED — R1 (per-block caps), R2 (size-convex pricing), R3+R4 (staleness-scaled spread with a configurable floor), S2 (firmness bond, deploy-opt-in via FIRMNESS_BOND_BPS), S3 (fills/failedPulls counters), S5 (per-range sigmaMulBps), S6 (bestQuote/buyBest with S1 phantom-depth skipping), and R5 (PythSpotAdapter pull-oracle for quoting, deploy-opt-in via PYTH/PYTH_PRICE_ID). Phase 0 is implemented too: S1 honest depth (useFirmDepth hook — firm-depth readout, soft badge, buy gating) and S8 markouts (analytics/markouts.mjs, verified live on Anvil). S6 routing lives in the SmileQuoteLens periphery so the vault stays under the EIP-170 size limit. Phase 3 is MVP-SCOPED OPEN: S4 shipped early as the plain-collateral FirmEscrow wrapper + lens firm-first tiebreak (see the S4 MVP note above); yield-bearing collateral remains deferred. Phase 4 has its first piece: S9 one-click income + self-custodial auto-roll keeper (see the S9 note). S12 is fully designed (sibling SpreadVault, dominance netting) but implementation stays gated. Still open: full S4, S10–S11, S12 implementation, S7.

Sequenced by (value ÷ effort), with a measurable gate before each phase. Phases 0–1 are days-to-weeks of contained work; nothing in them is wasted even if later phases never happen.

Phase Contents Effort Gate to proceed
0 — Measure & be honest S1 honest depth display · S8 markout job Frontend + off-chain script; no contracts — (do unconditionally)
1 — Harden R1 per-block caps · R2 size-convex pricing · R3 staleness spread · R4 spread floor · S2 firmness bond · S3 reliability score One contract PR: vault + instruction + tests Markouts confirm pick-offs exist (they will)
2 — Compete ✅ (contracts) S5 LP-quoted vol · S6 best-quote routing · R5 Pyth quoting oracle Contract PR (router index + instruction arg) + frontend Phase-1 markouts improved but spread still uncompetitive vs Deribit mid
3 — Firm up ✅ (MVP scope) S4 firm tier — MVP: plain-collateral FirmEscrow wrapper + lens firm-first preference (shipped); full: wstETH & sDAI yield-bearing escrow MVP: periphery-only, no vault changes; full: settlement FX reads + tests Gate opened early: the fill-failure metric cannot see demand that never routes to indicative depth (censoring); firm depth is an integrator precondition. Yield-bearing upgrade still gated on S3 data + firm-tier uptake
4 — Sell it S9 covered-call one-click + auto-roll keeper · S10 1inch distribution · S11 first long-tail listing Frontend + keeper + BD, minimal contracts Phases 1–3 metrics: LP markouts ≥ 0 over a month — i.e. the product is safe to market
5 — Scale capital S12 defined-risk netting · S7 IV anchor if passive tier drifts Significant contract design Real volume; LP demand for spreads

Kill criteria, stated in advance (the discipline the graveyard lacked): if after Phases 1–2 the passive tier's 30-minute markouts stay persistently negative at every spread level takers will accept, the passive-surface model is wrong — pivot the protocol to S5-only (all vol LP-quoted, protocol provides settlement + custody rails, no house model). If firm-tier fill-reliability and covered-call retention are strong but taker flow never arrives, pivot distribution-first (S10) before adding any further mechanism.

What this plan deliberately does not do

  • Drop Aqua. Its softness is priced (S1–S3) and competed against (S4) instead. The zero-commitment funnel is worth keeping.
  • Move pricing off-chain. The RFQ tier (Limitations R6) stays gated behind markout evidence; Phases 1–2 are expected to make it unnecessary.
  • Add margin/liquidations. S12 achieves capital efficiency only where it requires no liquidation engine.

AI Copilot

Smile ships an in-app AI chat assistant — the Copilot. It is not mentioned in the Overview; this page is its documentation.

Where it lives

A floating chat button, bottom-right corner of every tab in the app (frontend/components/copilot/CopilotPanel.tsx). Click it to open a slide-over chat panel with a few starter prompts:

  • "Explain the volatility smile in this protocol"
  • "I'm bullish on ETH — show me trade ideas"
  • "How does Smile compare to Panoptic?"
  • "Quiz me on the Greeks"

It sends {spot, chainId, address} with every message, so its pricing and on-chain answers always match what's visibly on screen — the server prices with the same code the payoff builder uses, and reads the same connected wallet address wagmi has.

Turning it on

The Copilot is hidden unless NEXT_PUBLIC_COPILOT=1 is set (it's absent on the static GitHub Pages export, which has no API server to talk to). Even with the flag on, the backend (app/api/copilot/route.ts) needs an LLM provider key to actually answer — set one of these in frontend/.env.local (local.sh preserves them across restarts):

NEXT_PUBLIC_COPILOT=1
COPILOT_PROVIDER=anthropic   # or openai / google / openrouter
# the provider's own API key env var, e.g. ANTHROPIC_API_KEY=...
COPILOT_MODEL=               # optional override; defaults to claude-opus-4-8 / gpt-5-mini / gemini-2.5-pro / openrouter/auto

openrouter is a fourth option: one key, hundreds of models across every major provider, OpenAI-API-compatible so it reuses the same client under the hood (frontend/lib/copilot/provider.ts) pointed at https://openrouter.ai/api/v1. Its default model is openrouter/auto, which lets OpenRouter itself pick a model per-prompt — set COPILOT_MODEL to pin a specific one instead (e.g. anthropic/claude-3.5-sonnet).

Bring-your-own-key is also supported without touching .env.local: the panel's settings gear lets a visitor paste their own Anthropic/OpenAI/Google/ OpenRouter key, stored only in that browser's localStorage and sent per-request via headers — never persisted server-side.

What it can actually do

The Copilot is tool-calling, not free-floating chat — every substantive answer comes from one of these (frontend/lib/copilot/tools.ts):

Tool Does
read_docs Reads a full section of the Overview, Limitations or Solutions pages and cites it.
get_market_state Live ETH spot, smile parameters, ATM vol, expected move, 25-delta risk reversal/butterfly.
price_strategy Prices a multi-leg strategy at the protocol's smile — cost, max P/L, PoP, breakevens, net Greeks; renders a payoff chart.
suggest_strategies Candidate strategies from the catalog for a stated market view, with live-priced strikes.
scenario_analysis Stress-tests a strategy across spot/vol shifts, optionally rolled forward in time.
analyze_adjustment Economics of rolling/modifying an existing position — before/after risk and cash flow.
get_onchain_quote Cross-checks a quote against the deployed pricing engine contract directly (a real eth_call, not the frontend model).
get_positions The connected wallet's balances, LP range authorizations, and long option positions.
portfolio_risk Aggregate Greeks/risk across the connected wallet's long positions, with a stress grid.
propose_trade Renders an interactive trade card with a "Load into Payoff Builder" button — the Copilot never executes trades itself.
quiz_question Asks one interactive multiple-choice question, scored against a real pricing-tool answer.

Tools over the tape — The Graph as the copilot's chain data (EthOnline 2026)

The second set reads the tape: every range, instrument, fill and position, from The Graph on Sepolia and Arc (subgraph/, Studio smile-sepolia / smile-arc-testnet) and, only on the local Anvil chain, from the vault's event log rebuilt into the same shape (frontend/lib/tape.ts). On a public network there is no RPC path: a missing subgraph is an error, not a capped scan. Every result carries source: "subgraph" | "anvil-logs" and the copilot is instructed to say where the numbers came from.

Tool Does Source
find_opportunities Screens every live strike on every active range: Smile's ask as an implied vol vs the nearest listed Deribit instrument's IV, and vs the last fill of the same instrument; free capacity and open interest per row; ranked cheap / expensive. Graph + Deribit
liquidity_map Every active range with capacity, used %, open interest, fills, days since last trade — flags scarce (≥80% used), empty, stale (>3 d), expiring — plus a per-strike heat map and the strikes near spot nobody quotes. "Expensive liquidity" is a query. Graph
portfolio_greeks The wallet's whole book: long positions (the Position entity, cost basis from its own fills) and the written side (open interest on its ranges), net delta/gamma/theta/vega, marks, and legs ready for the next two tools. Graph
hedge_suggestion How much spot ETH, or how many calls/puts at a strike, brings a book to a target delta ("hedge my 3 short puts with short calls"); before/after greeks. math
reference_market Deribit public API: ETH index, the DVOL 30-day vol index, ATM IV at the nearest listed expiry, nearest instrument to a strike/expiry with its mark IV. No key, 60 s cache. Deribit
macro_calendar Upcoming FOMC / US CPI / monthly and quarterly listed expiries (a hardcoded 2026 table) with the event-vol heuristic for each, plus the general ones (event-vol crush, ETH beta, weekend theta, max-pain pinning). static
prepare_lp_range A card proposing a range to write (band, expiry, size, expected premium) whose button prefills Earn · Write a Range; the user signs authorizeRange + Aqua.ship. UI
prepare_rfq_quote A card proposing an RFQ quote (range, strike, size, premium inside the formula ask, ttl) whose button prefills the RFQ signer; the user signs the EIP-712 message in the wallet. UI

The agent prepares; the user signs. No key ever leaves the wallet, and the copilot cannot send a transaction.

get_positions and portfolio_risk read the same tape now: the MAX_AUTHS = 50 cap and the per-strike N+1 RPC scan that used to blind them past 50 ranges (L12a) are gone on public networks.

Skills

The copilot's behaviour is packaged as skillsfrontend/skills/*.md in the SKILL.md convention (frontmatter name, description, starter; the body is what the model follows). The Skills button in the panel header lists them with a toggle and a one-click starter prompt, and lets you add your own (a name and a markdown body, kept in this browser only) — so "calendar spreads", which the strategy catalog does not have, is just a skill, and so is anything you want the copilot to do your way. Enabled skills ride each request and are appended to the system prompt as "Active skills".

Skill What it teaches the copilot
trading-opportunities screen with find_opportunities + reference_market, confirm with price_strategy, present with propose_trade; what "cheap" means; sanity checks
risk-management portfolio_greeksscenario_analysis → limits (max loss vs balance, gamma near expiry, vega vs DVOL); when to roll
delta-hedging net delta × spot, hedge_suggestion with spot or options, re-hedge triggers, the gamma caveat
explain-margin the margin tier, the liquidation waterfall, what the Risk Monitor shows, "why was I liquidated"
lp-market-making liquidity_map → empty/scarce bands → size vs collateral → expected premium → prepare_lp_range; adverse-selection risks
rfq-quoting recent fills and IV for an instrument → a quote inside the formula ask → prepare_rfq_quote; ttl/nonce hygiene
macro-context macro_calendar + heuristics, combined with find_opportunities; labelled as heuristics
calendar-spreads same strike, two expiries with per-leg expiryDays; term structure; theta/vega reading

MCP servers

The settings gear has an MCP servers list (name, URL, bearer token — kept in this browser, sent per request in a header the same way the BYOK key is). The copilot opens each server for the request and merges its tools with the built-ins. One preset: The Graph Subgraph MCP (https://subgraphs.mcp.thegraph.com/sse, token = a Gateway API key from Studio → API Keys) — with it the copilot can query any of The Graph's indexed subgraphs in natural language, not only Smile's own. The operator can seed the same list server-side with COPILOT_MCP_SERVERS (JSON) for a hosted demo.

For developers working on the repo, the same server is a one-file client config: .mcp.json.example at the repo root (Claude Code / Cursor), and the subgraph notes are the agent-facing description of Smile's subgraph — entities, canonical queries, endpoints, units — so an AI environment can query smile-sepolia without reading the schema.

Where the data comes from

Data Source Fallback
Spot, smile parameters, per-leg pricing the same code as the UI (lib/options.ts) and the deployed pricing engine (get_onchain_quote)
Ranges, instruments, fills, positions The Graphsmile-sepolia on Sepolia, smile-arc-testnet on Arc (recorded per chain in lib/deployments.ts; SUBGRAPH_URL server-side overrides with a gateway URL carrying an API key, proxied through /api/subgraph so the key never reaches the browser) Anvil only: the vault's event log rebuilt into the same entities
Listed reference vol Deribit public API tool reports "unreachable"; edge is then measured against the protocol's flat ATM vol
Macro dates hardcoded 2026 table
Anything else MCP servers you add

1inch Aqua and SwapVM in Smile

Summary

Smile is a non-custodial marketplace for fixed-expiry, cash-settled European options on ETH. Every option sold on Smile is backed by collateral that a liquidity provider (LP) has committed through 1inch Aqua, a shared-liquidity layer in which a maker's tokens stay in the maker's own wallet until the moment a trade actually needs them. Smile's pricing for covered calls runs inside 1inch SwapVM, a small on-chain virtual machine that executes a maker's strategy as a program of opcodes; Smile adds one custom opcode that prices an option.

This page collects everything Smile builds on Aqua and SwapVM: the original vault from before EthOnline 2026, and the three sibling vaults built at the event. Each sibling vault reuses the identical custody model and changes exactly one thing: how much collateral a spread locks (SpreadVault), how much margin a put writer posts (MarginVault), or who sets the price (RfqVault). The main vault's bytecode was never modified during the event.

Terms are defined at first use, and a glossary closes the page.

Features used

Feature Where in the code Provenance
Aqua JIT pull custody: LP ships a strategy, collateral is pulled from the LP wallet only when a buyer fills src/vaults/AquaCollateralVault.sol (execPutLeg, _putStrategy, getShipParams) pre-existing
AquaApp base contract and the nonReentrantStrategy guard around every pull every vault under src/vaults/ and src/periphery/ pre-existing; reused by the three event vaults
SwapVM custom opcode 33, OptionPremiumInstruction, dispatched by SmileSwapVMRouter src/swapvm/OptionPremiumInstruction.sol, src/swapvm/SmileSwapVMRouter.sol pre-existing
Official SwapVM fee opcode grossed up on top of the Ask for call fills AquaCollateralVault.buildOrder (FeeArgsBuilder.buildProtocolFee) pre-existing
Two-sided quote: forward swap direction is the Ask, reverse direction is the Bid (close() sellbacks) OptionPremiumInstruction.sol, AquaCollateralVault.close pre-existing
FirmEscrow: a maker wallet that cannot renege on shipped depth (S4 firm tier, MVP scope) src/periphery/FirmEscrow.sol pre-existing
SmilePremiumLib: the vault's premium math lifted into a library with an isCall flag src/periphery/SmilePremiumLib.sol EthOnline 2026
SpreadVault: credit spreads escrow their true maximum loss (S12) src/periphery/SpreadVault.sol, SpreadToken.sol, test/SpreadVault.t.sol (11), test/SpreadSettlement.t.sol (10) EthOnline 2026
MarginVault + MarginBackstop: opt-in margined puts with a liquidation waterfall (S13) src/periphery/MarginVault.sol, MarginBackstop.sol, test/Margin*.t.sol (54) EthOnline 2026
RfqVault: LP-signed EIP-712 quotes that settle through the same Aqua pull (R6) src/periphery/RfqVault.sol, test/RfqVault.t.sol (8) EthOnline 2026
Frontend tabs: Spreads · Defined Risk, Margin · Opt-in Puts, RFQ · Signed Quotes, Risk Monitor frontend/components/SpreadDesk.tsx, MarginDesk.tsx, RfqDesk.tsx, RiskMonitor.tsx EthOnline 2026
Lifecycle demos and keeper script/spread-lifecycle.sh, script/margin-lifecycle.sh, script/rfq-lifecycle.sh, keeper/margin.mjs EthOnline 2026

The Foundry suite grew from 82 tests before the event to 200 at the time of writing, all passing.

Why it is necessary

The problem Aqua solves. On-chain options venues before Smile fell into two families. DeFi Option Vaults (DOVs) such as Ribbon and Friktion lock collateral into a vault at a strike chosen by the vault manager; every other strike on the chain sits empty, and the locked capital earns nothing while it waits. Request-for-quote venues such as Premia rely on institutional market makers to stream prices off-chain, which reintroduces a dependency on a handful of counterparties. Neither produces a full, standing options chain with real depth at every strike.

Aqua changes the cost structure of quoting. An LP on Smile does not deposit anything. The LP authorizes a range (a span of strikes at one expiry) and ships an Aqua strategy that describes it. The LP's WETH or USDC remains in the LP's wallet, lendable and yield-bearing, until a taker buys a specific strike. At that moment the vault performs a just-in-time (JIT) pull of exactly the collateral that one fill needs. Quoting an entire strike chain therefore costs an LP nothing in idle capital, which is the precondition for the deep, wide market that standard options require.

The problem SwapVM solves. A covered call is a swap of premium (USDC) for collateral (WETH), so it fits SwapVM's shape exactly: the taker sends USDC, the maker's WETH is pulled through Aqua and escrowed by the vault, and an OptionToken is minted to the taker. Rather than build a separate quoting contract, Smile expresses the option price as a SwapVM instruction so that the price is computed atomically inside the same swap that executes it. There is no off-chain quote to go stale or to be front-run.

The problem the sibling vaults solve. Full collateralization was a deliberate choice (see L8 below), but it is expensive for the users Smile is built for. A 3000/3200 call credit spread has a worst case of 200 USD per unit, yet the main vault margins the short leg as if it were naked and locks a full 1 WETH. A cash-secured put writer must post the entire strike even when the put is far out of the money. Professional makers with their own pricing models had no way to quote inside the protocol's formula. The three event vaults address exactly these three gaps without touching the promise the main vault makes.

Market value add

For LPs (option writers).

  • Collateral stays in the LP's wallet and keeps earning until a fill. No other options venue, on-chain or off-chain, lets a maker quote a chain with capital that is simultaneously deployed elsewhere.
  • SpreadVault escrows a 3000/3200 call credit spread at 0.0625 WETH per unit instead of 1 WETH, a 16× reduction; the put-credit twin escrows 200 USDC instead of 3,200.
  • MarginVault lets an at-the-money 3000 put writer lock 1,500 USDC of initial margin instead of the 3,000 USDC strike, off a conservative worst-of-hour Chainlink mark that the vol surface cannot influence.
  • RfqVault lets a maker with a better model quote inside the formula price and win the flow, while the formula tier remains the public floor. This is the structure tradfi calls the national best bid and offer (NBBO) plus price improvement.

For takers (option buyers).

  • Every strike in an authorized range is quotable, not only the strike a vault manager picked.
  • The price is computed on-chain in the block of execution, with a protocol fee grossed up transparently on top of the Ask.
  • A written option on the main vault and on SpreadVault can always pay: the escrow is the structure's maximum payout to the wei. The margin tier is opt-in and labelled as such.

Against the alternatives. Deribit offers firm depth but custody and margin risk on a centralized venue. Panoptic removes the pricing oracle by streaming fees from Uniswap v3 positions, but has no expiries, no upfront credit, and forced liquidations. Ribbon-style DOVs lock capital at one strike. Smile keeps the instrument traders already know, fixed-expiry vanillas with known premiums, and rebuilds the market-making stack around Aqua so that quoting the whole chain is nearly free.

Technical details

The custody model: ship, then pull under the reentrancy guard

An LP calls authorizeRange on a vault, which records the range and computes a strategy hash. For calls this hash is the hash of a SwapVM order; for puts it is the hash of an abi-encoded terms blob, with the vault itself as the Aqua application. The LP then calls Aqua.ship(app, strategy, tokens, amounts) with the parameters the vault exposes through getShipParams. Nothing moves at ship time.

When a taker buys a put, the vault re-enters itself through an external function so that Aqua's nonReentrantStrategy guard brackets the pull. The premium goes straight from the buyer to the LP, the fee to the fee recipient, and then exactly collateralNeeded is pulled from the LP wallet into the vault.

src/vaults/AquaCollateralVault.sol

    function execPutLeg(
        address lp,
        bytes32 strategyHash,
        address premiumToken,
        address collateralToken,
        address feeRecipient_,
        address buyer,
        uint256 lpPremium,
        uint256 fee,
        uint256 collateralNeeded
    ) external nonReentrantStrategy(lp, strategyHash) {
        require(msg.sender == address(this), SelfOnly());
        if (lpPremium > 0) {
            IERC20(premiumToken).safeTransferFrom(buyer, lp, lpPremium);
        }
        if (fee > 0) {
            IERC20(premiumToken).safeTransferFrom(buyer, feeRecipient_, fee);
        }
        AQUA.pull(lp, strategyHash, collateralToken, collateralNeeded, address(this));
    }

The vault requires calls and puts to use different token pairings, and this single line is why calls go through SwapVM while puts pull directly: a call swaps two different tokens (USDC in, WETH out), which is a swap; a put is USDC in and USDC out, which is not.

src/vaults/AquaCollateralVault.sol

        require(isCall ? collateralToken != premiumToken : collateralToken == premiumToken, BadTokenPair());

The SwapVM opcode: pricing inside the swap

SmileSwapVMRouter is a SwapVM with the official Aqua opcode set (indices 0 through 32) plus one custom instruction at index 33. The dispatcher tries the custom opcode first and falls through to the official set otherwise.

src/swapvm/SmileSwapVMRouter.sol

contract SmileSwapVMRouter is Simulator, SwapVM, AquaOpcodes, OptionPremiumInstruction {
    /// @notice Opcode index of the custom option-premium instruction.
    /// Official AquaOpcodes occupy indices 0–32; custom instructions start at 33.
    uint256 public constant OPCODE_OPTION_PREMIUM = 33;

    constructor(
        address aqua,
        address weth,
        address owner
    ) SwapVM(aqua, weth, owner, "SmileSwapVM", "1") AquaOpcodes(aqua) {}

    /// @dev Dispatch custom opcodes first, then fall through to the official set.
    function _dispatch(Context memory ctx, uint256 opcode, bytes calldata args) internal override {
        if (opcode == OPCODE_OPTION_PREMIUM) {
            OptionPremiumInstruction._optionPremiumXD(ctx, args);
        } else {
            AquaOpcodes._runOpcode(ctx, opcode, args);
        }
    }
}

The instruction itself, _optionPremiumXD, reads the swap direction, the taker's strike, the oracle spot with a staleness bound, and the live sigma from the Uniswap v4 hook, then prices the trade in whichever of the four exact-in / exact-out branches applies (the pricing branches are elided here; the full function is in the source file).

src/swapvm/OptionPremiumInstruction.sol

    /// The instruction is TWO-SIDED — direction selects the quote side:
    ///   forward (premium in  → collateral out): buyer opens  → Ask (rounds against taker)
    ///   reverse (collateral in → premium out):  holder closes → Bid (rounds against taker)
    /// One shipped strategy therefore quotes a full two-sided market.
    function _optionPremiumXD(Context memory ctx, bytes calldata args) internal view {
        OptionTerms memory terms = _parseArgs(args);

        QuoteVars memory v;
        if (ctx.query.tokenIn == terms.premiumToken && ctx.query.tokenOut == terms.collateralToken) {
            v.forward = true;
        } else if (ctx.query.tokenIn == terms.collateralToken && ctx.query.tokenOut == terms.premiumToken) {
            v.forward = false;
        } else {
            revert OptionPremiumWrongTokenPair(ctx.query.tokenIn, ctx.query.tokenOut);
        }
        require(block.timestamp < terms.expiry, OptionPremiumExpired(terms.expiry, block.timestamp));

        v.strike = _takerStrike(ctx, terms);
        (v.spot, v.ageSec) = _oracleSpotWad(terms.oracle, terms.maxStaleness);
        v.timeToExpiry = terms.expiry - block.timestamp;
        // Live vol surface: σ per tenor from the sigma source, skewed per strike.
        uint256 sigmaTenor = terms.sigmaSource != address(0)
            ? ISigmaSource(terms.sigmaSource).sigmaFor(v.timeToExpiry)
            : DEFAULT_SIGMA;
        // S5: LP-quoted vol — the maker's own multiplier on the tenor σ
        // (1e4 = 1.0x; 0 = take the protocol surface as-is). Competing ranges
        // with different multipliers form an order book in vol space.
        if (terms.sigmaMulBps != 0) {
            sigmaTenor = (sigmaTenor * terms.sigmaMulBps) / BPS_DENOM;
        }
        v.sigmaStrike = SmileMath.smileVol(v.spot, v.strike, sigmaTenor, terms.alpha, terms.beta);

        if (v.forward) {
            if (ctx.query.isExactIn) {
        // ... four branches: forward exact-in / exact-out price at the Ask (rounds
        // against the taker), reverse exact-in / exact-out price at the Bid.
    }

The maker's packed arguments carry the oracle, the sigma source (the Uniswap v4 hook), the token pair, the strike range, the expiry, the smile parameters alpha and beta, and the adverse-selection defenses (a staleness-scaled spread, a size-convex impact term, and an LP vol multiplier). The taker selects the exact strike at swap time. The swap direction selects the side of the market: premium in and collateral out is an opening trade priced at the Ask, which rounds up; collateral in and premium out is a sellback priced at the Bid, which rounds down.

The protocol fee on a call fill is an official SwapVM opcode placed in front of the pricing instruction, guarded by a jump so the fee applies only to the opening direction.

src/vaults/AquaCollateralVault.sol

        if (auth.feeBps > 0) {
            bytes memory jumpArgs = ControlsArgsBuilder.buildJumpIfToken(auth.collateralToken, PC_AFTER_FEE);
            bytes memory feeArgs = FeeArgsBuilder.buildProtocolFee(auth.feeBps, auth.feeRecipient);
            program = abi.encodePacked(
                program,
                uint8(11), uint8(jumpArgs.length), jumpArgs,            // Controls._jumpIfTokenIn
                uint8(28), uint8(feeArgs.length), feeArgs               // Fee._aquaProtocolFeeAmountInXD
            );

The shared premium library

At the event the vault's premium math was lifted into SmilePremiumLib so that every sibling vault prices off the same surface. The Ask rounds up and grosses the fee up on top; the Bid rounds down and carries no fee.

src/periphery/SmilePremiumLib.sol

    function quote(Terms memory t, uint256 amountWad, bool isBuy, uint8 premiumDecimals, uint32 feeBps)
        internal
        view
        returns (uint256 lpPremium, uint256 fee)
    {
        uint256 unit = unitPremiumWad(t, amountWad, isBuy);
        if (isBuy) {
            uint256 totalWad = Math.ceilDiv(unit * amountWad, 1e18);
            lpPremium = SmileMath.scaleFromWad(totalWad, premiumDecimals, true);
            fee = feeBps > 0 ? Math.ceilDiv(lpPremium * feeBps, BPS - feeBps) : 0;
        } else {
            uint256 totalWad = (unit * amountWad) / 1e18;
            lpPremium = SmileMath.scaleFromWad(totalWad, premiumDecimals, false);
        }
    }

SpreadVault: escrow the true maximum loss

A credit spread is a short option plus a long option at the same expiry, where the long leg caps the loss. SpreadVault prices the taker's side as the long leg at the Ask minus the short leg at the Bid, and escrows only the structure's maximum loss: (K2 − K1) / K2 WETH per unit for a call credit spread, K2 − K1 USDC for a put credit spread. Both round in the writer's favor (Ceil).

src/periphery/SpreadVault.sol

        if (s.kind == Kind.CallCredit) {
            // Taker: long the K1 call at Ask, short the K2 call at Bid.
            (ask,) = SmilePremiumLib.quote(_terms(authId, s.strikes[2], true), units, true, usdcDecimals, 0);
            (bid,) = SmilePremiumLib.quote(_terms(authId, s.strikes[3], true), units, false, usdcDecimals, 0);
            // (K2-K1)/K2 WETH per unit.
            escrow = Math.mulDiv(units, s.strikes[3] - s.strikes[2], s.strikes[3], Math.Rounding.Ceil);
        } else if (s.kind == Kind.PutCredit) {
            // Taker: long the K2 put at Ask, short the K1 put at Bid.
            (ask,) = SmilePremiumLib.quote(_terms(authId, s.strikes[1], false), units, true, usdcDecimals, 0);
            (bid,) = SmilePremiumLib.quote(_terms(authId, s.strikes[0], false), units, false, usdcDecimals, 0);
            // K2-K1 USDC per unit.
            escrow = SmileMath.scaleFromWad(Math.ceilDiv(units * (s.strikes[1] - s.strikes[0]), 1e18), usdcDecimals, true);
        }

The pull has the same shape as the main vault's put leg, and pulls exactly the netted escrow.

src/periphery/SpreadVault.sol

    function execPull(
        address lp,
        bytes32 strategyHash,
        address buyer,
        address feeRecipient_,
        uint256 premium,
        uint256 fee,
        address collateralToken,
        uint256 escrow
    ) external nonReentrantStrategy(lp, strategyHash) {
        require(msg.sender == address(this), SelfOnly());
        IERC20(usdc).safeTransferFrom(buyer, lp, premium);
        if (fee > 0) {
            IERC20(usdc).safeTransferFrom(buyer, feeRecipient_, fee);
        }
        AQUA.pull(lp, strategyHash, collateralToken, escrow, address(this));
    }

Settlement uses one price and one floored payout formula whose maximum over the settlement price equals the escrow exactly, so holder payout + writer reclaim == escrow to the wei. test/SpreadSettlement.t.sol fuzzes this conservation over 256 settlement prices. The Anvil lifecycle at a 3,100 settlement pulled 62,500,000,000,000,000 wei, paid holders 32,258,064,516,129,032, and returned 30,241,935,483,870,968 to the writer.

MarginVault: initial margin, then a waterfall

MarginVault is puts-only and USDC-only, so margin, premium, penalties, the backstop and the insurance fund are all one token and the waterfall needs no swap. The margin requirement reads the Chainlink oracle only, never the vol hook: intrinsic value plus a spot buffer of 50% for initial margin (IM) or 30% for maintenance margin (MM), capped at the strike. The mark is the lowest Chainlink answer in the last hour.

src/periphery/MarginVault.sol

    function marginRequirement(uint256 strike, uint256 units, uint256 spotWad, bool initial)
        public
        view
        returns (uint256)
    {
        uint256 cap = (strike * units) / 1e30;
        uint256 intrinsic = spotWad < strike ? ((strike - spotWad) * units) / 1e30 : 0;
        uint256 buffer = (units * spotWad * (initial ? imBufferBps : mmBufferBps)) / 1e4 / 1e30;
        uint256 req = intrinsic + buffer;
        return req > cap ? cap : req;
    }

The contract's own worked example: strike 3000, one unit, spot 3000 gives IM 1,500 and MM 900; spot 2,000 gives 2,000 and 1,600; spot 0 gives 3,000 and 3,000.

When a position falls below maintenance it is flagged. The vault first sweeps the writer's free balance and, if the writer opted in, an Aqua credit line bounded by what is still shipped, in the wallet, and approved. After a one-hour grace period a 30-minute takeover auction opens with a bonus rising linearly from 1% to 10% of notional; a bidder posts fresh margin and the holder's token is untouched. Whatever nobody buys is absorbed by the backstop pool.

src/periphery/MarginVault.sol

    function absorb(bytes32 sid, address writer) external nonReentrant returns (uint256 drawn) {
        Position storage pos = positions[sid][writer];
        require(pos.auctionStart != 0, AuctionNotStarted());
        require(block.timestamp >= pos.auctionStart + AUCTION_LENGTH, AuctionNotOver());
        Series storage s = seriesOf[sid];
        require(block.timestamp < s.expiry, Expired());

        uint256 notional = (s.strike * pos.units) / 1e30;
        (uint256 spot,,) = markSpot();
        uint256 mm = marginRequirement(s.strike, pos.units, spot, false);
        uint256 tip = (notional * KEEPER_TIP_BPS) / 1e4;
        uint256 penalty = (notional * PENALTY_BPS) / 1e4;

        (uint256 moved, uint256 seed) = _detach(writer, pos, mm + tip + penalty, tip, penalty, msg.sender);
        accounts[msg.sender].free += tip > moved ? moved : tip;

        if (seed < mm) {
            drawn = backstop.draw(mm - seed);
            s.backstopDrawn += drawn;
        }
        uint256 units = pos.units;
        _attach(sid, address(backstop), units, seed + drawn);
        backstop.noteRequirement(notional, true);
        _closeOut(sid, writer, pos);
        emit Absorbed(sid, writer, units, moved, drawn, tip, penalty);
    }

Exposure is capped in the manner of MakerDAO's debt ceiling: naked notional may never exceed seven times the backstop pool's assets. The constant is seven rather than ten because a writer sitting exactly at maintenance who gaps 40% before settlement leaves a shortfall of one seventh of naked notional, and the gap-40 solvency test holds holders whole at exactly that multiple.

src/periphery/MarginVault.sol

    uint256 public constant BACKSTOP_MULTIPLE = 7;

Settlement is two-step: a per-writer waterfall, then one series finalization that draws the backstop, then the insurance fund, and only then imposes a pro-rata holder haircut, emitted loudly, with the IM buffer ratcheting up 500 basis points.

src/periphery/MarginVault.sol

        if (shortfall > 0) {
            s.haircutBps = uint16((shortfall * 1e4) / owed);
            uint16 im = imBufferBps + HAIRCUT_RATCHET_BPS > 1e4 ? 1e4 : imBufferBps + HAIRCUT_RATCHET_BPS;
            imBufferBps = im;
            emit HolderHaircut(sid, owed, s.pot, s.haircutBps, im);
        }

MarginBackstop is a share-based USDC pool with a 24-hour withdrawal delay; withdrawals cannot drop below the pool's standing requirement and freeze while any expired series is unfinalized. The Anvil lifecycle fills a put locking 1,500 USDC, crashes spot to 2,000, flags, auctions, absorbs drawing only 175 USDC, settles at 2,000, and pays the holder exactly 1,000 USDC of intrinsic.

The six stages the Anvil lifecycle script (script/margin-lifecycle.sh) runs:

flowchart TD
    S1["1 · Writer opens a margined put range<br/>authorizes IM capacity, ships to Aqua — nothing locked yet"]
    S2["2 · Holder buys 1 put, $3,000 strike<br/>only initial margin ~$1,500 pulled JIT through Aqua<br/>(the main vault would cash-secure the full $3,000)"]
    S3["3 · ETH crashes to $2,000<br/>put is $1,000 in-the-money · maintenance $1,600 > $1,500 locked<br/>keeper flags the position"]
    S4["4 · One hour grace to add margin<br/>writer does not, so the auction opens"]
    S5{"5 · Waterfall — who covers the gap?<br/>writer margin, then bidder, then backstop, then insurance"}
    T["Takeover: a bidder assumes the short,<br/>posts full margin, earns a 1 to 10% bonus<br/>out of the liquidated writer's margin"]
    B["Absorb: no bidder in 30 min, so the backstop pool<br/>takes the short, drawing only the shortfall<br/>then the insurance fund, then a holder haircut"]
    S6["6 · Expiry at $2,000<br/>settle off Chainlink, finalize,<br/>holder redeems $1,000 intrinsic in cash"]
    S1 --> S2 --> S3 --> S4 --> S5
    S5 -->|a bidder appears| T
    S5 -->|nobody bids| B
    T --> S6
    B --> S6

RfqVault: a signed price, the same custody

RfqVault implements the hybrid request-for-quote (RFQ) tier. The LP ships a range exactly as on tier 1 and then signs EIP-712 typed-data quotes off-chain, with no gas and from any pricing model. A taker submits the quote and signature to fill, which checks the range, the size cap, the time-to-live and the single-use nonce, recovers the signer, and only then pulls collateral through the vault's Aqua strategy. The event records the formula price alongside the signed price so the improvement is auditable on-chain.

src/periphery/RfqVault.sol

        require(amount <= q.maxAmount, OverQuoteSize());
        require(block.timestamp <= q.ttl, QuoteExpired());
        require(!nonceUsed[r.lp][q.nonce], QuoteUsed());

        bytes32 digest = quoteHash(q);
        address signer = ECDSA.recover(digest, signature);
        require(signer == r.lp, BadSigner(signer, r.lp));
        nonceUsed[r.lp][q.nonce] = true;

        (uint256 lpPremium, uint256 fee) = fillCost(q, amount);
        premiumPaid = lpPremium + fee;
        require(premiumPaid <= maxPremium, PremiumAboveMax());

        uint256 collateral = r.isCall ? amount : (q.strike * amount) / 1e30;
        this.execPull(r.lp, r.strategyHash, msg.sender, r.feeRecipient, r.premiumToken, r.collateralToken, lpPremium, fee, collateral);

The quote type is Quote(uint256 authId,uint256 strike,uint256 maxAmount,uint256 premiumPerUnit,uint256 ttl,uint256 nonce). Nonces are cancellable by the LP at any time. There is deliberately no close() in this vault, so holders are never captive to a market maker's uptime; sellbacks stay on tier 1. The Anvil lifecycle shows a formula Ask of 691.93 USDC beaten by a signed quote of 685.01, 1 WETH pulled just-in-time at the fill, and the second fill of the same nonce reverting with QuoteUsed. On Arc testnet a real-USDC signed fill executed at 0.688860 against a 0.695819 formula Ask.

Limitations

The relevant entries in Limitations are:

  • L8, full collateralization is capital-inefficient on purpose. The main vault locks 1 WETH per call unit and the full strike per put unit. Aqua softens this because the collateral is unrehypothecated and keeps earning until the fill, but it does not remove it. SpreadVault and MarginVault are the two rungs of the capital-efficiency ladder built to address it.
  • L11, Aqua liquidity is soft. Because the balance behind a quote sits in the LP's own wallet, it can be spent or de-approved before the fill, and the JIT pull then reverts. Displayed depth is indicative, not firm. FirmEscrow (S4, MVP scope) makes a range firm by becoming the LP's wallet from Aqua's point of view; honest depth display and firmness bonds are the other mitigations (S1 through S3).
  • L7, the demand-feedback loop is nudgeable. Sigma moves with every trade. This is why MarginVault reads margin off the oracle only and has no close(): a buyback priced off sigma and paid from margin would let a manipulated sigma drain margin.
  • L4, one transaction can drain a whole range. The main vault carries R1's per-authorization block cap (maxBlockNotional, checked in AquaCollateralVault.buy), which bounds the drain per block. The three sibling vaults do not: a single fill on SpreadVault, MarginVault or RfqVault can pull an authorization's entire remaining collateral at one price. Per-range block caps were on MarginVault's plan and were cut; there the loss per event is bounded by the range's maxCollateral.
  • L5, on-chain rules cannot reject informed traders. Every check a vault performs is public and can be simulated before submission, so no vault can filter informed flow; the remedy is pricing (R1 through R4), which is why the formula tier carries a spread and a staleness charge rather than an allow-list.
  • L10, the off-chain alternative has its own price. RfqVault is the R6 hybrid precisely because a pure RFQ market gives up the passive-LP thesis, composability, and permissionless quoting: a signed quote is frozen for its time-to-live while the market moves, and makers respond with shorter TTLs and last look. RfqVault keeps quotes single-use, keeps the formula tier as the always-live floor, and offers no close() so holders are never captive to a market maker's uptime; it does not make the quote-fading problem disappear for the maker who signs.
  • L12, the per-trade gas floor. A repeat fill costs about 198,000 gas, in the range of an ordinary Uniswap v3 swap; the first fill in a series costs about 1,040,000 because it deploys that series' ERC-20 token. The premium arithmetic is a rounding error inside that; the floor is series bootstrapping, and EIP-1167 clones or keeper pre-deployment are the mitigations. Aqua's pull adds one external call per fill and does not change this picture.
  • L13, bad debt in the opt-in margin tier. The haircut path exists. The seven-times ceiling and the gap-40 test bound it, they do not eliminate it: a gap larger than 40% from the maintenance point within one heartbeat can exceed the pool. The credit line is consent, not collateral. Takeover and absorb move whole positions only.
  • Scope cuts recorded in the code. SpreadVault validates iron-condor strikes but does not price or fill them. MarginVault covers puts only; a call shortfall is in WETH and has no USDC waterfall yet. A full liquidation run has been demonstrated on Anvil only, since public testnets cannot be time-warped; the fill itself is live on Sepolia and Arc.

Plans

The entries in Solutions and the plan:

  • S12, defined-risk netting. Implemented at the event as SpreadVault (tasks A1 through A4). Task A5, debit spreads collateralized by the long OptionToken under the dominance result, was optional in the plan and was cut first. Iron condor pricing (max-not-sum escrow of the two sides) is designed and strike-validated but not yet fillable. The optional SwapVM opcode for the call-credit leg was scoped and not attempted; a correct SwapVM-free SpreadVault is still an Aqua app.
  • S13, MarginVault. Implemented as tasks B1 through B8. Remaining items in the plan's cut order: partial-unit takeover, per-range block caps (maxBlockNotional, R1), and calls once a WETH shortfall can be paid.
  • R6, hybrid RFQ. Implemented as RfqVault (task A6, beyond the original plan). The interpolation of sigma across tenor buckets, which the README's design note defers to an RFQ-style quoting layer, is a natural next step now that a signed-quote path exists.
  • S4, the firm tier. FirmEscrow ships the MVP (plain collateral, firm Ask depth). The full version, yield-bearing escrow in wstETH and sDAI, is gated on fill-reliability data (S3) and firm-tier uptake.
  • S10, distribution through the 1inch ecosystem, remains a plan item and is not addressed by any code on this branch.

Glossary

  • Aqua. 1inch's shared-liquidity protocol. A maker registers a strategy with an application and keeps the tokens in the maker's own wallet; the application may pull tokens against that strategy when a trade executes.
  • AquaApp. The base contract an application inherits to interact with Aqua. Every Smile vault is an AquaApp; for puts and for the three sibling vaults, the vault itself is the application.
  • Strategy hash. The identifier under which Aqua tracks a maker's shipped balances for one strategy. For Smile calls it is the hash of the SwapVM order; for puts and the sibling vaults it is the hash of an abi-encoded terms blob.
  • Ship / dock. Aqua.ship activates a strategy and declares the tokens and amounts the maker commits to it; Aqua.dock deactivates it. Neither moves tokens.
  • JIT pull. A just-in-time Aqua.pull that moves collateral out of the maker's wallet only in the transaction that needs it. The collateral is unrehypothecated: it was never lent or reused while it waited.
  • nonReentrantStrategy. Aqua's reentrancy guard, keyed on the maker and strategy hash, that brackets every pull.
  • SwapVM. 1inch's on-chain virtual machine that executes a maker's order as a program of opcodes.
  • Opcode / instruction. One step of a SwapVM program. The official Aqua set occupies indices 0 through 32; Smile's OptionPremiumInstruction is index 33.
  • Order. The SwapVM structure a maker signs or hashes that carries the program and its arguments. Smile's call strategies are orders.
  • Ask / Bid. The price at which a taker opens a position (rounded up, fee grossed up on top) and the price at which a taker sells back (rounded down, no fee). One strategy quotes both; the swap direction selects the side.
  • Escrow. Collateral held by a vault against a written option. On the main vault and SpreadVault the escrow equals the option's maximum payout.
  • Credit spread. A short option plus a long option at the same expiry where the writer receives net premium; the long leg caps the writer's loss.
  • Initial margin (IM) / maintenance margin (MM). The collateral a MarginVault writer posts at the fill, and the floor below which liquidation begins.
  • Worst-of-hour mark. The lowest Chainlink answer in the last hour, used by MarginVault as the price for margin arithmetic.
  • Naked notional. Strike times units minus the margin locked at the fill; the exposure the backstop must be able to absorb.
  • Backstop. MarginBackstop, a pre-funded, share-based USDC pool that adopts positions nobody bought at auction and pays the residual shortfall at finalization.
  • Insurance fund. A balance inside MarginVault funded by half of the protocol fee and by liquidation penalties, drawn after the backstop.
  • Waterfall. The fixed order in which a holder is made whole: writer margin, writer free balance and credit line, takeover bidder, backstop, insurance, and finally a haircut.
  • Haircut. Holders receive less than owed, pro rata, because every earlier layer of the waterfall was exhausted; HolderHaircut is emitted and the IM buffer is raised.
  • RFQ (request-for-quote). A model in which a maker signs short-lived prices off-chain and the chain verifies and settles them.
  • EIP-712. The Ethereum standard for signing typed, human-readable structured data; RfqVault quotes are EIP-712 messages.
  • Nonce. A single-use number inside a quote that prevents the same quote from being filled twice; the LP can cancel a nonce at any time.
  • TTL (time to live). The timestamp after which a signed quote is no longer fillable.
  • NBBO plus price improvement. The tradfi structure in which a public best price is the floor and competing makers may beat it; the model for the RFQ tier.
  • Firm versus indicative depth. Firm depth is guaranteed to fill; indicative depth is an intention that may fail at fill time. Aqua depth is indicative; FirmEscrow depth is firm.
  • EIP-170. The 24,576-byte contract size limit; the reason the sibling vaults are separate contracts rather than additions to the main vault.

Uniswap in Smile

Summary

Smile uses Uniswap in two roles. On-chain, a Uniswap v4 hook named OptionPricingHook holds the protocol's implied-volatility surface: the volatility number that every option premium is computed from lives in the hook, is read live by the pricing path at the moment of each trade, and is moved up or down by order flow. Off-chain, the Uniswap Trading API gives the application its live ETH/USD spot price and builds the buyer's ETH-to-USDC premium swap through the Universal Router, so a buyer holding only ETH can pay a USDC premium in one flow.

All Uniswap code in the repository predates EthOnline 2026. The hook was scaffolded on 2026-06-14, the Trading API integration landed the same day, and the multiparameter surface (per-tenor buckets and skew) landed on 2026-07-07; the pre-event baseline commit is dated 2026-09-05. No new Uniswap code was written at the event. What the event added is consumers of the surface: the three new vaults (SpreadVault, MarginVault, RfqVault) read the hook's sigma through the shared SmilePremiumLib, and the AI copilot's find_opportunities tool reads the hook's live sigma per expiry so that its screening prices agree with what the chain would charge.

Terms used below are defined at first use and collected in the glossary at the end.

Features used

Feature Where in the code Pre-existing or EthOnline 2026
Uniswap v4 hook implementing IHooks; beforeSwap vetoes mispriced secondary-market swaps, afterSwap shifts the whole surface src/hooks/OptionPricingHook.sol pre-existing (2026-06-14, surface 2026-07-07)
Per-tenor sigma buckets and sigmaFor(timeToExpiry), the live sigma source for every quote OptionPricingHook.sigmaBuckets, sigmaFor, _bucketOf pre-existing
Smile multiplier with curvature alpha and skew beta src/swapvm/SmileMath.sol smileVol; hook.beta, setBeta pre-existing
Demand feedback: bumpSigma on every buy() and close(), gamma = 0.5 vol points OptionPricingHook.bumpSigma, AquaCollateralVault.sol:553 and :829 pre-existing
Sigma snapshot into each maker strategy at authorization time AquaCollateralVault.sol:299 (auth.sigmaSource = address(hook)) pre-existing
SwapVM instruction reads the hook inside the same swap that buys or sells src/swapvm/OptionPremiumInstruction.sol:163 pre-existing
Hook-to-vault wiring at deploy time script/Deploy.s.sol:239-240 pre-existing
Live ETH/USD spot from the Uniswap Trading API, with Chainlink and static fallbacks frontend/hooks/useUniswapSpot.ts pre-existing
Buyer's ETH-to-USDC premium swap built by the Trading API and sent to the Universal Router frontend/hooks/useUniswapTrade.ts, frontend/components/OptionMatrix.tsx:562 pre-existing
Trader-native reading of the surface: ATM vol, 25-delta risk reversal, 25-delta butterfly frontend/lib/options.ts surfaceQuotes pre-existing
Vol Surface tab: a rendered 3-D surface that applies the same bump per trade frontend/components/VolSurface.tsx, volsurface/server.py pre-existing
Sibling vaults quote off the same hook through the shared library src/periphery/SmilePremiumLib.sol (ISigmaSource.sigmaFor) EthOnline 2026
Copilot screener prices off the hook's live sigma per expiry frontend/lib/copilot/graphTools.ts liveSigmaByExpiry EthOnline 2026
Thirteen hook tests (tolerance veto, bump direction, bucket selection, skew, access control) test/OptionPricingHook.t.sol pre-existing

Why it is necessary

An options market needs two things a spot exchange does not: a price for volatility, and a venue in which to hedge.

A price for volatility. An option premium is mostly a bet on how much the underlying will move. The number that encodes that bet is the implied volatility (sigma), an annualised standard deviation of returns expressed as a percentage. A venue that fixes sigma by fiat cannot respond to demand: if everyone buys calls, the premium should rise, as it would on any exchange where a market maker widens and lifts quotes under one-sided flow. Smile therefore stores sigma on-chain and lets trades move it. The hook is the natural home for that state because Uniswap v4 hooks are contracts that the pool manager calls at fixed points in a swap's lifecycle, so the same contract can both observe secondary-market flow (afterSwap) and serve the primary market's pricing path (sigmaFor).

A venue to hedge in. Traditional options markets function because broker-dealers delta-hedge: they offset the directional exposure of the options they have written by trading the underlying. Without a liquid spot venue, a market maker cannot lay off risk, and quotes stay wide or absent. Uniswap is that venue for Smile. The README states the aim directly: the trading and settlement functionality provided by Uniswap and Chainlink should let capable liquidity providers and arbitrageurs continuously arbitrage away mispricings between options and the underlying.

Why the lookup is on-chain and stepwise. Smile chose an on-chain step lookup over an off-chain quoting service. sigmaFor is read atomically inside the same transaction that buys or sells, so the price a trader receives is exactly what the bucket held at that block; there is no off-chain quote to go stale or be front-run, and no quoting service that must stay online. The cost is that sigma is a step function of time to expiry with edges at 7, 30 and 90 days. The README's design note records the alternative (an RFQ-style signed quote, verified on-chain much as a CRE report is) and the reason it was deferred: blending feedback across neighbouring buckets would let a trade at a bucket edge nudge a bucket nothing traded in.

Market value add

Price impact for volatility. On a spot exchange, a large buy walks the book and the next buyer pays more. Smile's feedback loop gives options the same property: each buy raises the traded tenor's sigma by gamma, each sellback lowers it, so persistent one-sided demand steepens the surface and raises premiums. Higher premiums attract sellers and arbitrageurs, whose sellbacks bring sigma back down. This is the mechanism by which a market with no designated market maker can still discover a volatility level.

Emergent market makers. The README's glossary defines an emergent market maker as an arbitrageur who buys underpriced options at the Ask, delta-hedges on Uniswap, and sells back at the Bid when sigma corrects, capturing the spread while enforcing consistency between implied volatility and the spot market. Uniswap is the leg of that trade that makes it possible.

A familiar dictionary. The surface's three parameters, tenor sigma, alpha and beta, map exactly onto the level / skew / curvature decomposition that options desks quote to each other: ATM volatility, the 25-delta risk reversal and the 25-delta butterfly. The application computes these three numbers from the actual smile at the actual 25-delta strikes and shows them in the One-Click Income panel with plain-language captions. A Deribit market maker can therefore read Smile's surface risk in the units they already manage.

One-asset checkout. Because the Trading API builds the ETH-to-USDC swap for the exact premium amount (an EXACT_OUTPUT quote), a buyer who holds only ETH sees a single flow: swap, approve, buy. The premium is a USDC amount throughout the protocol; the swap is the on-ramp.

Technical details

The surface: tenor buckets and sigmaFor

Sigma is stored per tenor bucket, that is, per band of time to expiry. Four bands cover the term structure, and sigmaFor returns the band a given expiry falls into. Values are in WAD, the fixed-point convention in which 1e18 represents 1.0, so 0.8e18 is 80% annualised volatility.

src/hooks/OptionPricingHook.sol

/// @dev Tenor cutoffs for the σ term structure.
uint256 public constant TENOR_1 = 7 days;
uint256 public constant TENOR_2 = 30 days;
uint256 public constant TENOR_3 = 90 days;

/// @dev σ per tenor bucket in WAD, adjusted by demand feedback.
uint256[4] public sigmaBuckets;

/// @dev Signed skew β in WAD (0 = symmetric smile; negative = downside skew).
int256 public beta;

/// @dev Demand feedback step: γ = 0.5% per trade (in WAD).
uint256 public constant GAMMA = 0.005e18;

/// @notice σ for a given time-to-expiry — the tenor dimension of the surface.
/// This is the live σ source the SwapVM option-premium instruction queries.
function sigmaFor(uint256 timeToExpiry) public view returns (uint256) {
    return sigmaBuckets[_bucketOf(timeToExpiry)];
}

function _bucketOf(uint256 timeToExpiry) internal pure returns (uint256) {
    if (timeToExpiry < TENOR_1) return 0;
    if (timeToExpiry < TENOR_2) return 1;
    if (timeToExpiry < TENOR_3) return 2;
    return 3;
}

The smile: alpha and beta

The tenor sigma is the volatility at the money, that is, for a strike equal to spot. Strikes away from spot are priced with a multiplier in log-moneyness ln(K/S). Alpha sets the curvature (how much more the wings cost than the centre) and beta sets the skew (which side costs more). The multiplier is floored at 0.1 so that deep wings can never drive sigma to zero. In plain text the formula is sigma_strike = sigma_tenor * max(0.1, 1 + alpha * ln(K/S)^2 + beta * ln(K/S)).

src/swapvm/SmileMath.sol

/// @notice σ_strike = σ · (1 + α · ln(K/S)² + β · ln(K/S))  — smile + skew.
/// β < 0 tilts the surface so low strikes (downside) price richer, matching
/// the empirical equity/crypto skew; β = 0 recovers the symmetric smile.
/// The multiplier is floored at 0.1 so deep wings can never zero out σ.
function smileVol(
    uint256 spot,
    uint256 strike,
    uint256 sigma,
    uint256 alpha,
    int256 beta
) internal pure returns (uint256) {
    if (spot == 0) return sigma;
    // ln(K/S) in WAD
    int256 lnKS = lnWad(int256((strike * WAD) / spot));
    // lnKS² in WAD
    uint256 lnKS2 = uint256((lnKS * lnKS) / int256(WAD));
    // multiplier = 1 + α·lnKS² + β·lnKS  (in WAD, signed while skew applies)
    int256 multiplier = int256(WAD + (alpha * lnKS2) / WAD) + (beta * lnKS) / int256(WAD);
    int256 floorMultiplier = int256(WAD / 10);
    if (multiplier < floorMultiplier) multiplier = floorMultiplier;
    return (sigma * uint256(multiplier)) / WAD;
}

The defaults are alpha = 2.0 and beta = 0 (SmilePremiumLib.ALPHA = 2e18, frontend/lib/options.ts ALPHA = 2.0, BETA = 0.0). Beta is set by the hook's deployer through setBeta; a negative value makes puts richer than calls at equal distance from spot, which matches observed equity and crypto markets.

The feedback loop: bumpSigma and afterSwap

The primary market (the vault's own buy() and close()) knows which expiry traded, so it bumps only that tenor's bucket. A secondary-market swap through the Uniswap v4 pool carries no expiry, so afterSwap treats it as a surface-wide demand shift and bumps every bucket.

src/hooks/OptionPricingHook.sol

/// @notice Tenor-aware demand feedback: bump only the bucket that traded.
function bumpSigma(bool isBuy, uint256 timeToExpiry) external {
    require(msg.sender == vault, "only vault");
    _bump(_bucketOf(timeToExpiry), isBuy);
}

function _bump(uint256 bucket, bool isBuy) internal {
    if (isBuy) {
        sigmaBuckets[bucket] += GAMMA;
    } else {
        sigmaBuckets[bucket] = sigmaBuckets[bucket] > GAMMA ? sigmaBuckets[bucket] - GAMMA : 0;
    }
}

/// @notice Bump σ_global up on buy, down on sell — demand-driven IV feedback.
function afterSwap(
    address,
    PoolKey calldata,
    SwapParams calldata params,
    BalanceDelta,
    bytes calldata
) external returns (bytes4, int128) {
    require(msg.sender == poolManager, "only pool manager");
    bool isBuy = params.amountSpecified < 0;
    // Pool swaps carry no tenor info — treat as a surface-wide demand shift.
    for (uint256 i = 0; i < 4; i++) _bump(i, isBuy);
    return (IHooks.afterSwap.selector, 0);
}

The vault calls the hook after a fill and after a sellback:

src/vaults/AquaCollateralVault.sol

optionToken = _mintSeries(authId, auth, strike, amount, collateralNeeded, buyer);

if (address(hook) != address(0)) hook.bumpSigma(true, auth.expiry - block.timestamp);
if (address(hook) != address(0)) {
    hook.bumpSigma(false, auth.expiry > block.timestamp ? auth.expiry - block.timestamp : 0);
}

The veto: beforeSwap

On the secondary market, the hook computes a fair value from the pricing engine and rejects any swap whose execution price is more than 5% away from it. Hook data carries the option's spot, strike, expiry and alpha.

src/hooks/OptionPricingHook.sol

/// @dev Oracle price tolerance: 5% band around the engine's fair value.
uint256 public constant PRICE_TOLERANCE = 0.05e18;

uint256 fairValue = pricingEngine.quote(p);
uint256 executionPrice = _abs(params.amountSpecified);

uint256 diff = executionPrice > fairValue
    ? executionPrice - fairValue
    : fairValue - executionPrice;

require(diff * WAD / fairValue <= PRICE_TOLERANCE, "price outside oracle bounds");

How a quote reaches the hook

When a liquidity provider authorises a range, the vault records the hook's address as the strategy's sigma source, and the SwapVM instruction reads that source inside the swap. This is the atomic read described above: the premium is computed from the bucket's value at the block of the trade.

src/vaults/AquaCollateralVault.sol

auth.sigmaSource = address(hook);
auth.premiumDecimals = IERC20Metadata(premiumToken).decimals();
auth.beta = address(hook) != address(0) ? hook.beta() : int256(0);

src/swapvm/OptionPremiumInstruction.sol

// Live vol surface: σ per tenor from the sigma source, skewed per strike.
uint256 sigmaTenor = terms.sigmaSource != address(0)
    ? ISigmaSource(terms.sigmaSource).sigmaFor(v.timeToExpiry)
    : DEFAULT_SIGMA;

The vaults built at EthOnline 2026 take the same path through the shared library, so a spread, a margined put and an RFQ floor all quote off the identical surface:

src/periphery/SmilePremiumLib.sol

uint256 sigma = t.sigmaSource != address(0)
    ? ISigmaSource(t.sigmaSource).sigmaFor(timeToExpiry)
    : DEFAULT_SIGMA;
if (t.sigmaMulBps != 0) sigma = (sigma * t.sigmaMulBps) / 1e4; // S5
if (sigma < MIN_QUOTE_SIGMA) sigma = MIN_QUOTE_SIGMA;

uint256 sigmaStrike = SmileMath.smileVol(t.spotWad, t.strike, sigma, ALPHA, t.beta);

MarginVault is the deliberate exception: it prices premiums off the hook but computes margin from the Chainlink oracle only, never from sigmaFor, so that trading cannot move margin requirements.

Live spot from the Trading API

The application's spot price comes from a Uniswap Trading API quote for one WETH into USDC on mainnet, refreshed every 60 seconds, with a Chainlink on-chain read as the second source and a static value as the last resort.

frontend/hooks/useUniswapSpot.ts

async function fetchUniswap(apiKey: string): Promise<number> {
  const res = await globalThis.fetch(
    "https://trade-api.gateway.uniswap.org/v1/quote",
    {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-api-key": apiKey },
      body: JSON.stringify({
        type: "EXACT_INPUT",
        tokenInChainId: 1,
        tokenOutChainId: 1,
        tokenIn: WETH_MAINNET,
        tokenOut: USDC_MAINNET,
        amount: "1000000000000000000",
        swapper: "0x0000000000000000000000000000000000000000",
      }),
    }
  );
  if (!res.ok) throw new Error(`Uniswap API ${res.status}`);
  const data = await res.json();
  // Trading API v1: output amount is under data.quote.output.amount; older API: data.quote (string)
  const rawAmount = data.quote?.output?.amount ?? data.quote;
  const price = Number(rawAmount) / 1e6;
  if (!price || price < 100) throw new Error("bad quote");
  return Math.round(price);
}

Premium routing through the Universal Router

The buy flow asks the Trading API for an EXACT_OUTPUT quote (native ETH in, exactly the premium in USDC out) and then for the calldata, which the wallet sends to the Universal Router before the approve and buy() steps.

frontend/hooks/useUniswapTrade.ts

// Ask the Trading API for an EXACT_OUTPUT quote, then fetch calldata via /v1/swap.
// The Trading API v1 separates quoting (CLASSIC routing) from transaction building.
export async function fetchUniswapSwapQuote(
  usdcAmountOut: bigint,
  swapper: `0x${string}`,
  usdcToken: string,
  apiKey: string,
): Promise<UniswapSwapQuote> {

The trader's dictionary

The frontend evaluates the smile at the two strikes that have roughly 25% probability of finishing in the money, one call and one put, and reports the three desk numbers.

frontend/lib/options.ts

export function surfaceQuotes(spot: number, tYears: number): SurfaceQuotes {
  const atmVol = smileSigma(spot, spot);
  const k25call = strikeForDelta(spot, 0.25, true, tYears);
  const k25put = strikeForDelta(spot, 0.25, false, tYears);
  const volC = smileSigma(spot, k25call);
  const volP = smileSigma(spot, k25put);
  return {
    atmVol,
    expectedMovePct: atmVol * Math.sqrt(Math.max(tYears, 0)),
    rr25: volC - volP,
    bf25: (volC + volP) / 2 - atmVol,
    k25call,
    k25put,
  };
}

The Vol Surface tab

The Vol Surface tab renders a 3-D surface of sigma_strike(K, T) from a small Python service in volsurface/. Every confirmed buy or sell is posted to the service, which bumps the traded tenor bucket by the same gamma the hook applies (GAMMA = 0.005 in volsurface/server.py), so the surface visibly re-rates as order flow arrives. The 7, 30 and 90 day bucket edges appear as terraces on the plot.

Deployment and tests

script/Deploy.s.sol deploys the hook with the pricing engine and an initial sigma, then wires it before any range is authorised so that every strategy snapshots it as its sigma source (vault.setHook(address(hook)), hook.setVault(address(vault))). On Anvil and the testnets the pool manager argument is a placeholder because no live Uniswap v4 pool is deployed there; the hook's beforeSwap and afterSwap paths are exercised in test/OptionPricingHook.t.sol, whose thirteen tests cover the 5% tolerance veto in both directions, the bump direction on buy and sell, per-bucket selection, the surface-wide bump from afterSwap, the sigmaGlobal back-compat view, setBeta access control, and downside skew under a negative beta. A real v4 hook address must encode the before-swap and after-swap flags in its low byte (address & 0xFF == 0xC0), which the contract's header comment records for a mined deployment.

Limitations

The numbered items refer to Limitations.

  • L3, repricing lands after the trade. The bump is applied after the fill, so a trader always executes at the pre-bump price. A market maker who repriced only after each fill would be run over; on-chain, a sniper pays no price impact on the trade where it matters.
  • L6, parameter risk for passive LPs. An LP delegates pricing to the surface's sigma buckets, alpha and beta. If governance moves alpha or beta, or the feedback loop walks a bucket away from fair, every open quote in every affected range marks against the LP with no action on their part. These exposures are the desk's butterfly and risk-reversal sensitivities, but there is no dashboard surfacing them yet.
  • L7, the feedback loop is nudgeable. Trades move sigma and trades can be manufactured: an attacker can sell back to walk sigma down before buying size. Each round trip pays the full spread and the 1% fee on re-entry, and gamma is small, so the attack is a bounded nuisance rather than a free lunch, but the loop is not manipulation-proof. SmilePremiumLib adds a MIN_QUOTE_SIGMA floor of 20% so a two-leg spread quote cannot collapse to zero on the way down.
  • L1 and L2, the oracle latency gap. The surface prices off the last Chainlink round, so between heartbeats the quote is stale relative to the live market (L1) and drift smaller than the feed's deviation threshold leaves no on-chain signal at all (L2). A sniper who watches the live market buys the stale quote before the feed catches up; the hook's bump arrives after that trade (L3). The Pyth adapter (R5) narrows the window for quoting; it does not close it.
  • L4, one transaction can drain a whole range. The main vault bounds this with a per-authorization block cap (R1, maxBlockNotional); the sibling vaults built at EthOnline 2026 have no such cap, so on them a single fill can consume an authorization's entire remaining collateral at one stale price, and the sigma bump fires only afterwards. The loss per staleness event is then bounded by the range's maxCollateral.
  • L5, on-chain rules cannot reject informed traders. Every rule the hook or the vault could apply is public, so a sniper simulates it and submits only trades that pass. Rules can filter mechanically definable patterns (staleness, size, rate) but never informedness, which is not observable on chain. Rejection is therefore the wrong frame and pricing is the right one: the recommendations R1 through R4 make toxic flow pay for its toxicity through the spread rather than trying to identify it. Part 1 of Limitations explains adverse selection from zero for readers new to the term.
  • Bucket-edge discontinuities. Sigma is a step function of time to expiry. Two expiries one day apart on either side of the 30-day edge can price off different buckets, and a trade can only move the bucket it landed in.
  • Secondary market not deployed (L14). The v4 pool for OptionTokens is designed and tested but no live pool exists on Anvil, Sepolia or Arc; the pool manager is a placeholder in the deploy script. afterSwap is exercised in tests, not on a public network.
  • Spot source is mainnet. The Trading API quote is for mainnet WETH/USDC regardless of the connected chain, and the premium swap targets mainnet as well; on testnets the Chainlink fallback supplies the displayed spot.

Plans

The numbered items refer to Solutions.

  • S5, per-range LP-quoted vol. The instruction and the library already apply an LP-chosen sigmaMulBps multiplier on top of the hook's tenor sigma. The protocol surface becomes the default for passive LPs; opinionated LPs quote their own volatility, which is how professional options markets quote.
  • S6, best-quote routing. A router view that scans active ranges covering a strike and returns the best executable Ask or Bid. With S5, overlapping ranges with different sigma opinions form an order book in volatility space, and the touch is the discovered market volatility.
  • S7, an optional external IV anchor. If range competition stays thin, anchor the default buckets to an external reference such as Deribit ATM volatility, with the feedback loop reduced to a bounded deviation around the anchor. This adds an oracle dependency and is gated on evidence that the default surface drifts.
  • Smooth interpolation. The README's design note leaves smooth interpolation across tenors to a future RFQ-style quoting layer. RfqVault, built at EthOnline 2026, is the first piece of that layer: an LP may sign a quote from any model, and the formula surface remains the public floor.
  • Live v4 pool. Deploying the OptionToken pool with a mined hook address would put beforeSwap and afterSwap on a public network.

Glossary

  • Uniswap v4 hook. A contract that the Uniswap v4 pool manager calls at fixed points in a pool's lifecycle. A hook's address encodes which callbacks it implements. OptionPricingHook implements beforeSwap and afterSwap.
  • PoolManager. The single Uniswap v4 contract that holds every pool's state and invokes hooks. Only it may call afterSwap on Smile's hook.
  • beforeSwap / afterSwap. The hook callbacks invoked immediately before and after a swap executes. Smile uses the first to veto and the second to reprice.
  • Universal Router. Uniswap's router contract that executes the swap calldata the Trading API returns.
  • Uniswap Trading API. Uniswap's hosted quoting and transaction-building service. Smile uses its /v1/quote endpoint for spot and its swap endpoint for the premium swap.
  • Implied volatility (sigma). The annualised standard deviation of returns that an option premium implies. Expressed as a percentage; 80% means the market prices one-standard-deviation moves of 80% per year.
  • Vol surface. Implied volatility as a function of strike and time to expiry. Smile's is sigma_strike(K, T).
  • Tenor bucket. A band of time to expiry that shares one sigma. Smile has four: under 7 days, 7 to 30, 30 to 90, and 90 days or more.
  • Term structure. How implied volatility varies with time to expiry; the tenor dimension of the surface.
  • Smile. The shape of implied volatility across strikes at one expiry. It is called a smile because wings usually price above the centre.
  • Curvature (alpha). How much more the wings cost than the centre; the coefficient on ln(K/S)^2.
  • Skew (beta). Which side costs more; the signed coefficient on ln(K/S). Negative beta makes downside strikes richer.
  • Log-moneyness. ln(K/S), the natural logarithm of strike over spot. Zero at the money, negative for strikes below spot.
  • ATM (at the money). A strike equal to the current spot price. ATM volatility is the tenor sigma itself.
  • OTM / ITM. Out of the money and in the money: a call is ITM when spot is above the strike; a put is ITM when spot is below it.
  • Delta. The sensitivity of an option's value to the spot price, between 0 and 1 for calls. Approximately the probability of finishing in the money.
  • 25-delta. The reference strikes, one call and one put, whose delta is 0.25; roughly 25% probability of finishing in the money. The near-universal points at which desks measure the wings.
  • Risk reversal (RR). 25-delta call volatility minus 25-delta put volatility. Its sign says which direction costs more.
  • Butterfly (BF). The average of the two 25-delta wing volatilities minus ATM volatility. The market's charge for fat tails.
  • Expected move. ATM volatility times the square root of time to expiry, as a fraction of spot: the size of move the premium is charging for.
  • Gamma (feedback step). In this document, the per-trade sigma step of 0.5 volatility points (GAMMA = 0.005e18). Not the option Greek of the same name.
  • Demand feedback loop. The rule that every buy raises the traded tenor's sigma by gamma and every sellback lowers it.
  • Delta hedging. Offsetting the directional exposure of an options position by trading the underlying. On Smile, the underlying leg trades on Uniswap.
  • Arbitrage. Buying and selling equivalent exposure in two places to capture a price difference. An arbitrageur who buys underpriced options and hedges on Uniswap is the mechanism that corrects Smile's sigma.
  • Emergent market maker. An arbitrageur who, by repeatedly buying cheap options and selling back rich ones while hedging on Uniswap, performs the role of a designated market maker without being appointed.
  • Ask / Bid. The price a taker pays to open a position and the price a holder receives to sell it back. The premium instruction prices the Ask in the forward swap direction and the Bid in the reverse direction.
  • Primary market. Trades against the vault (buy() and close()), which mint or burn OptionTokens.
  • Secondary market. Transfers of existing OptionTokens between holders, designed to run through a Uniswap v4 pool with the hook attached.
  • WAD. Fixed-point notation in which 1e18 represents 1.0. All sigma values in the contracts are WAD.
  • SwapVM instruction. A custom opcode executed inside the 1inch SwapVM. OptionPremiumInstruction is Smile's; it reads sigmaFor at execution.
  • Strategy / authorization. A liquidity provider's signed range of strikes and expiries. At authorization the hook's address is stored as the strategy's sigma source.
  • RFQ (request for quote). A model in which a maker signs a price off-chain for a specific taker to fill. RfqVault implements it as a tier above the formula surface.

The Graph in Smile

This page documents every way Smile uses The Graph, why an options venue needs it, what it adds for traders and liquidity providers, how it is built, what it does not do yet, and what comes next. All of the work described here was built during EthOnline 2026 on the EthOnline2026_continuation_track branch. Terms are defined at first use and collected in the glossary at the end.

Summary

The Graph is a decentralised indexing protocol: it watches a blockchain, runs user-written code on every relevant event, and stores the result in a database that can be queried with GraphQL, a query language in which the caller names exactly the fields it wants. Smile's subgraph (the unit of indexing on The Graph) turns the raw events of the AquaCollateralVault contract into four tables: every liquidity range an LP has written, every option fill, every option instrument with its open interest, and every holder's balance. Together these tables are Smile's tape, the running record of what has traded and at what price, which a centralised exchange publishes as a matter of course and which an on-chain venue otherwise does not have.

Two subgraphs are live on Subgraph Studio, The Graph's hosted deployment service: smile-sepolia (version 0.0.4) for the Sepolia testnet and smile-arc-testnet (version 0.0.1) for Circle's Arc testnet. On 2026-09-12 both were also published to The Graph Network, the decentralised network of indexers, and the live application reads them through the network's gateway with an API key that never leaves the server. On those public networks the application and the AI copilot read positions, liquidity and trade history from The Graph only; no RPC scan exists as a fallback. The copilot is built on that tape as a trading agent: it screens every live strike against the listed reference market, maps where liquidity is scarce, computes the greeks of a wallet's whole book, sizes a hedge, and prepares the range an LP might write or the quote a market maker might sign. The user signs; the agent never holds a key. The copilot's know-how ships as portable skill files, and it can connect to The Graph's own Subgraph MCP server as well as any server the user adds.

Features used

Feature Where in the code Origin
Subgraph with Authorization, Fill, Instrument, Position entities subgraph/schema.graphql, subgraph/subgraph.yaml EthOnline 2026
Event handlers with bound contract calls that refresh usedCollateral from chain state subgraph/src/vault.ts EthOnline 2026
Studio deployments for Sepolia and Arc testnet subgraph/networks.json; frontend/lib/deployments.ts EthOnline 2026
Published to The Graph Network (Arbitrum One) and served through the gateway with an API key held server-side; subgraph ids Bf9T8wuSLwvNSR9oTx2uuSjoL2P5kCagWAitFgykyes2 (Sepolia) and 9ZcFMvnhbWygRg7oB29NL8smoVysqCbdQpqNMhWtHbmq (Arc testnet) frontend/app/api/subgraph/route.ts (SUBGRAPH_URL_<chainId>), the subgraph notes EthOnline 2026 (2026-09-12)
Browser and server GraphQL client with per-chain endpoint resolution frontend/lib/subgraph.ts EthOnline 2026
Server-side proxy so a gateway API key never reaches the browser frontend/app/api/subgraph/route.ts EthOnline 2026
The tape: one shape for ranges, instruments, fills and positions; chain-id gate frontend/lib/tape.ts EthOnline 2026
LP dashboard and copilot position tools reading the tape (the L12a fix) frontend/components/LPDashboard.tsx, frontend/lib/copilot/chain.ts EthOnline 2026
Copilot trading tools: find_opportunities, liquidity_map, portfolio_greeks, hedge_suggestion, reference_market, macro_calendar, prepare_lp_range, prepare_rfq_quote frontend/lib/copilot/graphTools.ts, deribit.ts, macro.ts, tools.ts EthOnline 2026
Tab-aware briefing in the copilot prompt frontend/lib/copilot/systemPrompt.ts, tabs.ts EthOnline 2026
Eight trader skills and a Skills menu with user-added skills frontend/skills/*.md, frontend/components/copilot/SkillsMenu.tsx EthOnline 2026
MCP servers: operator-seeded (COPILOT_MCP_SERVERS) and per-user (settings gear, x-copilot-mcp header), opened per request; The Graph's Subgraph MCP seeded on the live deployment and verified end to end frontend/lib/copilot/mcp.ts, frontend/components/copilot/CopilotSettings.tsx, frontend/app/api/copilot/route.ts EthOnline 2026 (verified 2026-09-12)
The copilot itself: one server route, four providers (operator env or bring-your-own-key header), a system prompt assembled from the knowledge pack, the tab briefing and the active skills, nineteen built-in tools frontend/app/api/copilot/route.ts, frontend/lib/copilot/provider.ts, systemPrompt.ts, tools.ts, knowledge.ts Pre-existing (route, providers, docs tools); EthOnline 2026 (tape tools, tabs, skills, MCP, OpenRouter)
Agent-facing subgraph documentation and client configuration the subgraph notes, .mcp.json.example EthOnline 2026
Traded premium and implied volatility per instrument on the price chart frontend/components/PriceChart.tsx EthOnline 2026
A seeded tape of one hundred trades on the local chain script/SeedTape.s.sol, script/seed-tape.sh, local.sh EthOnline 2026

Why it is necessary

An on-chain options venue has no public tape. On a centralised exchange the order book, the last trade and the open interest of every instrument are published continuously. On a blockchain those facts exist only as events scattered across blocks. A wallet that wants to know "what did the 3,000 call last trade at?" or "how much of this range is already used?" must either walk the chain's event log from the deployment block or call the contract once per candidate strike. Neither scales, and neither is queryable by an outside program in a reasonable time.

The capped scan went blind. Before the subgraph existed, the copilot's position reader looped over every authorisation identifier up to a hard limit of fifty (MAX_AUTHS = 50 in frontend/lib/copilot/chain.ts), then walked a forty-strike grid per range with one RPC call per strike. Past fifty ranges ever created it silently stopped seeing new ones, including the connected wallet's own. The LP dashboard used a getLogs scan from block zero as a stopgap and showed only one range per LP. Both are recorded as Limitations, L12a. The subgraph is the correct fix rather than an add-on: it replaces a bounded, brute-force scan with an indexed query, and on public networks the scan no longer exists at all.

An agent needs indexed data. An AI copilot that reasons about a market must be able to ask "every active range", "open interest by strike", "this wallet's positions" and "the last twenty fills of this instrument" as single, cheap questions. Those are precisely the queries a subgraph answers. Without one, every copilot answer about positions or liquidity would rest on the same capped scan, and every answer would be suspect past the cap.

Market value add

A trading coach on live data. The copilot's second tool set reads the tape and behaves like a desk analyst. find_opportunities prices every live strike on every active range at the vault's own current volatility, converts that ask to an implied volatility, and compares it with the nearest listed instrument on Deribit, the largest crypto options exchange, and with the last fill of the same instrument on Smile; the result is ranked cheap to expensive. liquidity_map reports capacity, utilisation, open interest and staleness per range, flags bands that are scarce, empty, stale or expiring, and draws a per-strike heat map so that "where is liquidity thin?" is a one-line question. portfolio_greeks reads a wallet's long positions from the Position entity and its written exposure from the open interest on its own ranges, and returns net delta, gamma, theta and vega with marks and profit and loss. hedge_suggestion turns that book into a quantity of spot ETH, or of calls or puts at a strike, that brings it to a target delta.

The agent prepares; the user signs. prepare_lp_range and prepare_rfq_quote render cards whose buttons prefill the Write a Range form and the RFQ signer respectively. The copilot cannot send a transaction and never holds a key. This keeps the non-custodial property of the protocol intact while removing the spreadsheet work from market making.

Portable know-how. The copilot's behaviour is packaged as eight skill files in the SKILL.md convention (a markdown file with a name, a description, a starter prompt and a procedure). A trader can read them, toggle them, and add their own without a rebuild. The subgraph notes describe Smile's subgraph to any AI environment, and .mcp.json.example is a one-file client configuration for The Graph's Subgraph MCP server, so the same data is reachable from Claude Code or Cursor without reading the schema.

A chart with a tape under it. The price chart draws the traded premium per unit and the implied volatility of each fill for a chosen instrument, so a trader sees whether the vault's volatility feedback loop has moved the price of a strike, not only the price of the underlying.

Technical details

Entities

The schema defines four entities. Instrument is one strike of one range, identified by its OptionToken address; open interest is bought minus closed minus redeemed.

subgraph/schema.graphql:

type Instrument @entity(immutable: false) {
  id: ID!                      # optionToken address, lowercase hex
  optionToken: Bytes!
  authorization: Authorization!
  lp: Bytes!
  strike: BigInt!              # WAD USD
  expiry: BigInt!
  isCall: Boolean!
  openInterest: BigInt!        # WAD option units outstanding
  volume: BigInt!              # WAD option units ever bought
  fillCount: Int!
  lastPremiumPerUnit: BigInt!  # premium-token units per 1e18 option units, fee included
  lastTradeAt: BigInt!
  fills: [Fill!]! @derivedFrom(field: "instrument")
  positions: [Position!]! @derivedFrom(field: "instrument")
}

Authorization is one LP range with strikeMin, strikeMax, expiry, isCall, collateralToken, maxCollateral, usedCollateral, active and fillCount. Fill is one OptionBought event, immutable, keyed by transaction hash and log index. Position is one holder's balance in one instrument, credited on OptionBought and debited on OptionClosed and Redeemed.

Handlers and the bound-call refresh

The RangeAuthorized event does not carry the collateral token or a live usedCollateral, and the vault's just-in-time pull accounting is not something to re-implement in AssemblyScript. Instead, one bound contract call per relevant event overwrites those fields from chain state.

subgraph/src/vault.ts:

function refreshFromChain(auth: Authorization, vaultAddress: Address): void {
  let vault = AquaCollateralVault.bind(vaultAddress);
  let res = vault.try_authorizations(auth.authId);
  if (res.reverted) return;
  auth.maxCollateral = res.value.value4;
  auth.usedCollateral = res.value.value5;
  auth.collateralToken = res.value.value6;
  auth.active = res.value.value8;
}

The OptionBought handler updates the instrument, the buyer's position, writes the fill, and refreshes the authorisation:

export function handleOptionBought(event: OptionBought): void {
  let id = event.params.authId.toString();
  let auth = Authorization.load(id);
  if (auth == null) return;

  let inst = loadOrCreateInstrument(event.params.optionToken, auth, event.params.strike);
  inst.openInterest = inst.openInterest.plus(event.params.amount);
  inst.volume = inst.volume.plus(event.params.amount);
  inst.fillCount = inst.fillCount + 1;
  if (event.params.amount.gt(BigInt.zero())) {
    inst.lastPremiumPerUnit = event.params.premium.times(WAD).div(event.params.amount);
  }
  inst.lastTradeAt = event.block.timestamp;
  inst.save();
  ...

Seven events are handled: RangeAuthorized, AuthorizationRevoked, OptionBought, OptionClosed, Redeemed, CollateralReleased and PullFailed (a dishonoured just-in-time pull deactivates the range on-chain, and the handler mirrors it).

Deployments

Network Vault address Start block Studio endpoint
Sepolia 0x82AcBBFE5E03510d5407d8C50435B08e6d2d0a4D 11677088 https://api.studio.thegraph.com/query/44448/smile-sepolia/v0.0.4
Arc testnet 0xE37ED711F7D1dc5aC045206b4A6367C55229C789 61227750 https://api.studio.thegraph.com/query/44448/smile-arc-testnet/v0.0.1

Both endpoints are recorded per chain in frontend/lib/deployments.ts and report hasIndexingErrors: false with the testnets' real fills.

The tape query

The frontend client defines the queries once as strings and mirrors the entities as TypeScript interfaces.

frontend/lib/subgraph.ts:

export const INSTRUMENTS = `query Instruments($first: Int!) {
  instruments(orderBy: lastTradeAt, orderDirection: desc, first: $first) { ${INSTRUMENT_FIELDS} }
}`;
export const POSITIONS_BY_HOLDER = `query PositionsByHolder($holder: Bytes!) {
  positions(where: { holder: $holder, balance_gt: "0" }, first: 1000) { ${POSITION_FIELDS} }
}`;

The chain-id gate

readTape is the single entry point for ranges, instruments, fills and positions. A subgraph endpoint is used whenever one resolves for the chain. Only the local Anvil chain (chain id 31337 or 1337) may rebuild the same entities from the event log; on a public network with no endpoint the call throws rather than scanning.

frontend/lib/tape.ts:

export async function readTape(opts: TapeOpts): Promise<Tape> {
  const url = subgraphUrlFor(opts.chainId);
  if (url) return tapeFromSubgraph(url, opts.since ?? 0);
  if (isLocalChain(opts.chainId) && opts.client && opts.vault) return (await stateFromLogs(opts.client, opts.vault)).tape;
  throw new SubgraphRequiredError(opts.chainId);
}

Every Tape carries a source field, "subgraph" or "anvil-logs", and the copilot is instructed to state where its numbers came from.

The proxy and the per-chain gateway URL

A gateway URL carries the API key in its path, so it must never reach a browser. It is configured server-side, per chain, and the browser reaches it through /api/subgraph, which forwards the request body unchanged. Resolution order in the route: an explicit NEXT_PUBLIC_SUBGRAPH_URL override, then SUBGRAPH_URL_<chainId>, then a chain-agnostic SUBGRAPH_URL, then the recorded Studio endpoint for the chain. The live deployment sets SUBGRAPH_URL_11155111 and SUBGRAPH_URL_5042002, so Sepolia and Arc reads go through the network while Anvil has no entry.

frontend/app/api/subgraph/route.ts:

export async function POST(req: Request) {
  const chainId = Number(new URL(req.url).searchParams.get("chainId") ?? "0");
  const url =
    process.env.NEXT_PUBLIC_SUBGRAPH_URL ||
    process.env[`SUBGRAPH_URL_${chainId}`] ||
    process.env.SUBGRAPH_URL ||
    DEPLOYMENTS[chainId]?.subgraph ||
    "";
  if (!url) return Response.json({ errors: [{ message: `no subgraph for chain ${chainId}` }] }, { status: 404 });
  const upstream = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: await req.text(),
  });
  return new Response(upstream.body, {
    status: upstream.status,
    headers: { "content-type": "application/json" },
  });
}

The same resolution lives in subgraphUrlFor in frontend/lib/subgraph.ts for server-side callers such as the copilot's tape tools; in the browser that function returns the proxy path when the build has a server, and the Studio endpoint directly on the static GitHub Pages export, which has no server and therefore no key.

Published to The Graph Network

A subgraph on The Graph exists in two places with different guarantees. A Studio deployment is served by The Graph's own upgrade indexer from a rate-limited development endpoint with no key. Publishing records the subgraph on the protocol's contracts, which live on Arbitrum One regardless of the chain the subgraph indexes, and makes it available through the gateway, the query endpoint that routes each request to an indexer and bills it to an API key. No curation signal is required: the upgrade indexer keeps serving a published subgraph until independent indexers pick it up.

Both subgraphs were published on 2026-09-12. The publish transaction is a wallet step in Studio; the API key is created under Studio → API Keys.

Network Subgraph id (network) Deployment id (IPFS hash)
Sepolia Bf9T8wuSLwvNSR9oTx2uuSjoL2P5kCagWAitFgykyes2 QmRkbvKcWtMShTSDGXJhEUYkjahWGvKsWU1EcTM3wDHEua
Arc testnet 9ZcFMvnhbWygRg7oB29NL8smoVysqCbdQpqNMhWtHbmq QmTA9unF8d66AwATQz6MYxM6zvEW6LAj3kned4E48txoc3

The gateway URL has the form https://gateway.thegraph.com/api/<key>/subgraphs/id/<subgraph id>. Both answered with the testnets' real instruments and hasIndexingErrors: false within minutes of publishing (the Arc subgraph took about two minutes to appear). A subgraph id names the subgraph across versions; a deployment id names one built version, and the gateway can also be addressed by it (/deployments/id/<hash>).

The Studio development endpoints remain recorded in lib/deployments.ts as the fallback for the browser on the static export and for any environment without the key. The same key is the bearer token for The Graph's Subgraph MCP server (next sections), so one credential covers both the data path and the agent path.

The copilot

The copilot is a chat panel in the application and one server route, POST /api/copilot/. The route exists only on server builds (the Vercel deployment); the static GitHub Pages export has no server, so the widget hides itself there (NEXT_PUBLIC_COPILOT). Each request carries the chat history and a context object the client assembles from what is on screen, so the copilot's numbers match the visible interface.

frontend/app/api/copilot/route.ts:

const ctx: CopilotContext = {
  spot: typeof context?.spot === "number" && context.spot > 0 ? context.spot : 3420,
  chainId: context?.chainId,
  address: context?.address,
  tab: isTabId(context?.tab) ? context.tab : undefined,
  skills: Array.isArray(context?.skills) ? context.skills.filter((s) => typeof s === "string") : undefined,
  customSkills: Array.isArray(context?.customSkills) ? context.customSkills : undefined,
};

Model providers. lib/copilot/provider.ts supports four providers, anthropic, openai, google and openrouter, with defaults claude-opus-4-8, gpt-5-mini, gemini-2.5-pro and openrouter/auto. The operator chooses one with COPILOT_PROVIDER and the matching key variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENROUTER_API_KEY), optionally overriding the model with COPILOT_MODEL. A user may instead bring their own key: the settings gear stores provider, key and model in the browser's local storage only, and the panel sends them per request in the x-copilot-provider, x-copilot-api-key and x-copilot-model headers; the route builds that request's model client from them and never stores or logs them. The live deployment runs openrouter with COPILOT_MODEL=openrouter/free, a free routed model, so visitors need no key of their own; the tool-routing rules in the prompt were written with a weak model in mind.

The system prompt. lib/copilot/systemPrompt.ts is one auditable template with, in order: the current context (spot, chain, wallet); the briefing for the tab on screen; the pricing model (the smile formula, the current sigma, alpha and beta, how the spread arises, how ranges and the just-in-time pull work); the tool rules (never do options mathematics in the head, which tool answers which question, that trades are never executed); the data-source rule (say whether numbers came from the subgraph or the local event log); the active skills, in full; rules for rolls, output style, teach mode and quiz mode; and, from the build-time knowledge pack, a table of contents of every documentation section and the glossary from the limitations page. The knowledge pack (lib/copilot/knowledge.generated.json, built by scripts/gen-knowledge.mjs from the README, the User Guide, the limitations, solutions and copilot pages and the five integration pages) holds the full section bodies for the read_docs tool, capped at six thousand characters per section.

Tab awareness. lib/copilot/tabs.ts describes every application tab: what is on screen, which documentation sections to read first, the best first move, and three starter prompts. The panel sends the active tab in the context; the prompt gains a "where the user is" block so that "explain this" means the tab on screen, and the panel shows that tab's starters.

Skills. The eight built-in skills are markdown files in the SKILL.md convention (frontmatter name, description, starter, then the procedure), bundled into the knowledge pack at build time: Trading opportunities, Risk management, Delta hedging, Explain margin, LP market making, RFQ quoting, Macro context and Calendar spreads. The Skills menu in the panel header toggles them and accepts user-written skills; both lists live in local storage and ride each request in the context (custom skills are capped at five of four thousand characters). Enabled skill bodies are appended to the prompt under "Active skills".

Built-in tools. lib/copilot/tools.ts defines nineteen tools; the model may take up to ten tool steps per turn. Grouped by what they read:

Tool What it does and what it reads
read_docs Returns one section of the documentation from the knowledge pack by section id; the model is told to cite the id.
get_market_state Spot, the smile parameters, ATM vol, expected move, 25-delta risk reversal and butterfly from the same code as the interface (lib/options.ts).
price_strategy, suggest_strategies, scenario_analysis, analyze_adjustment Price a multi-leg strategy at the protocol's smile (entry, max profit and loss, probability of profit, breakevens, greeks), propose strategies for a stated view, run a spot and vol stress grid, and price a roll or adjustment before and after. Client-side mathematics, no chain read.
get_onchain_quote Calls the deployed pricing engine for a live call quote and cross-checks it against the front-end formula.
get_positions, portfolio_risk The connected wallet's balances, written ranges and long positions, and their aggregate risk. Positions come from the tape (lib/copilot/chain.ts reads the subgraph on public networks).
find_opportunities Every live strike on every active range priced at the vault's live volatility per expiry, expressed as implied volatility and compared with the nearest listed Deribit instrument and with the last fill of the same instrument; ranked cheap to expensive. Reads the tape and Deribit.
liquidity_map Every active range with capacity, utilisation, open interest, fills and days since the last trade, flagged scarce, empty, stale or expiring, plus a per-strike heat map. Reads the tape.
portfolio_greeks The wallet's whole book from the tape, long positions with cost basis from its own fills and the written side from open interest on its ranges, with net delta, gamma, theta and vega.
hedge_suggestion The quantity of spot ETH, or of calls or puts at a strike, that brings a book to a target delta, with greeks before and after. The prompt forbids computing a hedge any other way.
reference_market Deribit's public API: index price, the DVOL index, ATM implied volatility at the nearest listed expiry, and the nearest listed instrument to a strike and expiry. Cached for sixty seconds.
macro_calendar Scheduled FOMC, CPI and listed-expiry dates within a horizon from a static 2026 table, with event-volatility heuristics.
prepare_lp_range, prepare_rfq_quote, propose_trade Interface tools: each renders a card whose button prefills the Write a Range form, the RFQ signer or the Payoff Builder. The user reviews and signs in the wallet; the copilot cannot send a transaction and never holds a key.
quiz_question Renders one multiple-choice question as clickable choices; the pick returns as the tool result.

find_opportunities is representative of the tape tools:

frontend/lib/copilot/graphTools.ts:

export async function findOpportunities(
  chainId: number | undefined,
  spot: number,
  opts: { side?: "cheap" | "expensive" | "both"; isCall?: boolean; maxResults?: number }
) {
  const [tape, ref] = await Promise.all([loadTape(chainId), tryReference()]);
  const vault = (chainId ? contractsFor(chainId) : CONTRACTS).aquaVault as Address;
  const sigmas = await liveSigmaByExpiry(getPublicClient(chainId), vault, [...new Set(tape.auths.map((a) => a.expiry))]);
  ...
      const ask = liveAsk(spot, k, a.isCall, t, sigmaGlobal);
      const smileIv = impliedVol(ask, spot, k, t, a.isCall);
      const near = ref ? nearestReference(ref, k, a.expiry, a.isCall) : null;

liquidity_map treats a range as scarce at eighty percent used, stale after three days without a trade and expiring within three days. portfolio_greeks combines Position rows for the holder with open interest on the holder's own ranges. Every tape tool returns the tape's source field, "subgraph" on public networks or "anvil-logs" on the local chain.

MCP servers

The Model Context Protocol (MCP) is an open standard by which an AI model connects to external tool servers over HTTP. In Smile, MCP is how the copilot's toolset grows without a rebuild.

How the plumbing works. lib/copilot/mcp.ts takes two lists of servers: the operator's, from the COPILOT_MCP_SERVERS environment variable, and the user's, sent from the browser in the x-copilot-mcp header the same way the bring-your-own-key headers are. Both are JSON arrays of { name, url, token?, transport? }; only https:// URLs are accepted, the transport is http or sse, and the lists are merged by name with the user's entry winning, capped at five servers. On every request the route opens each server (with an eight-second handshake timeout), asks it for its tools, and merges them after the built-in tools so that a built-in name always wins; a colliding name between two servers is prefixed with the server's name. The servers are closed when the stream finishes or errors. A server that fails to connect is logged and skipped, so a dead server cannot break the chat.

frontend/lib/copilot/mcp.ts:

export function mergeMcpConfigs(env: McpServerConfig[], header: McpServerConfig[]): McpServerConfig[] {
  const byName = new Map(env.map((c) => [c.name, c]));
  for (const c of header) byName.set(c.name, c);
  return [...byName.values()].slice(0, MAX_SERVERS);
}
const client = await createMCPClient({
  transport: {
    type: c.transport ?? "http",
    url: c.url,
    headers: c.token ? { Authorization: `Bearer ${c.token}` } : undefined,
  },
  // Bound the handshake so a dead server cannot stall the chat.
  initializationOptions: { timeout: 8000 },
});

What it has today. The Graph's Subgraph MCP server, https://subgraphs.mcp.thegraph.com/sse, authenticated with a Gateway API key as the bearer token. It is seeded operator-side on the live deployment through COPILOT_MCP_SERVERS, so every copilot request there carries its tools with no setup by the user; it is also the one preset in the settings gear (the user pastes their own key). The server exposes nine tools: search_subgraphs_by_keyword, get_top_subgraph_deployments, get_schema_by_subgraph_id, get_schema_by_deployment_id, get_schema_by_ipfs_hash, execute_query_by_subgraph_id, execute_query_by_deployment_id, execute_query_by_ipfs_hash and get_deployment_30day_query_counts. With them the copilot can find any indexed subgraph on The Graph Network, read its schema and query it in natural language, not only Smile's own. It was verified end to end on 2026-09-12: asked to search subgraphs for "uniswap", the deployed copilot called search_subgraphs_by_keyword through the seeded server and answered with a real subgraph name.

The same server is available to developers outside the application: .mcp.json.example at the repository root is a one-file client configuration for Claude Code or Cursor, and the subgraph notes describe Smile's entities, canonical queries, endpoints and units so that an AI environment can query smile-sepolia or smile-arc-testnet without reading the schema.

.mcp.json.example:

{
  "mcpServers": {
    "thegraph": {
      "type": "sse",
      "url": "https://subgraphs.mcp.thegraph.com/sse",
      "headers": {
        "Authorization": "Bearer <GATEWAY_API_KEY>"
      }
    }
  }
}

What it can have. Any MCP server reachable over HTTPS: the settings gear takes a name, a URL, an optional bearer token and the transport. Servers that fit a trading coach include a market-data server for Deribit or another listed venue (replacing the built-in sixty-second Deribit cache with the user's own feed), a price-oracle server for Chainlink or Pyth rounds, a transaction-simulation server so a proposed trade can be dry-run before the user signs, and another project's subgraph server for cross-protocol positions. Limits: five servers per request, HTTPS only, one merged toolset (a server tool with a built-in's name is shadowed, and a name shared by two servers is prefixed), an eight-second connect budget per server, and the routing quality of the model in use; the free routed model on the live deployment follows explicit tool rules well and open-ended tool choice less well, so a server with many similar tools benefits from a skill that names which one to call.

The seeded tape

A chart and a screener need trades to look at. script/seed-tape.sh writes three ranges (two call expiries and one put) and one hundred trades on the local chain. On Anvil the trades are split into ten batches about six simulated hours apart, and the mock oracle random-walks up to 1.5 percent between batches so that premiums and implied volatility move across the tape. ./local.sh runs it by default (SEED_TRADES=100; set 0 to skip). The resulting tape contains eighty-three buys and seventeen sellbacks across fifty-four simulated hours.

Limitations

  • Transfers of option tokens are not indexed. Position is credited on OptionBought and debited on OptionClosed and Redeemed. An ERC-20 transfer of an option token between wallets is not observed, so a transferred position shows on the original buyer until it is closed or redeemed. Tracking it needs a data-source template per OptionToken (plan G6, not built). Balances are clamped at zero so an unseen transfer cannot drive them negative.
  • The macro calendar is static. macro_calendar reads a hardcoded 2026 table of FOMC, CPI and listed-expiry dates rather than a live feed.
  • No local graph-node on arm64. The development host has no graph-node image for its architecture, so the local Anvil chain has no subgraph. lib/tape.ts rebuilds the same entities from eth_getLogs there, gated on chain id, and that path does not exist on public networks. The subgraph's matchstick unit tests are written but run only on x86.
  • The gateway path depends on one key and one deployment. The server-side gateway URLs are set on the Vercel deployment and in the local environment file; the static GitHub Pages export has no server and falls back to the Studio development endpoints, which are rate-limited. The key is a shared operator credential subject to The Graph's per-key query quota, not a per-user one.
  • The MCP toolset is only as good as the model routing it. The Graph's server is verified with an explicit request; whether the free routed model reaches for it unprompted on an open question is not guaranteed. Servers are opened on every request, which adds their handshake time to each turn.
  • The screener is a model, not a market. find_opportunities prices Smile's ask with the vault's own formula at the hook's live volatility and inverts a Black-Scholes price for the implied volatility. Deribit's instruments are perpetual-margined and listed at different strikes and expiries; the nearest match is a reference, not a like-for-like quote. The skills instruct the copilot to call something cheap only when both the reference comparison and the last-fill comparison agree.

Plans

The phase-two status table and cut list in the Graph plan record what remains.

  • Publish and key (P8): done 2026-09-12. Both subgraphs are published on Arbitrum One and served through the gateway; the live deployment reads them with SUBGRAPH_URL_11155111 and SUBGRAPH_URL_5042002. What remains is operational: rotate the key, watch the query allowance, and add curation signal if independent indexers are wanted beyond the upgrade indexer.
  • Dynamic data sources (G6). A data-source template per OptionToken so that ERC-20 transfers of option tokens update Position.
  • The sibling vaults. The subgraph indexes AquaCollateralVault only. SpreadVault, MarginVault and RfqVault emit their own events and would need their own data sources for the tape to cover spreads, margined puts and signed-quote fills.
  • A live macro feed in place of the static table.
  • More MCP servers as presets. The settings gear has one preset; a Deribit market-data server and a transaction-simulation server are the natural next two, each with a skill naming when to call it.

Glossary

  • API key (Gateway). The credential created in Studio that authorises queries to the gateway and against which they are metered. In Smile it is embedded in the server-side gateway URL and reused as the bearer token for The Graph's Subgraph MCP; it never reaches a browser.
  • Agent (copilot). An AI model that answers by calling tools rather than from memory. Smile's copilot calls pricing, tape and preparation tools; it prepares transactions but never signs or sends one.
  • Bearer token. A credential sent in an HTTP Authorization header. The copilot's MCP client sends a server's token this way on every request.
  • Bound call. In a subgraph mapping, a read-only call to the indexed contract at the block being processed, used here to refresh usedCollateral and collateralToken from chain state.
  • Bring your own key (BYOK). The settings-gear option by which a user supplies their own model-provider key from the browser, sent per request in headers and never stored by the server.
  • Curation signal. GRT staked on a published subgraph to attract independent indexers. Not required for Smile's subgraphs, which the upgrade indexer serves.
  • Deployment id. The IPFS hash of one built version of a subgraph (Qm…). The gateway can be addressed by it as well as by the subgraph id.
  • Deribit. The largest centralised crypto options exchange, used by the copilot as the listed reference market for implied volatility.
  • DVOL. Deribit's thirty-day implied volatility index for ETH.
  • Entity. A table in a subgraph's schema. Smile has four: Authorization, Fill, Instrument, Position.
  • Gateway. The Graph's query endpoint for subgraphs published to the decentralised network, authenticated by an API key; it routes each query to an indexer. In Smile the key stays server-side behind /api/subgraph, configured per chain as SUBGRAPH_URL_<chainId>.
  • GraphQL. A query language in which the client names the fields it wants and receives exactly those.
  • Greeks. The sensitivities of an option's price: delta (to the underlying price), gamma (of delta to the underlying price), theta (to time) and vega (to volatility).
  • Handler (mapping). The code, written in AssemblyScript, that a subgraph runs on each event to update its entities. Smile's handlers are in subgraph/src/vault.ts.
  • Hedge. A position taken to offset the risk of another. A delta hedge brings a book's net delta to a target, usually zero.
  • Implied volatility (IV). The volatility that, put into a pricing model, reproduces an observed option price. The copilot inverts Black-Scholes to obtain it from a premium.
  • Indexer. A node on The Graph's network that runs subgraphs and serves queries. Studio deployments are served by an upgrade indexer without curation.
  • Instrument. One strike of one range, represented by one OptionToken contract.
  • Just-in-time pull. The 1inch Aqua mechanism by which an LP's collateral stays in the LP's wallet until a buyer matches and is pulled at that moment.
  • L12a. The limitation entry on the Limitations page describing the fifty-range cap that the subgraph lifted.
  • MCP (Model Context Protocol). An open standard for connecting an AI model to external tool servers over HTTP. The copilot opens the operator's and the user's servers on every request and merges their tools with its own. The Graph's Subgraph MCP exposes any indexed subgraph to a model through nine tools.
  • Publish. Registering a subgraph on The Graph Network's contracts on Arbitrum One so that it can be served through the gateway and indexed by the network. The indexed chain is unchanged by publishing.
  • Open interest. The number of option units outstanding in an instrument: bought minus closed minus redeemed.
  • Range (authorisation). An LP's standing offer to write options between two strikes up to one expiry, backed by a maximum collateral.
  • Skill. A markdown file in the SKILL.md convention (name, description, starter, procedure) that teaches the copilot a workflow.
  • Subgraph. The unit of indexing on The Graph: a manifest naming the contract and events, a schema of entities, and the handlers.
  • Subgraph id. The identifier of a published subgraph on the network (Bf9T… for Sepolia, 9ZcF… for Arc testnet), stable across versions.
  • Subgraph Studio. The Graph's hosted service for deploying and testing subgraphs before publishing them to the network.
  • System prompt. The instructions the copilot receives before the conversation: context, the pricing model, tool rules, the tab briefing, active skills, the documentation table of contents and the glossary.
  • Tape. The running record of ranges, instruments, fills and positions. On public networks it is the subgraph; on the local chain it is rebuilt from the event log.
  • Upgrade indexer. The indexer The Graph operates to serve Studio deployments and newly published subgraphs that have no curation signal.
  • WAD. A fixed-point number with eighteen decimals, the unit for strikes, option amounts and WETH collateral in the schema.

Circle: Arc, USDC and the Circle App Kits

This page describes how Smile uses Circle's products: the Arc blockchain, the USDC stablecoin on Arc and on Sepolia, and the two Circle App Kits (Gateway and Developer-Controlled Wallets) that fund the margin tier's treasury. It is written for a reader who knows what an option is but has not read the rest of the documentation. Every term of art is defined where it first appears, and a glossary closes the page.

Summary

Smile is a non-custodial venue for European-style options on ETH. On Circle's Arc testnet, the entire Smile stack is deployed with Circle's real Arc USDC as the only dollar in the system. On that chain, USDC is the native gas asset, and the contract at 0x3600000000000000000000000000000000000000 is its ERC-20 view. One balance therefore pays premiums, protocol fees, put collateral, initial margin, backstop-pool deposits, insurance-fund deposits, and transaction gas.

Four vaults are live on Arc and each has executed at least one real fill: the single-leg AquaCollateralVault, the defined-risk SpreadVault, the margined-put MarginVault with its MarginBackstop pool, and the signed-quote RfqVault. The application serves Arc from the same build as Anvil and Sepolia, and a dedicated subgraph on The Graph indexes the Arc deployment.

Two keeper scripts added on 2026-09-11 use Circle's App Kits to fund the margin tier's safety funds without any private key for the treasury living in this repository: keeper/insurance-gateway.mjs moves USDC from Sepolia to Arc through Circle Gateway and deposits it into the insurance fund, and keeper/backstop-wallet.mjs operates a Circle developer-controlled wallet on Arc that deposits into the backstop pool.

Arc has no ether, so on Arc ETH is the reference price only. Puts, put spreads, margined puts, premiums, fees and gas were USDC from the first deploy; on 2026-09-13 the SpreadVault was redeployed cash-settled, so call credit spreads escrow K2−K1 USDC per unit and settle in USDC as well. What remains mock is stated plainly throughout: the ETH/USD price feed (a keeper mirrors Sepolia's Chainlink answer into it every 30 minutes) and, for the covered calls of the main vault and RfqVault only, a WETH stand-in, because a covered call needs the asset itself and Arc has none.

Features used

Feature Where in the code Pre-existing or EthOnline 2026
Arc testnet chain branch in the deploy script (real USDC, mock WETH, mock oracle) script/Deploy.s.sol, chain id 5042002 branch EthOnline 2026 (108e25d, 7d409bc)
Full stack deployed on Arc: Aqua registry, router, pricing engine and hook, AquaCollateralVault, AquaOptionSettlement, SpreadVault the Arc deployment notes, broadcast/Deploy.s.sol/5042002/ EthOnline 2026
Cash-settled SpreadVault on Arc: deployed with no WETH, call credit spreads escrow and settle in USDC (cashSettledCalls) src/periphery/SpreadVault.sol, test/SpreadVaultCash.t.sol, the Arc deployment notes ("Cash-settled SpreadVault") EthOnline 2026 (2026-09-13)
MarginVault, MarginBackstop, RfqVault and their settlement contracts on Arc script/DeployArcSiblings.s.sol, the Arc deployment notes EthOnline 2026 (588ff9f)
Real-USDC demo transactions as cast send calls script/arc-smoke.sh, script/arc-siblings-smoke.sh EthOnline 2026
Arc network entry in the wallet configuration and the per-chain address map frontend/config/wagmi.ts, frontend/lib/deployments.ts, .env.arc.example EthOnline 2026
Circle USDC on the Sepolia redeploy (0x1c7D…7238) the Sepolia deployment notes, script/Deploy.s.sol EthOnline 2026
Arc subgraph smile-arc-testnet on The Graph Studio subgraph/networks.json, frontend/lib/deployments.ts EthOnline 2026 (a2bf596)
Circle Gateway: Sepolia deposit, EIP-712 burn intent, attestation, mint on Arc, fundInsurance keeper/insurance-gateway.mjs EthOnline 2026 (ad42071)
Circle Developer-Controlled Wallets: entity secret, wallet set, Arc wallet, approve + deposit into the backstop keeper/backstop-wallet.mjs, @circle-fin/developer-controlled-wallets EthOnline 2026 (ad42071)
On-chain automation reused on Arc: the auto-roll keeper and the margin keeper keeper/roll.mjs, keeper/margin.mjs Pre-existing (roll.mjs); EthOnline 2026 (margin.mjs)
Yield and treasury flows reused on Arc: One-Click Income presets, the backstop pool frontend/components/IncomeOneClick.tsx, src/periphery/MarginBackstop.sol Pre-existing; EthOnline 2026

Why it is necessary

A stablecoin-native options venue. The put side of an options market is a dollar business. A cash-secured put is collateralized with the strike price in dollars, its premium is quoted in dollars, and its settlement pays the holder a dollar amount. Smile already used USDC for premiums, fees and put collateral on every chain. On Arc, the chain's gas asset is also USDC, so the last non-dollar dependency disappears: a put writer or a put buyer holds one asset and needs nothing else to transact. The MarginVault extends the same property to the margin tier, where initial margin, the backstop pool and the insurance fund are all USDC as well. The result is a venue whose quote currency is the chain's native dollar, which is the property the phrase "stablecoin-native" is meant to name.

Real USDC rather than a mock. Deploying on Arc with a mock USDC would have proven nothing that Anvil does not already prove. The deploy script's Arc branch therefore points at Circle's actual USDC contract, and every recorded fill moved real testnet USDC. The two things that remain mock, WETH and the ETH/USD feed, are mock because Arc testnet does not yet provide real ones; the deploy script and the deployment notes say so explicitly rather than leaving the reader to discover it.

Treasury custody without a key in the repository. The margin tier depends on two pools of capital that must be funded by someone: the backstop pool, which absorbs positions nobody bought at auction, and the insurance fund, which is drawn after the backstop. A protocol treasury that funds those pools from a private key stored in a script is a liability. Circle's Developer-Controlled Wallets let the treasury be a wallet that Circle custodies and signs for; the only credential in the operator's possession is an entity secret, and the repository holds neither it nor any private key. Circle Gateway addresses the complementary problem of getting USDC onto Arc from wherever it already sits, without a bridge contract of Smile's own and without a wrapped token.

Market value add

For traders. A buyer of a put on Smile-on-Arc funds one balance, pays the premium and the gas from it, and receives settlement into it. There is no second gas token to acquire and no wrapped asset to unwrap. This is the user experience of a centralized, USD-settled venue such as Deribit's USDC-margined products, delivered by a non-custodial contract on a public chain.

For liquidity providers. A put writer's collateral stays in their own wallet until a buyer matches, is pulled just in time through 1inch Aqua, and is denominated in the same asset that pays the writer's premium and gas. A writer on the margin tier posts initial margin instead of the full strike (1.50 USDC instead of 3.00 USDC in the recorded Arc fill), and the capital that protects holders when a writer fails is held in USDC pools funded through Circle's own tooling.

For the protocol treasury. The backstop pool is a yield-bearing treasury position: depositors receive shares and earn the absorbed positions' upside, and naked notional across the vault is capped at seven times the pool. Funding it from a Circle-custodied wallet and topping up the insurance fund through Gateway turn "the treasury" from a spreadsheet entry into an auditable set of on-chain transactions originating from a wallet Smile does not hold the key to.

Compared with USD-settled centralized venues. Deribit settles its USDC products against an index it computes and holds the customer's collateral. Smile-on-Arc settles against an on-chain price round, holds no customer funds before a match, and lets the customer verify every transfer on the explorer. The comparison is not that Smile is more liquid, which it is not, but that it delivers the same single-currency experience without custody.

Technical details

The deploy script's Arc branch

The deploy script selects token and oracle addresses by chain id. On Arc it deploys a mock WETH and a mock ETH/USD aggregator, then points the USDC slot at Circle's real contract.

script/Deploy.s.sol

} else if (block.chainid == 5042002) {
    // ── Arc testnet: Circle's REAL USDC — the chain's native asset,
    //    6-dec ERC-20 view — for premiums, fees, and put collateral.
    //    No canonical WETH on Arc and no Chainlink-style ETH/USD feed
    //    documented there yet (the Arc plan, step X1),
    //    so the call-side collateral and the spot oracle stay mock. ──
    vm.startBroadcast(deployerKey);
    MockERC20 arcWeth = new MockERC20("Wrapped Ether", "WETH", 18);
    arcWeth.mint(deployer, 100e18);
    MockV3Aggregator arcOracle = new MockV3Aggregator(8, 3000e8);
    vm.stopBroadcast();
    usdcAddr   = 0x3600000000000000000000000000000000000000; // Arc USDC (docs.arc.io contract addresses)
    wethAddr   = address(arcWeth);
    oracleAddr = address(arcOracle);

The full stack cost about 0.46 USDC of gas to deploy on 2026-09-10; the margin and RFQ siblings cost about 0.58 USDC more the same evening. Every address is listed in the Arc deployment notes.

Demo transactions as cast send

Foundry's forge script simulates every transaction locally before broadcasting. Arc's USDC is a native-asset system contract, and the local simulator (revm) cannot execute it from fetched bytecode; it fails with StackUnderflow before anything is sent. The node itself, and MetaMask, execute it without difficulty. The demo scripts therefore use cast send, which skips the local simulation.

script/arc-smoke.sh

send() { cast send --rpc-url "$RPC" --private-key "PRIVATEKEY"json"PRIVATE_KEY" --json "@" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["transactionHash"], "status", d["status"])'; }
...
echo "authorizeRange (authId $AUTH): $(send $VAULT 'authorizeRange(uint256,uint256,uint256,uint256,address,address,bool)' 2800000000000000000000 3200000000000000000000 $EXPIRY 5000000000000000000 $WETH $USDC true)"
...
echo "buy $UNITS @ 3000: $(send $VAULT 'buy(uint256,uint256,uint256,uint256)' $AUTH 3000000000000000000000 $UNITS $MAX)"

The recorded run on 2026-09-10 (deployer as LP, buyer and fee recipient, 0.01 units) produced the following on-chain results, with full hashes in the deployment notes:

Step Result
authorizeRange for calls at $2,800–$3,200 with real-USDC premium tx 0x586eb3a4…aa6aae
buy 0.01 units at $3,000 OptionToken 0x9b12…e128 minted
SpreadVault.openStructure 3000/3200 call credit, then buy 0.01 units (first, WETH-collateralized vault, 2026-09-10) SpreadToken 0xFAEe…ea70 minted; 0.000625 WETH pulled where a naked leg would lock 0.01 WETH
SpreadVault.openStructure 2600/2800 call credit on the cash-settled vault, then buy 0.01 units (2026-09-13) SpreadToken 0x70d5…FBF2 minted; exactly 2.00 USDC pulled — K2−K1 per unit — where a naked call would need 0.01 ETH, which Arc does not have
MarginBackstop.deposit and MarginVault.fundInsurance seeded in USDC pool holds 30 USDC, fund holds 4 USDC
MarginVault.buy 0.001 units of the $3,000 put 1.50 USDC of initial margin pulled, not the 3.00 USDC strike
RfqVault.fill of an LP-signed quote at 0.688860 USDC formula Ask was 0.695819; 0.001 WETH pulled just in time

The wallet configuration and address map

The application defines Arc as a chain in the wagmi configuration. The native currency is USDC, shown with 18 decimals in the native view; the ERC-20 view at 0x3600…0000 has 6 decimals.

frontend/config/wagmi.ts

export const arcTestnet = defineChain({
  id: 5042002,
  name: "Arc Testnet",
  nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } },
  blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } },
  testnet: true,
});

One build serves every chain. The connected chain id selects its own contract addresses, subgraph endpoint and explorer from frontend/lib/deployments.ts; the Arc entry carries the subgraph URL https://api.studio.thegraph.com/query/44448/smile-arc-testnet/v0.0.1 and the note "Circle's native USDC — premium, collateral, margin, backstop, and gas". For local development, .env.arc.example holds the same addresses and is copied to frontend/.env.local.

Circle Gateway: funding the insurance fund from another chain

Circle Gateway maintains a unified USDC balance: a depositor places USDC into a Gateway wallet contract on any supported chain, signs a burn intent, receives an attestation from Circle, and mints native USDC on the destination chain. The keeper script performs that sequence from Sepolia (domain 0) to Arc (domain 26) and then deposits the minted USDC into MarginVault.fundInsurance. No bridge contract of Smile's is involved and no wrapped token is created.

keeper/insurance-gateway.mjs

const burnIntent = { maxBlockHeight, maxFee, spec };
const signature = await account.signTypedData({ domain: { name: "GatewayWallet", version: "1" }, types, primaryType: "BurnIntent", message: burnIntent });
const res = await fetch(`${GATEWAY_API}/v1/transfer`, { method: "POST", headers: { "content-type": "application/json" }, body: json([{ burnIntent, signature }]) });
const transfer = await res.json();
if (!transfer.attestation) { console.error("transfer failed:", transfer); process.exit(1); }
console.log(`attestation ${transfer.transferId} · fees ${usd(transfer.fees?.total ?? 0)} USDC`);

// ── 3. mint on Arc, fund the insurance pool ──────────────────────────────────
const before = await arcPub.readContract({ address: ARC.usdc, abi: ERC20, functionName: "balanceOf", args: [account.address] });
const mintHash = await arcWal.writeContract({ address: GATEWAY_MINTER, abi: MINTER, functionName: "gatewayMint", args: [transfer.attestation, transfer.signature], chain: null });

Recorded run (2026-09-12): a 5 USDC Gateway balance already on the Sepolia domain carried a 3 USDC burn intent (Gateway requires value plus fee to fit the balance); Circle returned attestation ee4b1e71-… with a 0.000001 USDC fee, GatewayMinter.gatewayMint credited 2.997032 native USDC on Arc (0xa5baa3e5…) and MarginVault.fundInsurance took the fund from 4.003514 to 7.000546 USDC (0xc5493a8e…).

The script is idempotent in the sense that a Gateway balance already credited on the source domain is spent before a new deposit is made, and it waits for source-chain finality (about nineteen minutes on Sepolia) by polling Circle's balance endpoint. It is run with PRIVATE_KEY=0x… AMOUNT=5 node insurance-gateway.mjs from the keeper directory.

Circle Developer-Controlled Wallets: the treasury that funds the backstop

A developer-controlled wallet is an account whose key Circle generates and holds; the developer authorizes transactions through Circle's API using an entity secret that Circle encrypts with the entity's public key. The keeper script registers that secret once, creates a wallet set named "Smile treasury" with one externally owned account on ARC-TESTNET, and then submits ordinary contract executions: USDC.approve(backstop) followed by MarginBackstop.deposit.

keeper/backstop-wallet.mjs

// A contract call through Circle: they build, sign and broadcast it; we poll
// until it is on chain and return the hash.
async function exec(walletId, contractAddress, abiFunctionSignature, abiParameters) {
  const { data } = await client.createContractExecutionTransaction({
    walletId, contractAddress, abiFunctionSignature, abiParameters,
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });
  ...
}
...
if (cmd === "deposit") {
  if (balance < amount + 200_000n) { console.error(`wallet holds ${usd(balance)} USDC; needs ${usd(amount)} + gas`); process.exit(1); }
  const before = await pub.readContract({ address: BACKSTOP, abi: POOL, functionName: "totalAssets" });
  const a = await exec(w.walletId, USDC, "approve(address,uint256)", [BACKSTOP, amount.toString()]);
  console.log(`USDC.approve(backstop) → EXPLORER{EXPLORER}{a}`);
  const d = await exec(w.walletId, BACKSTOP, "deposit(uint256)", [amount.toString()]);

Recorded run (2026-09-12): the treasury wallet 0x61bd6c481248f2e5bfd6d0aadf5215f353dc3368, funded with 1.5 USDC by the deployer (0x2c25a967…), submitted USDC.approve (0x95005ec6…) and MarginBackstop.deposit of 1 USDC (0xbfd2db0a…), taking the pool from 30.002108 to 31.002108 USDC; both transactions were built, signed and broadcast by Circle. The Margin tab lists them under "Funded through Circle App Kits".

The script has four commands: setup (generate and register the entity secret; a recovery file is written next to the script), wallet (create or show the Arc wallet), deposit (approve and deposit AMOUNT USDC into the backstop pool) and withdraw (request withdrawal of all shares; withdraw() opens after the pool's 24-hour delay). Wallet state lives in keeper/.circle-wallet.json; that file, the recovery file and the .env holding the API key and entity secret are all ignored by git. The treasury wallet created on 2026-09-12 is 0x61bd6c481248f2e5bfd6d0aadf5215f353dc3368.

Arc, in Circle's own terms

Circle describes Arc in terms of programmable money flows, automation, yield and treasury. Each is an existing Smile flow that runs on Arc unchanged:

  • Conditional payments. An option is a conditional payment instrument: premium now, payout contingent on the settlement price. Aqua's just-in-time pull is itself conditional: collateral leaves the writer's wallet only when a buyer matches.
  • Multi-step settlement. Buy, expiry, permissionless settleWithChainlinkRound, redeem, reclaimCollateral, all as Arc transactions paid in USDC.
  • On-chain automation. keeper/roll.mjs settles, reclaims, revokes and re-ships a writer's range at the new spot with no human in the loop; keeper/margin.mjs drives margin calls, auctions, absorptions and settlement for the margin tier.
  • Yield and treasury. One-Click Income presets (covered calls and cash-secured puts with an estimated premium APR) are the yield product; the backstop pool and insurance fund, funded through Circle's App Kits, are the treasury.

The Arc subgraph

The Graph Studio subgraph smile-arc-testnet indexes AquaCollateralVault at 0xE37ED711F7D1dc5aC045206b4A6367C55229C789 from block 61,227,750 (subgraph/networks.json). On Arc, as on Sepolia, the application and the AI copilot read positions only from The Graph; there is no RPC scan on public networks. The Graph page in this sidebar covers the subgraph in detail.

Limitations

  • Covered calls need the asset, and Arc has none. A covered call is backed by ether itself, and Arc has no ether: its native asset is USDC. So the main vault's and RfqVault's calls on Arc collateralize with a freely mintable MockERC20 standing in for WETH, and those calls are demonstrations of the mechanism, not of a market. The SpreadVault no longer has this limitation: since 2026-09-13 it is cash-settled on Arc, a call credit spread escrows K2−K1 USDC per unit and settles in USDC (cashSettledCalls). Removing WETH from the last two places means USDC-margined calls in MarginVault, which is put-only by design today; that is the next step.
  • The ETH/USD price feed is a mock on Arc. No Chainlink-compatible feed is documented on Arc testnet (Pyth does not list Arc; Chainlink and RedStone show nothing; Arc's contract page lists no oracles). Quoting and settlement both read a MockV3Aggregator. Since 2026-09-12 a keeper (keeper/arc-oracle-tick.sh, a systemd timer on the dev box) mirrors Sepolia's Chainlink ETH/USD answer into it every 30 minutes, so Arc's spot tracks the real ETH price and the staleness checks in MarginVault.buy (90 minutes) and RfqVault.formulaQuote (one hour) pass; if the keeper stops, anyone may post a fresh round. The feed remains a contract anyone can set, so Arc demonstrates the mechanism, not the oracle trust model.
  • forge script cannot simulate Arc's USDC. Deploys that do not call USDC work through forge script; anything that calls USDC must be sent with cast send. The frontend is unaffected because MetaMask does not simulate locally.
  • No liquidation run on a live chain. The margined put fill is on Arc, but the crash-to-auction-to-settlement path relies on time warps and lives in the Anvil script script/margin-lifecycle.sh.
  • Arc's RPC blocks well-known development keys. At least one default Anvil/Hardhat key returns "Blocked address"; a fresh key is required.
  • Faucet USDC is both gas and balance. The faucet grants 20 USDC per address every two hours; spending premium reduces the gas balance and the reverse.
  • Both App Kit flows have exactly one recorded run each (2026-09-12). Gateway moved 2.997032 USDC from a Sepolia deposit into MarginVault.fundInsurance (mint 0xa5baa3e5…, fund 0xc5493a8e…) and the Circle-custodied treasury wallet 0x61bd…3368 deposited 1 USDC into the backstop (0xbfd2db0a…); the Arc deployment notes list every hash. Two details learned on that run are now in the keeper: Circle's Gateway API returns amounts as decimal strings, and a transfer requires value plus fee to fit the Gateway balance, so the 5 USDC balance carried a 3 USDC intent. The treasury wallet holds about 0.5 USDC after the deposit; further deposits need a top-up from the faucet or the deployer.
  • RfqVault is on Arc but not on Sepolia. The Sepolia address map leaves rfqVault empty.
  • The gas floor is Arc's to set (L12). Every first fill in a series pays roughly 1,040,000 gas to deploy the series token and every repeat fill roughly 198,000; on Arc that gas is denominated in USDC, so the minimum economical trade size is a direct function of Arc's gas price. The ~0.46 USDC full-stack deploy suggests the floor is small on testnet; mainnet pricing is unknown until 2026-09-16.
  • Arc mainnet is not live. Arc mainnet launches on 2026-09-16; every figure on this page is testnet.

Plans

  • FX options on Arc (USDC/EURC). Mechanically the same engine pointed at a EUR/USD feed with EURC in the call-collateral slot. Task X1 of the Arc plan found the only oracle with a documented Arc testnet deployment to be Stork (0xacC0a0cF13571d30B4b8637996F5D6D774d4fd62), a pull-model oracle that requires an adapter in the shape of the existing PythSpotAdapter, an update-posting flow and an API key. It is recorded as the lead for the 2026-09-16 to 2026-09-30 window.
  • Arc mainnet. Task X6 of the plan is the mainnet deploy of the same script against Arc's mainnet RPC once Circle publishes it, treated with the care of a real-money deploy. The mainnet deployment is a post-launch follow-up.
  • Keep the treasury funded and automate the top-ups. Both keepers have run once (2026-09-12; hashes in the Arc deployment notes and on the Margin tab). The next step is scheduling them: a cron or CRE trigger that tops up the backstop from the Circle wallet when totalAssets falls below a floor and refills the insurance fund through Gateway when a haircut draws it down, so the treasury is an automated money flow rather than a manual keeper run.
  • A real feed when Arc provides one. Replacing the mock aggregator is a one-branch change in script/Deploy.s.sol.
  • USDC-margined calls. Extend MarginVault from puts to calls so that no product on Arc needs a WETH stand-in: the last step to a fully USDC-native venue.
  • Gateway onboarding in the application. Task X4 scopes a frontend flow that lets a user with USDC on another chain act on Smile-on-Arc through Gateway's unified balance rather than a manual bridge step.

Glossary

  • Arc. Circle's EVM-compatible blockchain, on which USDC is the native gas asset. The testnet has chain id 5042002, RPC https://rpc.testnet.arc.network and explorer https://testnet.arcscan.app. Mainnet launches on 2026-09-16.
  • Native USDC (on Arc). The chain's gas asset. The system contract at 0x3600000000000000000000000000000000000000 exposes it as a 6-decimal ERC-20 token; the native view used by wallets shows 18 decimals.
  • USDC. Circle's dollar stablecoin. On Sepolia, Smile uses Circle's Sepolia USDC at 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238.
  • EURC. Circle's euro stablecoin, the intended call-collateral asset for a future FX options product on Arc.
  • Chain id 5042002. The numeric identifier of Arc testnet, used by the deploy script's branch, the wagmi chain definition and the address map.
  • Faucet. Circle's testnet faucet at https://faucet.circle.com, which grants 20 USDC per address every two hours on Arc testnet.
  • Gateway. Circle's cross-chain USDC service: a unified balance funded by deposits into a Gateway wallet contract on any supported chain and spent by minting native USDC on a destination chain. Chains are identified by numeric domains; Sepolia is domain 0 and Arc is domain 26 in the keeper script.
  • Gateway wallet and Gateway minter. The two Gateway contracts: GatewayWallet (0x0077777d7EBA4688BDeF3E311b846F25870A19B9) receives deposits on the source chain and GatewayMinter (0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) mints on the destination chain.
  • BurnIntent. The EIP-712 typed message a depositor signs to authorize Gateway to burn a deposited amount on the source domain and mint it on the destination domain. It carries a transfer specification, a maximum fee and a maximum block height.
  • Attestation. Circle's signed confirmation, returned by the Gateway API's transfer endpoint, that a burn intent is valid; the attestation and its signature are the arguments to gatewayMint.
  • EIP-712. The Ethereum standard for signing structured, human-readable typed data. Smile uses it for Gateway burn intents and for the RFQ vault's signed quotes.
  • Developer-controlled wallet. A wallet in Circle's Wallets product whose private key Circle generates and custodies; the developer authorizes transactions through Circle's API.
  • Entity secret. The 32-byte credential a developer registers with Circle, encrypted with the entity's public key, that authorizes transactions from developer-controlled wallets. Smile keeps it in an ignored .env file and never in the repository.
  • Wallet set. Circle's grouping of developer-controlled wallets under one entity; Smile's is named "Smile treasury".
  • Recovery file. The file Circle returns when an entity secret is registered, used to recover access if the secret is lost; it is written next to the keeper script and ignored by git.
  • Initial margin. The amount a put writer on the margin tier must post at fill, computed from the lowest Chainlink answer of the last hour; 1.50 USDC for the recorded Arc fill against a 3.00 USDC strike.
  • Backstop pool. MarginBackstop, a share-based USDC pool that absorbs margined positions nobody bought at auction; naked notional across the vault is capped at seven times its assets.
  • Insurance fund. A USDC reserve inside MarginVault, drawn after the backstop pool and before any haircut, funded through fundInsurance.
  • Just-in-time (JIT) pull. 1inch Aqua's custody model, in which a writer's collateral stays in their wallet until a buyer matches and is then pulled by the vault in the same transaction.
  • cast. Foundry's command-line tool for sending transactions and calling contracts directly against a node, without local simulation.
  • forge script. Foundry's scripting runner, which simulates transactions locally in revm before broadcasting them; it cannot execute Arc's native-asset USDC contract.
  • revm. The Rust EVM implementation Foundry uses for local simulation.
  • Subgraph. An indexed view of contract events served by The Graph; smile-arc-testnet is the one for Arc.
  • One-Click Income. Smile's covered-call and cash-secured-put presets with an estimated premium APR, the yield product.
  • Keeper. A script that performs permissionless or self-custodial maintenance transactions on a schedule; Smile ships roll.mjs, margin.mjs, insurance-gateway.mjs and backstop-wallet.mjs.

The Smile Frontend

This page describes the Smile web application: what it is, where it runs, what was added to it at EthOnline 2026, and how the TradingView-engine price chart works. It is written for a reader who has used a trading application before but has not read the rest of the documentation. Every term of art is defined where it first appears, and a glossary closes the page.

Summary

The frontend is a Next.js application in frontend/ that puts every Smile vault on one screen: buying options from the on-chain price surface, writing ranges as a liquidity provider (LP), the three sibling vaults built at the event (Spreads, Margin, RFQ), a risk monitor for the margin tier, a strategy builder, a vol-surface view, and an AI copilot. One build serves three networks: the local Anvil chain, Ethereum Sepolia, and Circle's Arc testnet. The application reads its contract addresses from the connected chain (frontend/lib/deployments.ts and frontend/config/wagmi.ts), so switching networks in the wallet switches the whole app.

The same code ships in two shapes. The static export at https://oslinin.github.io/Smile/ is built by GitHub Actions with a base path and has no server, so it carries no copilot. The server build at https://smile-frontend-omega.vercel.app runs on Vercel and adds the copilot route (/api/copilot) and the subgraph proxy (/api/subgraph), through which the application reads The Graph with a gateway key that never reaches the browser. Locally, ./local.sh starts Anvil, deploys every contract, seeds a 100-trade tape, and runs the server build against it.

Before EthOnline 2026 the app had the option matrix, range authorization, a payoff builder, an LP dashboard, the vol surface, and the copilot. The event added the Overview landing tab, the Spreads, Margin, RFQ and Risk Monitor tabs, a TradingView-engine price chart with the strategy and the traded tape drawn on it, an OptionStrat-grade builder, recorded testnet receipts, the multi-chain address map, a User Guide, the integration help pages, and a copilot that trades off The Graph with skills, MCP servers and preparation cards.

Features used

Feature Where in the code Pre-existing or EthOnline 2026
Wallet stack: wagmi + viem, injected connector, WalletConnect when a project id is set; no Privy, no embedded wallets frontend/config/wagmi.ts, frontend/app/providers.tsx Pre-existing
One build for Anvil / Sepolia / Arc: recorded addresses per chain, env addresses as the Anvil fallback, CONTRACTS follows the connected chain frontend/config/wagmi.ts (DEPLOYED_ADDRESSES, contractsFor, CONTRACTS proxy), frontend/lib/deployments.ts EthOnline 2026 (33cb2d3)
Arc Testnet network entry and wallet_addEthereumChain payload frontend/config/wagmi.ts (arcTestnet), frontend/app/page.tsx (ADDABLE_CHAINS) EthOnline 2026 (108e25d)
Overview tab: capital-efficiency ladder as live bars, live counters across vaults, recorded testnet receipts frontend/components/Story.tsx EthOnline 2026 (33cb2d3)
Tabs in user language: Overview · Trade · Earn · One-Click · Earn · Write a Range · Spreads · Margin · Risk Monitor · RFQ · My Positions · Vol Surface · Receipts frontend/app/page.tsx (TABS) EthOnline 2026 (33cb2d3)
Spreads tab: open, ship and fill a credit spread on SpreadVault frontend/components/SpreadDesk.tsx EthOnline 2026 (a5c17eb)
Margin tab: margined put ranges, pool and fund dials, and the Circle App Kits receipts frontend/components/MarginDesk.tsx EthOnline 2026 (1cfcc39, a412b5f)
RFQ tab: LP signs EIP-712 quotes in the wallet, taker fills frontend/components/RfqDesk.tsx (useSignTypedData) EthOnline 2026 (1006a2d)
Risk Monitor: health bars per position, vault risk dials, liquidation timeline rebuilt from events, "Explain with the copilot" frontend/components/RiskMonitor.tsx EthOnline 2026 (de5b3c2)
Strategy builder: today / halfway / expiry curves, price × date P&L heat map, breakevens, greeks, per-vault writer collateral frontend/components/PayoffBuilder.tsx, frontend/lib/options.ts (pnlSeries, pnlMatrix, writerCollateral, strategyStats) Pre-existing builder; EthOnline 2026 upgrade (0c35e99)
Price chart: TradingView Lightweight Charts 5.2.1, ETH/USD hourly candles (Coinbase, Kraken fallback), strategy overlay, per-instrument premium and implied vol from the tape frontend/components/PriceChart.tsx EthOnline 2026 (5098a25, 9235276)
The tape: ranges, instruments, fills and positions from The Graph on public chains, from event logs on Anvil frontend/lib/tape.ts, frontend/lib/subgraph.ts EthOnline 2026 (38f8922)
Subgraph proxy so a gateway key stays server-side; per-chain gateway URLs frontend/app/api/subgraph/route.ts EthOnline 2026 (38f8922, e9feda6)
LP Dashboard shows the connected wallet's own active range, read from the subgraph frontend/components/LPDashboard.tsx EthOnline 2026 fix (dfc964a, b818634)
Copilot panel: tab-aware context, per-tab starter prompts, Skills menu, MCP servers with The Graph preset, preparation cards frontend/components/copilot/CopilotPanel.tsx, SkillsMenu.tsx, CopilotSettings.tsx, PrepareCard.tsx, frontend/lib/copilot/tabs.ts EthOnline 2026 (5f1b9b5, 39e9a41, 00cbbe3, 16ad3f5)
OpenRouter as a fourth copilot provider frontend/lib/copilot/provider.ts EthOnline 2026 (ede13fd)
Live spot: Uniswap Trading API when a key is set, Chainlink feed read otherwise, static fallback last frontend/hooks/useUniswapSpot.ts Pre-existing
Help site generator with an Integrations group; knowledge pack for the copilot frontend/scripts/gen-help.mjs, frontend/scripts/gen-knowledge.mjs Pre-existing generators; Integrations group and pages 2026-09-12 (1cb0cae)
User Guide in the help sidebar and in the copilot's knowledge User Guide EthOnline 2026 (0c35e99)
GitHub Pages static export and the Vercel server build .github/workflows/pages.yml, frontend/next.config.ts Pages pre-existing; continuation-branch deploys and Vercel 2026-09-12

Why it is necessary

A first look lasts three minutes. The numbers that make Smile's case are concrete: a 3000/3200 call credit spread escrows 0.0625 WETH instead of 1 WETH, a margined put locks 1,500 USDC instead of 3,000, and holders stay whole after a 40% gap. A README can state those numbers; only a screen can show them being true on the connected chain. The Overview tab exists to put the ladder on screen as live bars, with the vault counters and the recorded receipts beside it, before the viewer clicks anything.

A venue needs a tape and a chart. An options venue whose trades are only visible as transaction hashes has no market. The subgraph gives Smile a tape (every range, instrument, fill and position), and the price chart draws that tape as premium and implied volatility over time next to the underlying's candles. Without the chart, the σ feedback loop and the price history are invisible; with it, a viewer can watch a fill move the surface.

A margin tier needs a monitor. MarginVault is the one place a written option can fail to pay in full. A liquidation waterfall (margin call, grace period, takeover auction, backstop pool, insurance fund, haircut) that runs only in a shell script is not something a writer can trust. The Risk Monitor shows each position's health against the live mark and replays the waterfall as it happens.

Three new vaults need three new desks. Each sibling vault has a different write path (a two-leg structure, a margined range, a signed quote) and a different number to show (netted escrow, initial margin, price improvement). A single "Trade" form cannot express them; the Spreads, Margin and RFQ tabs each exist to show their one number next to the action that produces it.

Market value add

For a trader, the application is the OptionStrat and Deribit workflow on a non-custodial venue. The builder shows a strategy's payoff at expiry, its value today and halfway to expiry, a price × date profit-and-loss heat map, the breakevens, and the greeks, with the premium quoted from Smile's own surface rather than a guess. The chart draws the strikes and breakevens over real candles so a trade is placed against the market's actual history. The tape shows what the last fill paid and what implied volatility it implied.

For a liquidity provider, the Earn tabs and the Spreads, Margin and RFQ desks show the capital each tier locks for the same trade, and the LP Dashboard and Risk Monitor show what happens to that capital afterwards. A writer can compare a naked put, a credit spread, and a margined put on one screen before choosing a rung.

For a reviewer or an integrator, the Overview tab's receipts and the Receipts tab link every recorded testnet transaction to its explorer, per chain. The Vercel build carries the copilot, so the AI trading agent described on The Graph page can be tried without any setup.

Technical details

The chart: TradingView Lightweight Charts

What it is. Lightweight Charts is TradingView's open-source charting engine, published as the lightweight-charts npm package under the Apache-2.0 licence. It is the renderer behind TradingView's charts, not the TradingView website or its embeddable widget: it draws candles, lines and price lines on a canvas from data the application supplies, with no account, no data feed and no network calls of its own. Smile pins version 5.2.1 (frontend/package.json).

Why this library. There is no open-source OptionStrat, and the nearest React project draws expiry payoffs only, so the builder stayed in-house. For charting, a component was needed rather than a product; OpenCharts, an MIT-licensed terminal built on the same engine, is a full standalone application, not a component to embed. Using TradingView's engine directly gives a trader the chart they already know, in a component the page controls.

What is drawn. Hourly ETH/USD candles from Coinbase Exchange's public candles endpoint, with Kraken's OHLC endpoint as the fallback; the protocol's spot as a dotted line; every leg's strike as a solid line, green for long and red for short; each breakeven as a dashed yellow line; and, in the lower third, one selected instrument's traded premium per unit and the implied volatility that premium means, computed in the browser by inverting Black-Scholes against the candle close at that hour. Market data is context for the trade; the protocol prices off its oracle, not off these candles.

frontend/components/PriceChart.tsx

async function fetchCandles(): Promise<{ candles: Candle[]; source: string }> {
  try {
    const r = await fetch("https://api.exchange.coinbase.com/products/ETH-USD/candles?granularity=3600");
    if (!r.ok) throw new Error(String(r.status));
    const rows = (await r.json()) as number[][]; // [time, low, high, open, close, volume], newest first
    const candles = rows.map((c) => ({ time: c[0] as UTCTimestamp, low: c[1], high: c[2], open: c[3], close: c[4] })).sort((a, b) => a.time - b.time);
    return { candles, source: "Coinbase ETH-USD · 1h" };
  } catch {
    const r = await fetch("https://api.kraken.com/0/public/OHLC?pair=ETHUSD&interval=60");
    const j = (await r.json()) as { result: Record<string, (string | number)[][]> };
    const key = Object.keys(j.result).find((k) => k !== "last") ?? "";
    const candles = (j.result[key] ?? []).map((c) => ({ time: Number(c[0]) as UTCTimestamp, open: Number(c[1]), high: Number(c[2]), low: Number(c[3]), close: Number(c[4]) }));
    return { candles, source: "Kraken ETH/USD · 1h" };
  }
}

The chart is created once, with the candles on the right price scale and the two tape lines on their own scales in the lower third of the canvas:

    const s = c.addSeries(CandlestickSeries, { upColor: "#22c55e", downColor: "#ef4444", borderVisible: false, wickUpColor: "#22c55e", wickDownColor: "#ef4444" });
    // Tape lines live in the lower third: premium on the (left) price axis,
    // IV on an overlay scale with the same margins.
    premSeries.current = c.addSeries(LineSeries, { color: "#a78bfa", lineWidth: 2, priceScaleId: "left", priceFormat: { type: "price", precision: 2, minMove: 0.01 }, title: "premium" });
    ivSeries.current = c.addSeries(LineSeries, { color: "#f472b6", lineWidth: 2, priceScaleId: "iv", priceFormat: { type: "percent", precision: 1, minMove: 0.1 }, title: "IV" });
    c.priceScale("iv").applyOptions({ scaleMargins: { top: 0.68, bottom: 0.02 } });

The strategy overlay is redrawn whenever the builder's legs or the spot change, using the engine's price lines:

    const add = (price: number, color: string, title: string, style = LineStyle.Solid, width: 1 | 2 = 1) =>
      lines.current.push(s.createPriceLine({ price, color, title, lineStyle: style, lineWidth: width, axisLabelVisible: true }));
    add(spot, "#60a5fa", "Smile spot", LineStyle.Dotted, 1);
    for (const leg of legs) {
      add(leg.strike, leg.direction === "buy" ? "#22c55e" : "#ef4444", `${leg.direction === "buy" ? "long" : "short"} ${leg.isCall ? "call" : "put"} ${leg.amount}×`, LineStyle.Solid, 2);
    }
    if (legs.length > 0) {
      for (const be of findBreakevens(pnlSeries(legs, spot))) add(Math.round(be), "#fbbf24", "breakeven", LineStyle.Dashed, 1);
    }

Implied volatility is recovered from each fill by bisection, since the Black-Scholes price is monotone in volatility; a premium below intrinsic value yields null and is left off the line:

function impliedVol(premium: number, spot: number, strike: number, tYears: number, isCall: boolean): number | null {
  const type = isCall ? "call" : "put";
  let lo = 0.01, hi = 5;
  if (blackScholes(spot, strike, tYears, lo, RISK_FREE_RATE, type) > premium) return null;
  for (let i = 0; i < 60; i++) {
    const mid = (lo + hi) / 2;
    if (blackScholes(spot, strike, tYears, mid, RISK_FREE_RATE, type) > premium) hi = mid; else lo = mid;
  }
  return (lo + hi) / 2;
}

The legend names the tape's source ("The Graph" or "Anvil event log") so a viewer knows where the fills came from.

One build, three chains

Recorded testnet addresses live in DEPLOYED_ADDRESSES; the environment supplies the Anvil addresses that ./local.sh writes. CONTRACTS is a proxy that resolves each key against the chain the page has set, so every component reads the right vault without knowing which chain it is on:

frontend/config/wagmi.ts

let activeChainId = 0;
export function setActiveChainId(id: number) { activeChainId = id; }
export function contractsFor(chainId: number): ContractMap { return DEPLOYED_ADDRESSES[chainId] ?? ENV_CONTRACTS; }

export const CONTRACTS = new Proxy(ENV_CONTRACTS, {
  get(target, key: string) {
    const table = DEPLOYED_ADDRESSES[activeChainId];
    return (table ?? target)[key as ContractKey];
  },
}) as Omit<ContractMap, "usdc" | "weth"> & { usdc: Addr; weth: Addr };

frontend/lib/deployments.ts holds the same chains' explorer URLs, contract lists, subgraph endpoints, a one-line "what is real money here" note, and the demo receipts. The Overview and Receipts tabs render the receipts, and the Margin tab filters the ones whose label begins with "Treasury ·" into its "Funded through Circle App Kits" block:

frontend/lib/deployments.ts

export type DemoTx = { label: string; hash: string; note?: string };
export type Deployment = {
  chainId: number;
  name: string;
  explorer: string;
  contracts: { label: string; address: string }[];
  demo: DemoTx[];
  subgraph?: string;
  realMoney: string;
};

frontend/components/MarginDesk.tsx

  const dep = chainId ? DEPLOYMENTS[chainId] : undefined;
  const treasury = dep?.demo.filter((t) => t.label.startsWith("Treasury ·")) ?? [];

The Overview ladder

The landing tab computes the at-the-money strike from the live spot and reads MarginVault.marginRequirement for it, then draws four rungs whose bar widths are the collateral each tier locks for the same trade:

frontend/components/Story.tsx

  const ladder = [
    { title: "Naked put", sub: "the main vault · cash-secured", value: k, note: `${usd0(k)} USDC locked per unit`, color: "bg-blue-700", cta: "Trade", tab: "chain" as TabId, tone: "old" as const },
    { title: "Credit spread", sub: `SpreadVault · usd0(k)/{usd0(k)}/{usd0(k2)}`, value: k2 - k, note: `${usd0(k2 - k)} USDC — the true max loss, ${(k / (k2 - k)).toFixed(0)}× less`, color: "bg-green-600", cta: "Spreads", tab: "spreads" as TabId, tone: "new" as const },
    { title: "Margined put", sub: "MarginVault · opt-in, IM off the worst-of-hour mark", value: imUsd, note: `${usd0(imUsd)} USDC initial margin — ${(k / Math.max(imUsd, 1)).toFixed(1)}× less, liquidation-backed`, color: "bg-emerald-600", cta: "Margin", tab: "margin" as TabId, tone: "new" as const },
    { title: "Signed quote", sub: "RfqVault · LP-signed price, same collateral rules", value: k, note: "any price the LP signs — the custody model never changes", color: "bg-teal-700", cta: "RFQ", tab: "rfq" as TabId, tone: "new" as const },
  ];

The tape and its chain-id gate

Every read of ranges, instruments, fills or positions goes through readTape. On a chain with a subgraph endpoint the tape comes from The Graph; on the local Anvil chain, where no graph-node runs on this project's arm64 host, the same entities are rebuilt from eth_getLogs; on any other chain without a subgraph the call throws rather than falling back to a capped scan:

frontend/lib/tape.ts

export class SubgraphRequiredError extends Error {
  constructor(chainId?: number) {
    super(`No subgraph configured for chain ${chainId ?? "unknown"} — on public networks The Graph is the only position source (no RPC scan exists).`);
  }
}

/** Ranges, instruments and fills for the chain. Subgraph on public chains, event logs on Anvil. */
export async function readTape(opts: TapeOpts): Promise<Tape> {
  const url = subgraphUrlFor(opts.chainId);
  if (url) return tapeFromSubgraph(url, opts.since ?? 0);
  if (isLocalChain(opts.chainId) && opts.client && opts.vault) return (await stateFromLogs(opts.client, opts.vault)).tape;
  throw new SubgraphRequiredError(opts.chainId);
}

In the browser on the server build, subgraphUrlFor returns the proxy path /api/subgraph/?chainId=…; the route resolves a per-chain gateway URL (SUBGRAPH_URL_11155111, SUBGRAPH_URL_5042002) or a global one from server-side environment variables and otherwise forwards to the recorded Studio endpoint. The static export has no route and calls the Studio endpoint directly.

The builder's heat map

The strategy builder draws three profit-and-loss curves with Recharts and a price × date heat map as a table whose cell colour scales with the profit or loss at that price on that day. The matrix comes from pnlMatrix in frontend/lib/options.ts (fifteen prices by eight dates by default):

frontend/components/PayoffBuilder.tsx

function HeatMap({ legs, spot }: { legs: Leg[]; spot: number }) {
  const m = useMemo(() => pnlMatrix(legs, spot), [legs, spot]);
  const scale = useMemo(() => Math.max(1, ...m.pnl.flat().map((v) => Math.abs(v))), [m]);
  const cell = (v: number) => {
    const a = Math.min(1, Math.abs(v) / scale) * 0.85 + 0.08;
    return v >= 0 ? `rgba(34,197,94,a):rgba(239,68,68,{a})` : `rgba(239,68,68,{a})`;
  };

The builder's greeks (delta, gamma, theta per day, vega) come from strategyStats, which uses the black-scholes and greeks npm packages; the per-leg "what the writer locks on each vault" panel comes from writerCollateral, which reports the naked, netted and margined collateral for every leg.

The Risk Monitor

The monitor reads every MarginVault event (MarginLocked, Flagged, AuctionStarted, TakenOver, Absorbed, PositionSettled, SeriesFinalized, HolderHaircut, and others) every five seconds, derives the set of positions from them, and reads each position's health from the vault. A bar shows the locked margin against the maintenance and initial thresholds:

frontend/components/RiskMonitor.tsx

        <div className={`absolute inset-y-0 left-0 ${healthy ? (locked >= im ? "bg-green-600" : "bg-yellow-600") : "bg-red-600"} transition-all duration-700`} style={{ width: pct(locked) }} />
        <div className="absolute inset-y-0 w-0.5 bg-white/70" style={{ left: pct(mm) }} title="maintenance" />
        <div className="absolute inset-y-0 w-0.5 bg-white/30" style={{ left: pct(im) }} title="initial" />

The timeline renders each event as a sentence a writer can act on; the "Explain with the copilot" button, shown only when the copilot is enabled, hands the recent events to the chat.

The RFQ desk

The LP signs a quote as EIP-712 typed data in the wallet, with no gas, through wagmi's useSignTypedData; the taker's fill is an ordinary contract call:

frontend/components/RfqDesk.tsx

  const { signTypedDataAsync, isPending: signing, error: signError } = useSignTypedData();
    const signature = await signTypedDataAsync({
      types: QUOTE_TYPES, primaryType: "Quote", message: quote,

The copilot panel

The floating panel sends the visible spot, chain id, wallet address and the active tab with every request so the server prices exactly what the screen shows. A per-tab briefing (frontend/lib/copilot/tabs.ts) tells the model what is on screen, which guide sections explain it, and which tools fit; the panel shows that tab's starter prompts. The Skills menu toggles the eight built-in trader skills and accepts user-written ones; the settings gear holds a bring-your-own-key provider choice and the list of MCP servers, with a one-click preset for The Graph's Subgraph MCP:

frontend/components/copilot/CopilotSettings.tsx

const THEGRAPH_MCP: McpServer = { name: "thegraph", url: "https://subgraphs.mcp.thegraph.com/sse", transport: "sse" };

The two preparation cards (prepare_lp_range, prepare_rfq_quote) hand the agent's proposed numbers to the matching form, where the user reviews and signs; the copilot never holds a key. The Graph page documents the tools, skills and MCP in full.

The help site and the knowledge pack

frontend/scripts/gen-help.mjs renders the README, the User Guide, Limitations, Solutions, the copilot page and the six integration pages (the Integrations group in the sidebar) into public/help.html, with KaTeX for the README's formulae and Mermaid for its diagrams; the Reference Table and Continuation Track pages are embedded as standalone documents. frontend/scripts/gen-knowledge.mjs compiles the same documents into a token-cheap table of contents for the copilot's system prompt and full sections served on demand through its read_docs tool. Both run on predev and prebuild, so the site and the pack are never stale relative to the docs.

Two builds

frontend/next.config.ts switches on one variable: when NEXT_PUBLIC_BASE_PATH is set (the GitHub Pages workflow sets it to /Smile), the build is a static export served from a subpath, and route handlers are excluded; when it is unset, the build is a normal server build that carries /api/copilot and /api/subgraph. The Pages workflow runs on pushes to main and the continuation branch and on documentation changes; the Vercel project builds the continuation branch as production with the copilot provider, model, keys and MCP seed held as server-side environment variables.

Limitations

  • The static export has no copilot. GitHub Pages serves files only, so the copilot button is hidden there (NEXT_PUBLIC_COPILOT unset) and the subgraph is read directly from the Studio endpoint. The Vercel build is the one with the agent.
  • The premium swap is key-gated and mainnet-routed. The Uniswap Trading API is used for the displayed spot and, only when NEXT_PUBLIC_UNISWAP_API_KEY is set, to quote an ETH→USDC swap for the premium; that quote targets mainnet, so on Sepolia and Arc the buyer pays premium from USDC held already.
  • Market data is context, not pricing. The candles come from Coinbase or Kraken; the protocol prices off its oracle. The two can disagree, and the chart says so in its legend.
  • Implied vol on the chart is a browser estimate. It inverts Black-Scholes with the candle close at the fill's hour as spot and the builder's risk-free rate; the protocol's own sigma is the hook's, not this number.
  • The Risk Monitor and the Anvil tape scan logs. On Anvil both rebuild state from eth_getLogs every few seconds, which is fine for a local chain and would not scale to a busy public one; on public chains the tape comes from the subgraph, but the Risk Monitor's event timeline is still a log scan of MarginVault.
  • Transfers of option tokens are not on the tape. The subgraph does not index ERC-20 transfers of OptionTokens, so a position sold on to another wallet still shows on the original buyer until closed or redeemed (see The Graph page).
  • A weak default model. The live deployment runs the copilot on openrouter/free; the tool-routing rules in the system prompt carry more weight than they would with a stronger model, and the settings gear lets a user bring their own key.
  • Layout is desktop-first. The tab bar wraps on narrow screens and tables scroll horizontally, but the builder, chart and desks are designed for a wide viewport.

Plans

  • A live secondary market on the chart. When an OptionToken v4 pool exists (Limitations L14), its trades belong on the same chart as the primary tape.
  • Per-chain spot sources. The Trading API quote is mainnet-only; a chain-aware spot source (a feed adapter per chain, or Pyth for quoting per R5) would let the displayed spot and the protocol's spot agree on every network.
  • Index OptionToken transfers. A data-source template per OptionToken in the subgraph would make the tape's positions transfer-aware, and the My Positions tab correct after a resale.
  • Mobile layouts for the desks. The Overview and Trade tabs read well on a phone; the builder's heat map and the desks would benefit from stacked layouts.
  • Recorded liquidation on a public chain. The Risk Monitor's waterfall has been shown on Anvil only, because public testnets cannot be time-warped; a scheduled, long-dated demo on Sepolia or Arc would put a real timeline on screen.

Glossary

  • Next.js. The React framework the app is built with; it can produce a static site or a server that also runs API routes.
  • Static export. A Next.js build that emits plain HTML, CSS and JavaScript with no server, deployable to GitHub Pages; API routes cannot exist in it.
  • Server build. A Next.js build that runs on a Node.js server (here Vercel) and can serve route handlers such as /api/copilot.
  • Base path. The URL prefix (/Smile) under which the static export is served on GitHub Pages; assets must be prefixed with it or they fail to load.
  • Route handler / proxy route. A server-side endpoint inside the app. /api/subgraph forwards the browser's query to The Graph with a key the browser never sees.
  • wagmi and viem. The React hooks library and the low-level Ethereum client the app uses to connect wallets, read contracts and send transactions.
  • Injected connector. A wallet available as a browser extension (MetaMask and similar) that injects a provider into the page.
  • WalletConnect. A protocol for connecting mobile wallets by QR code; enabled only when a project id is configured.
  • Lightweight Charts. TradingView's open-source charting engine, an npm package under Apache-2.0 that draws candles, lines and price lines on a canvas from application-supplied data.
  • Candle / OHLC. One bar of price history: the open, high, low and close over an interval, here one hour.
  • Price line. A horizontal line the chart engine draws at a given price with a label; used for strikes, breakevens and the spot.
  • Tape. The record of every range, instrument, fill and position on the venue, read from The Graph on public chains and rebuilt from event logs on Anvil.
  • Instrument. One (strike, expiry, call-or-put) series, identified by its OptionToken address.
  • Premium per unit. What one option unit cost at a fill, fee included, in USD.
  • Implied volatility (IV). The volatility that, put into Black-Scholes, reproduces an observed premium; on the chart it is recovered by bisection.
  • Payoff diagram. The profit or loss of a strategy as a function of the underlying's price at expiry.
  • Today / halfway curves. The strategy's model value at the current time and at half the time to expiry, drawn with the expiry payoff.
  • Breakeven. A price at which the strategy's profit and loss is zero.
  • Heat map. A grid of profit or loss by price (rows) and date (columns), coloured by sign and magnitude.
  • Greeks. Delta (sensitivity to the underlying's price), gamma (delta's sensitivity), theta (time decay per day) and vega (sensitivity to volatility).
  • Writer collateral. What a writer locks for a leg on each vault: the naked amount on the main vault, the netted max loss on SpreadVault, the initial margin on MarginVault.
  • Ladder. The Overview tab's four rungs, from naked collateral to a signed quote, drawn as bars sized by the collateral each locks.
  • Receipts. The recorded testnet transactions per chain, with explorer links, on the Overview, Receipts and Margin tabs.
  • Health. A margined position's locked margin against its maintenance and initial requirements, read from MarginVault.health.
  • Liquidation timeline. The sequence of MarginVault events for a position: flagged, auction, taken over or absorbed, settled, finalized, and if necessary haircut.
  • EIP-712. The standard for signing structured typed data in a wallet; the RFQ desk uses it for quotes.
  • Copilot. The AI chat panel; on the server build it calls tools that read the tape, price strategies, and prepare ranges and quotes for the user to sign.
  • Skill. A markdown file of trading know-how that the copilot loads into its prompt when enabled.
  • MCP (Model Context Protocol). A standard through which the copilot connects to external tool servers, such as The Graph's Subgraph MCP.
  • Knowledge pack. The compiled table of contents and sections of the documentation that the copilot's read_docs tool serves.