Skip to main content
This page answers one operational question: how does a consumer actually reach each of the five behavioral signals? It maps every signal across all three access layers — the SDK, the REST API, and the subgraph — and tells you exactly which surfaces are live right now versus how any layer degrades to a typed pending state when a backing is absent. If you want the what and the why-non-circular of the signals, read /moat/five-signals and /moat/circular-vs-behavioral. If you want the primitives that feed them, read /moat/primitives. This page is the plumbing.

The three-layer routing rule

Before the per-signal table, internalize the rule that decides which layer you reach for:

SDK

Needs to write, or needs the exact price right now. The SDK is the only layer that holds a signer and the only one that can answer “what dollar premium will I be charged on THIS position if I sign in the next block?” — a fresh on-chain read plus the position geometry.

Subgraph

Needs history or an aggregate. The subgraph is internal infrastructure — nobody queries it directly in production except the API.

API

Needs frictionless public access with no wallet and no RPC key. A thin, cached, read-only facade over the subgraph (+ a small set of cached live RPC reads routed through the SDK, so pricing has exactly one implementation).
One pricing implementation, three faces. The current load surface (“price-to-beat”) is computed in exactly one place — the SDK’s CvammPricing TS port. The API’s live RPC reads route through the SDK; the subgraph never reprices, it only records what already cleared. There is no second pricing model to drift out of parity. The Phi-sum is never reimplemented anywhere — it is read from the on-chain FairValueOracle.

The per-signal access map

This is the page’s core artefact. For each signal: the DataClient SDK method, the API route, the subgraph entity, and the live-now flag. The API-route strings are the SDK↔API contract named in DATA_API_ROUTES.
Read the “Live now?” column carefully — it is not binary per signal. Three of the five signals are split across two live halves with distinct sources. Signal 1’s current surface is live RPC while its history is served from the subgraph; Signals 2 and 4 each have an off-chain telemetry half (the dynamic / latent half that can never be on-chain) alongside a structural half served from the subgraph. The split is the point — it is what makes the dataset captured, not retroactive (see the I7 exclusion below).

The one live SDK exception — getCurrentLoadSurface

Every other DataClient history method is API-backed (served from the subgraph via the hosted API). getCurrentLoadSurface is the single exception: it hits live RPC directly to compute the current pool load surface — the price-to-beat, with no subgraph in the path. It is the live-RPC half of Signal 1.
1

One multicall per market, over public getters

Per market, a single multicall reads:
  • InflexionCore.markets(marketId) → market config;
  • FairValueOracle.fairPremium(token, a, b, T, maxIL) → (premium, fairRate, σ_ref) — the Phi-sum, read on-chain, never reimplemented;
  • ConvexityVault.inventory() → (total, locked, free, util, conc);
  • InflexionCore.loadParams() (read once, shared across markets).
2

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

The load stack is completed client-side with the parity-locked CvammPricing TS port, thenpoolPremium  =  ⌈fairPremium⋅(1+totalLoad)⌉capped at MaxIL\text{poolPremium} \;=\; \Bigl\lceil \text{fairPremium}\cdot\bigl(1 + \text{totalLoad}\bigr) \Bigr\rceil \quad\text{capped at MaxIL}This is exactly the on-chain _pricePathAFromFair path. The client-side re-evaluation is permitted only because the deployed CvammPricing library is delegatecall-only — a direct eth_call reverts — so the SDK keeps a byte-equal TS mirror asserted in math.parity.test.ts. The Phi-sum is never mirrored; only the deterministic pure load transform is.
3

Per-market graceful degradation, inline

An unknown/inactive market or a reverting oracle yields an inlined degraded SurfaceRow (oracle-degraded / market-unknown) — never a thrown call. A market that supplies no geometry uses a neutral reference geometry, because the load % is geometry-independent; only the dollar fairPremium/poolPremium scale with a/b/maxIL.
data-client.ts
Why getCurrentLoadSurface is the baseline, not a behavioral signal. What it serves is the Path-A mechanical pool load — the deterministic price-to-beat. That leg is circular by itself (inverting it recovers our own load stack). Its job in Signal 1 and Signal 2 is to be the transparent yardstick the behavioral MM load is measured against — the subtraction mmLoad − poolLoad is exactly what removes the circular component. The historical evolution of this same surface is getLoadSurfaceHistory (API-backed, served live from the subgraph).

The two telemetry halves that are live now

Signals 2 and 4 each have a half that is already capturing, served from off-chain engine telemetry rather than the chain or the subgraph. This is TelemetrySink, writing two append-only JSONL sinks from the first interaction. The writer is best-effort: a telemetry failure never breaks a quote ack, a preview, or a /quote request.

DEMAND_LOG → Signal 4 (latent half)

Written on every GET /quote and every POST /telemetry/preview ping. Schema DemandRecord is coarse-bucketed and PII-free — { ts, marketId, widthBucket, distanceBucket, durationBucket, previewedPremium?, filled:false (ALWAYS — this is the latent half), source }. No raw geometry, no tokenId, no addresses — the SDK computes the buckets client-side and POSTs only the labels. The SDK LpClient.previewPremium fires this fire-and-forget; it never blocks or fails the preview.

COMPETITION_LOG → Signal 2 (dynamic half)

Written for every inbound WS quote — winners AND losers/stale/withdrawn-equivalent. Schema CompetitionRecord: { ts, marketId, mm, loadBps, validUntil, accepted, reason? }. Without it, the engine keeps only the latest stored quote per (market, MM) — the competing field (who else quoted, how wide, who widened/withdrew) is lost.

Why two halves can never be on-chain

This is the structural reason the telemetry sinks exist — and why capturing them now is non-negotiable rather than a nice-to-have.
Limit. An LP who priced a position but did not buy, and an MM quote that lost or was withdrawn, leave no on-chain trace. So the latent half of Signal 4 and the dynamic half of Signal 2 cannot be served from the chain or the subgraph.Why. Invariant I7 guarantees that an unchosen quote touches no nonce and no capacity — only the executed rail mutates state. Putting unfilled interest on-chain would both cost gas and break I7. The consequence: this data is unreconstructable retroactively — once the interaction passes uncaptured, it is gone.Mitigation / path forward. The off-chain telemetry sinks capture both halves from the first interaction, append-only and best-effort, exactly because they cannot be recovered later. Engine GET /health reports sink status { telemetry: { demand: true, competition: true } }. The hosted API reads these JSONL logs and serves GET /data/demand-requests (latent) and GET /data/quote-competition (dynamic), each carrying the maturity disclaimer.

Graceful degradation everywhere

Degradation is not an edge case in the access layer — it is the default contract at every layer. Nothing throws; everything degrades to a typed, named state. This is what lets the docs, the app, and any integrator render the dataset’s current maturity instead of showing an error.
Every API endpoint returns a discriminated union and never throws:
  • Live RPC + telemetry routes return real data NOW.
  • Subgraph-backed routes return real data NOW, served from the deployed subgraph via the hosted API. If SUBGRAPH_URL were unset, they would degrade to a typed pending body with the exact GraphQL query embedded, so the UI can show “coming from <route>” and the route wires up with zero re-derivation.
Live. Every history/aggregate surface — Signal 1’s history, Signal 3 entirely, the structural halves of Signals 2 and 4, and Signal 5 — now serves real data, indexed by the deployed subgraph and served through the hosted API.How. The on-chain moat events (SwapPriced, QuoteFilled) and CvammPricing.loadComponents shipped in the 2026-06-05 redeploy and emit every field the indexer needs. The redeploy block 274081134 is the subgraph startBlock — the dataset’s genesis — and the indexer has caught up from there, including the lifecycle swaps. The live RPC (current load surface, σ_ref) and telemetry (latent demand, quote competition) surfaces serve real data alongside it. Both the per-MARKET dimension (the API /markets route returns per-market aggregates) and the GEOMETRY-keyed aggregates (BucketAggregate, GeometryDemandBucket) are populated, so every moat signal is reachable today. See /moat/state-and-limits and /reference/status-caveats.
Structures ship from day one; the full DYNAMICS (pool-vs-MM spread as a forward-vol signal) mature with multiple competing MMs and volume. The dataset begins at the single redeploy; the off-chain telemetry halves begin at the first interaction logged.What Inflexion offers today is the architecture and the first public view into the microstructure of the DeFi LP volatility-risk premium — not yet a mature dataset:
  • Structures are present day-one. All five non-circular behavioral signals are defined and queryable from launch.
  • Dynamics mature with volume. The richer signal — the pool-vs-MM spread read as a forward-vol indicator — only sharpens once multiple market makers compete and real flow accumulates.
  • The dataset begins at the single redeploy, and the off-chain telemetry halves begin at the first interaction logged. History is served live from the subgraph via the API; if a backing (subgraph URL / RPC / telemetry) is absent, surfaces degrade to a typed pending state — never an error.

Live-now summary

A one-glance read of what serves real data today versus what degrades to a typed pending state:
The pattern, restated. Anything that needs the price right now or who quoted what right now is live (SDK live RPC + engine telemetry). Anything that needs history or an aggregate distribution is served live from the subgraph via the hosted API — and would degrade to a typed pending state, with the query embedded and never an error, only if a backing were absent. The dataset’s structures are all reachable today; its dynamics (regimes, forward-vol distributions, dispersion) mature with more MMs and flow.

Where to go next

The five signals

Each behavioral signal, defined precisely, with its non-circularity verdict, sourcing, and maturity caveat.

The on-chain primitives

SwapPriced, QuoteFilled, the load-breakdown library, and why two signals can never be on-chain (I7).

The DataClient

The SDK surface for the moat — method signatures, degradation envelopes, and the live load-surface read.

State and limits

The framed account of what is live, what is pending, and the path forward for each surface.