null for the API to fill, and they are documented per-field
below. The subgraph owns only the realized, on-chain halves of the
five signals; the dynamic halves (live quote competition,
latent unfilled demand) are engine telemetry the API serves
separately.What it is, and what it is not
The schema is explicitly mapped to the five data-moat signals, and every signal is reconstructable two ways: as a current snapshot (the latest aggregate row) and as a historical series (per-fillSwap rows plus time-bucketed
snapshots).
It IS
It is NOT
The non-circular schema rules
The honest-framing rules are written directly into the schema header. They are load-bearing for the moat’s credibility — read circular vs behavioral for the full proof, but the constraints the indexer enforces are:1 — No impliedVol field anywhere (the circular idea, dropped)
1 — No impliedVol field anywhere (the circular idea, dropped)
fairRate to back out an implied vol is circular — the fair rate
was computed from σ_ref in the first place, so the inversion
returns only the protocol’s own published inputs. There is deliberately no
impliedVol field in the schema. This is the idea the moat
explicitly drops; see the data moat for why.2 — MM loadBps is the single NON-CIRCULAR load datum
2 — MM loadBps is the single NON-CIRCULAR load datum
loadBps is an actor’s chosen price of risk — not a mechanical
function of inputs. The Path-A pool totalLoadWad is the
mechanical baseline / price-to-beat, kept as the yardstick the behavioral MM
load is measured against, never sold as the signal itself.3 — Cap-bound fills carry ZERO load information, and are excluded
3 — Cap-bound fills carry ZERO load information, and are excluded
cappedAtMaxIL == true priced against the cap, not against the risk,
so it would poison a load mean. Cap-bound fills are
excluded from the load aggregates (Signals 1/2/3). They still count toward
demand (Signal 4) — demand is demand — which is why that lives in a separate
entity (below).4 — The dynamic halves of Signals 2 & 4 are NOT subgraph entities
4 — The dynamic halves of Signals 2 & 4 are NOT subgraph entities
5 — Signal 5 net-gamma Greeks are off-chain compute
5 — Signal 5 net-gamma Greeks are off-chain compute
NetGammaSnapshot are written back by the API/GreeksEngine
over the active-swap set the subgraph tracks (Swap.isActive) — finite-difference
of the deployed FairValueOracle, not the indexer.Data sources & manifest
specVersion: 0.0.5, apiVersion: 0.0.7, language: wasm/assemblyscript.
Network arbitrum-sepolia, chainId 421614. There
are three Ethereum data sources, all starting at the same block 274081134:
inflexionCore,
convexityVault, volOracle). For the concrete values, see
the deployments page.
subgraph.yaml is
auto-generated from the on-chain address registry — the same registry the
SDK’s addresses.ts consumes. One registry, three
consumers (contracts, SDK, subgraph), guaranteed in lockstep.Enrichment eth_calls
Two events carry too little data on their own, so the mapping does aneth_call
enrichment — the canonical Graph pattern: bind the contract, use try_* so a
revert degrades gracefully instead of failing the handler.
- SwapCreated → positions() + swaps()
- CollateralLocked → lockedByMarket()
SwapCreated the mapping calls
NonfungiblePositionManager.positions(tokenId) to decode the range geometry
(tickLower / tickUpper / liquidity / token0 / token1 / fee), and
InflexionCore.swaps(swapId) to recover
expiry/createdAt → durationSeconds. This is
why NonfungiblePositionManager is a second ABI on the InflexionCore source.uniswap.nonfungiblePositionManager into src/generated-addresses.ts. The
AssemblyScript mapping cannot read the JSON at runtime, so the codegen step bakes
it in.
The deploy story
subgraph.yaml is auto-generated — never hand-edited. It is templated from
the single on-chain address registry.
The start block — 274081134 — is the genesis of the dataset
The redeploy block is the moat genesis
startBlock = 274081134 on all three sources. This is
the redeploy block (_deployBlock in the on-chain address registry),
set via env: SUBGRAPH_START_BLOCK=<deployBlock> pnpm prepare:manifest.
The moat dataset BEGINS there.Why the redeploy is the genesis
QuoteFilled, SwapPriced) were coded but did not fire
before the FULL-FRESH redeploy of 2026-06-05. Pre-redeploy their handlers are
simply idle / never invoked; the entities exist and are ready. Indexing from the
redeploy block means no wasted scan over blocks that emit none of the moat data.The committed default is 0
0 (index from genesis — safe but slow). CI
only runs graph codegen, which validates schema + ABIs + mappings offline and
needs no real start block; the full build/deploy plugs in the redeploy block.Build / deploy scripts
test script, so pnpm -r test does not invoke
matchstick (which would need a toolchain download).
The deployed state
inflexion-arb-sepolia is
deployed on The Graph Studio, query URL
https://api.studio.thegraph.com/query/1754692/inflexion-arb-sepolia/version/latest.
It has indexed on-chain history from the redeploy block 274081134 (including the
lifecycle swaps), so the subgraph’s history and aggregate surfaces are served
live — through the hosted REST API, which is wired to
the subgraph. The current (non-historical) load surface is also available live via
the SDK multicall.Graceful degradation remains a design property: if SUBGRAPH_URL is unset, the
API serves these surfaces from a typed pending state — never an error (see
degradation).- Structures are present day-one. All five non-circular behavioral signals are defined and queryable from launch.
- Dynamics mature with volume. The richer signal — the pool-vs-MM spread read as a forward-vol indicator — only sharpens once multiple market makers compete and real flow accumulates.
- The dataset begins at the single redeploy, and the off-chain telemetry halves begin at the first interaction logged. History is served live from the subgraph via the API; if a backing (subgraph URL / RPC / telemetry) is absent, surfaces degrade to a typed
pendingstate — never an error.
The marketId join key
marketId is the key that ties subgraph ⇄ SDK ⇄ on-chain contract together,
so its byte layout is load-bearing. The full three-way parity treatment lives on
marketId parity; the subgraph-side facts are:
- On-chain it is
keccak256(abi.encodePacked(address token0, address token1, uint24 fee, uint32 durationSeconds))—abi.encodePackedis TIGHT packing, no 32-byte padding — so the preimage is exactly 20 + 20 + 3 + 4 = 47 bytes. - The subgraph replicates it in
deriveMarketId: concatenate the raw big-endian bytes at each field’s on-chain width (address → 20 bytes,uint24 fee→ 3 BE bytes,uint32 durationSeconds→ 4 BE bytes), thencrypto.keccak256. It uses a hand-rolleduintBytesBE, notethereum.encode, becauseethereum.encodeABI-pads every operand to 32 bytes and would produce a different, non-matching hash.
durationSeconds is a uint32 on-chain but arrives in
AssemblyScript as an i32; a value ≥ 2³¹ arrives negative after
BigInt.toI32() and sign-extends to i64. Because uintBytesBE only emits the
low width bytes and masks each with & 0xff, the sign bits (which live above
width) are never written — the uint32 preimage stays bit-for-bit correct across
the full on-chain domain.0x67c4bee1ee037851fbe2a8ecfdd0b8ae3d358283e940750c268621f776479d69 (dWETH,
dUSDC, fee 500, 604800s = 7d) by two parity tests. That marketId
matches the marketId_fee500_7d entry in the on-chain address registry.
Recovering the marketId from a SwapCreated: SwapRecord carries no marketId.
The mapping recovers it by (1) reading
token0/token1/fee from the NPM positions() decode, (2) reading
expiry/createdAt from InflexionCore.swaps() to compute durationSeconds,
then (3) calling deriveMarketId — reusing the NPM bind, not re-binding.
Both the geometry decode and the swap-record read must succeed.
The entity catalogue
All entities are@entity(immutable: false) (mutable, get-or-create + update)
except SigmaPoint which is @entity(immutable: true) (append-only).
Market — the market directory
Market — the market directory
MarketRegistered plus cvAMM
config events. id = marketId hex.- Directory facts:
token0,token1,fee(Int),durationSeconds,durationBucket(hour|day|week|month|longer),oracleToken,active,cvammEnabled(nullable Boolean — the Path-A rail flag, null until first set). - Denormalised lifetime counters (so directory reads are cheap, no scan over
Swap):totalSwaps,totalSettled,totalV0,totalPremium,totalPayout,pathBFills, plusswaps: [Swap!]! @derivedFrom(field: "market").
handleMarketRegistered; the counters are bumped
in handleSwapCreated. A Market created by a swap
before its MarketRegistered lands is back-filled by that handler, which
preserves the counters on the existing entity.Swap — the atomic per-fill lifecycle record
Swap — the atomic per-fill lifecycle record
SwapStatus {ACTIVE, SETTLED}, SwapPath {UNKNOWN, POOL_A, MM_B}. id =
swapId decimal string.isActive is the open-set membership flag for Signal 5 (true between Created and
Settled).BucketAggregate — Signals 1 (clearing load) & 3 (term structure)
BucketAggregate — Signals 1 (clearing load) & 3 (term structure)
id = `${widthBucket}-${distanceBucket}-${durationBucket}`.- Counts:
countPoolFills(Path-A baseline),countMMFills(Path-B behavioral),countMMWins(path == MM_B),totalNonCappedFills. The win-ratecountMMWins / (countPoolFills + countMMFills)is computed by the API; the counts are canonical here. - Running sums over non-capped fills → API derives means/medians (the store
has no median operator):
sumMMLoadBps,sumPoolLoadWad,sumSpreadWad,sumSigmaRefWad(Signal 1 normalises load over σ_ref),sigmaRefFloor(regime floor),v0Volume, pluslastFillTimestamp,lastFillSwap.
GeometryDemandBucket — realized half of Signal 4 (demand skew)
GeometryDemandBucket — realized half of Signal 4 (demand skew)
SwapCreated + geometry decode. The latent half (unfilled interest) is
off-chain telemetry served by the API. Fields: id (same
bucketId), the three buckets, realizedFillCount, realizedV0, firstSeen,
lastSeen.Why separate from BucketAggregate: demand skew counts every create
(including capped fills — demand is demand), whereas BucketAggregate excludes
capped fills (load info only). Different inclusion rules → different entity.NetGammaSnapshot — Signal 5 (net convexity / gamma supply)
NetGammaSnapshot — Signal 5 (net convexity / gamma supply)
id = `${bucketStart}`; fields bucketStart, activeSwapCount,
totalV0, totalMaxIL, aggGammaWad/aggVegaWad (null until computed),
volumeWeightedLoadWad = Σ(per-swap load × V0) / Σ V0 (API-populated).ProtocolState — singleton live active-set counters
ProtocolState — singleton live active-set counters
id = 'global') holding the current snapshot of the active-swap set
(Signal 5 current snapshot): activeSwapCount, totalActiveV0,
totalActiveMaxIL, cumulativeSwaps, cumulativeSettled, nextNetGammaBucket.
Bumped on SwapCreated (add to active set, increment cumulative) and decremented
on SwapSettled (remove, with floor-at-zero guards). Created with all fields
zeroed by getProtocolState.Depositor — per (address, tranche) vault flow
Depositor — per (address, tranche) vault flow
id = `${address}-${tranche}` (tranche 0 =
senior, 1 = junior). Fields: address, tranche, shares, totalDeposited,
totalWithdrawn (running, for realized-yield reconstruction),
pendingWithdrawShares, pendingUnlockAt, firstSeen, lastUpdated. Written by
handleDeposited / handleWithdrawRequested / handleWithdrawn. See
the tranches for the
junior-first-loss / senior-systemic-tail model.getDepositor is keyed by the composite ${address}-${tranche}
id, but its internal Depositor.load(addrHex) is passed the id string while the
local var is named addrHex. The id is
correct (it is the composite), so per-tranche separation holds — the variable name
is just misleading, no correctness issue.MarketMaker — Signal 2 per-MM win-rate + PnL
MarketMaker — Signal 2 per-MM win-rate + PnL
id = MM address. Fields: fills, exposureV0,
exposureMaxIL, cumulativePremium, cumulativePayout, pnl (= premium −
payout), cumulativeWinCount (Path-B wins: a SwapPriced with path == MM_B
and this mm), cumulativeQuoteFillCount (total quotes via QuoteFilled),
firstSeen, lastSeen.NOTE on “MM” for Path A: handleSwapCreated bumps the counterparty’s
MarketMaker row for every swap, and on Path A the counterparty is the
ConvexityVault. So the ConvexityVault address
appears as a MarketMaker with fills/exposure/premium — the API should
distinguish it. cumulativeWinCount and cumulativeQuoteFillCount only increment
for genuine Path-B MMs.SigmaPoint — the σ_ref series, immutable
SigmaPoint — the σ_ref series, immutable
SwapPriced backfill — the only
@entity(immutable: true). Enum SigmaSource {POKE, INITIALIZED, SWAP_PRICED_BACKFILL}. id = `${blockNumber}-${logIndex}` (the eventUid)
— globally unique across all three data sources, so the
InflexionCore backfill points never collide with the VolOracle poke points. Fields:
token, timestamp, blockNumber, priceWad, dtSeconds, sigmaShortWad,
sigmaLongWad, sigmaRefWad (carried forward), source.Why the gap-fill is load-bearing: poke() is a no-op (emits no Poked)
when dt < minSampleInterval, so on quiet feeds the series has holes.
The InflexionCore mapping fills them: every SwapPriced
with a positive sigmaRefWad writes a SigmaPoint with source = SWAP_PRICED_BACKFILL, so consumers see a
continuous series. σ_ref reconstruction: from a Poked, sigmaRefWad = max(sigmaShortWad, sigmaLongWad) (“max window binds” EWMA);
at Initialized both windows seed at the floor, so sigmaRefWad = floorWad.
See σ_ref.Pool snapshots — PoolDaySnapshot / PoolHourSnapshot / PoolState
Pool snapshots — PoolDaySnapshot / PoolHourSnapshot / PoolState
PoolDaySnapshot id = day index
(timestamp / 86400); PoolHourSnapshot id = hour index (timestamp / 3600).
Both carry seniorAssets, juniorAssets, totalLocked, utilWad (=
totalLocked / (seniorAssets + juniorAssets)), concWad, premiumAccrued,
payouts, juniorLoss, seniorLoss. PoolState is the singleton (id = 'global') running NAV the snapshots checkpoint.NAV reconstruction: deposits/withdrawals
move principal per tranche; PremiumAccrued credits each tranche
(toSenior/toJunior); JuniorLoss debits junior first, then senior — the
on-chain encoding of the junior-first-loss /
senior-systemic-tail model. Every flow calls checkpoint(ts) to write running
state into the current day + hour snapshots and addFlow(...) to accumulate
per-bucket deltas.MarketStateSnapshot — per (market × day) load surface
MarketStateSnapshot — per (market × day) load surface
`${marketId}-${dayStart}`.
Fields: market (ref), dayStart, lockedByMarket, nullable utilWad/concWad,
sigmaRefWad, baseLoadWad, utilSkewWad, dispSkewWad, totalLoadWad,
fillCount, v0Volume, pathBFills.Two writers, by design: the vault writes
lockedByMarket/utilWad/concWad/fillCount on CollateralLocked (the only
ConvexityVault event carrying a marketId). The
load-component fields originate from SwapPriced (InflexionCore), not the vault
— they are left null and the API joins them by (market, day).Nonce — (mm, nonce) fill/cancel state
Nonce — (mm, nonce) fill/cancel state
id = `${mm}-${nonce}`. Fields: mm, nonce,
filled, cancelled, swap (the swap that consumed it), updatedAt.Why it exists / the disambiguation: on-chain isNonceUsed returns true on
both fill and cancel — the subgraph disambiguates: QuoteFilled sets filled = true, while NoncesCancelled sets cancelled = true only if the nonce was not already filled. This preserves
invariant I7 (capacity
authority: a cancelled bitmap-nonce bit cannot fill) in the indexed view.The event-order fix
This is the single most important mapping subtlety — the heart of Signal 2. Consequence + fix: the spread (Signal 2) and the non-circular MM-loadBucketAggregate accumulation are done in handleSwapPriced (the later of
the pair), because only there are all three needed values available
simultaneously:
- the pool
totalLoadWad(on theSwapPricedevent itself), - the authoritative
cappedAtMaxIL(on theSwapPricedevent), - the swap’s persisted
mmLoadBps(written by the earlierQuoteFilled).
handleQuoteFilled therefore only persists the per-swap MM fields
(quoteId/nonce/mmLoadBps) and the Nonce / MM-count — it must not read
poolLoadWad/cappedAtMaxIL, which handleSwapPriced has not written yet.
Doing the accumulation in handleQuoteFilled would
read a not-yet-written poolLoadWad/cappedAtMaxIL → spread always null, capped
fills miscounted.
The spread + MM-load math: on a Path-B fill, convert
mmLoadBps → WAD as
then ; persist it on the
Swap and add to sumMMLoadBps / sumSpreadWad.
s.mm == address(0)
— the defensive-create branch where a SwapPriced was indexed without its
SwapCreated/QuoteFilled (a re-org/replay artefact) — so a phantom 0 cannot
poison the aggregates. A real fill (even a legit 0-load one) always carries a
non-zero mm from SwapCreated, so genuine data is never dropped.
More broadly, handleSwapPriced and the other
lifecycle handlers tolerate out-of-order arrival by get-or-create with zeroed
defaults.Event → entity map
TreasurySet / CvammConfigured / CvammFrozen / LoadParamsSet are indexed
for completeness via no-op handlers — they touch no entity.
The bucketing classifiers
The aggregates key on a (width × distance × duration) triple chosen to mirror the off-chain engine telemetry buckets, so the realized (on-chain) and latent (off-chain) halves of Signals 2/4 share one coordinate system the API can join on.GraphQL query examples
https://api.studio.thegraph.com/query/1754692/inflexion-arb-sepolia/version/latest.
The slug inflexion-arb-sepolia is deployed and indexing. If SUBGRAPH_URL is
unset, the API serves these surfaces from a typed pending state — never an error.
Prefer the REST API in production; query the subgraph
directly only for exploration.A market’s swaps — the per-fill lifecycle
The load surface — Signal 1 (clearing load) + Signal 3 (term structure)
The clearing load over σ_ref, bucketed by geometry, lives inBucketAggregate.
The API derives means as sum / count and the term-structure slope by comparing
the …-week / …-month / …-longer rows of one (width, distance) pair.
Net gamma / active-swap set — Signal 5
Pool / depositor — claim B (capital not guaranteed)
σ_ref series — continuous, gap-filled
Per-MM leaderboard — Signal 2 per-MM
Where it sits
Network: Arbitrum Sepolia · chainId421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05
Core protocol
Stylus & libraries
Oracles
Uniswap
Chainlink feeds
Demo tokens / pool / LP
Lifecycle swaps
0xb8bbd684f213d5833886ade7b531a6949d85522249881a2b5d46a5cc76e439c2):
- Path A (cvAMM convexity) — swapId 1, LP tokenId 3220 · V0 $270,531.28 · MaxIL $1,669.24 · premium $9.70 (0.58% of MaxIL) · settled payout $148.64 from ConvexityVault.
- Path B (MM routing) — swapId 2, LP tokenId 3221 · MM beat cvAMM ($8.93 vs $13.80) · MaxIL $3,215.65 · MM collateral locked $3,215.65 · settled payout $245.66 from MM collateral.
Go deeper
marketId parity
The REST API
The engine
The data moat
The data client
Degradation
pending state surfaces if SUBGRAPH_URL is unset.