The oracle and the breaker
One interface, five resolver kinds behind it, and a breaker that freezes rather than moves.
There is one on-chain interface, push_price(value_usd_e6, source_count), and
five resolver kinds behind it. The resolver is metadata, not dispatch. Every
resolver funnels into the same instruction; the difference lives in the relayer,
and what it changes on chain is the risk badge and which defaults a pairable is
allowed to carry.
| Resolver | How | Covers | In the registry |
|---|---|---|---|
PushFeed | Pyth / Switchboard / Chainlink | crypto, equities, FX, metals, rates | 27 |
Http | Switchboard on-demand or Chainlink Functions over N providers, median with a deviation gate | retail SKU prices, the Coke case, plus Spotify, Steam, weather, box office, FRED and BLS | 53 |
OnChain | read an account or contract directly | NFT floors, TVL, gas, staking yield | 28 |
Optimistic | UMA-style bonded assertion with a dispute window | anything a human can verify but no API serves | 2 |
Pinned | a curated constant governance moves | jokes, and numbers that are conventions | 1 |
Registry counts are from pairables/seed.json on 2026-08-25 and sum to 111.
The optimistic resolver is not built. Two entries are registered against
it (TUITION-IVY and OPENAI-VAL) and both are priced today by an operator
attestation instead. Building it is what would make registration
permissionless rather than curated.
The defence stack
Lifted from floorlaunch unchanged, because it is already the right one.
pub fn ema_step(ema: u64, spot: u64, dt: i64, window_secs: u32) -> u64
Pull ema toward spot by min(dt, window) / window. Worth restating what
it does and does not do: a push arriving after a full window adopts the new
value entirely. Smoothing only throttles rapid-fire sequences. The
standing defences between pushes are the breaker and the staleness gate.
A push deviating from the TWAP by more than breaker_bps freezes the
pairable instead of moving it, and pushes are refused while frozen.
That second half is load-bearing. Otherwise a compromised oracle could walk the TWAP breaker-step by breaker-step during a freeze and hand governance a poisoned price at unfreeze.
live_price refuses a frozen pairable, a zero price, and a stale one.
Accrual, claims and fulfilment all go through it. Trading never does,
because trading does not read this program.
Size the breaker to the largest plausible move between pushes
This is the rule, and produce is what taught it.
Fresh fruit gaps hard at season turns. Domestic pears run out around June and
imports take over, and a 25% week-over-week move is ordinary rather than an
attack. The httpDaily preset at 2500bps would trip on the harvest and freeze
every market on it until governance intervened.
So there is a produce preset, and the numbers are enormous next to a
15-second Pyth feed:
| Preset | TWAP window | Min push interval | Breaker | Max price age |
|---|---|---|---|---|
pushFeed | 300s | 15s | 1500bps | 1h |
httpDaily | 1h | 300s | 2500bps | 2 days |
httpVolatile | 1800s | 120s | 4000bps | 1 day |
onChain | 600s | 60s | 3000bps | 2h |
optimistic | 1 day | 1h | 5000bps | 30 days |
produce | 1 week | 6h | 6000bps | 14 days |
slowIndex | 1 day | 1h | 3500bps | 30 days |
slowIndex exists for a different failure: a semi-annual print under an
httpDaily window would go stale two days after every publication, permanently.
Its TWAP window is a day rather than an hour because smoothing a series that
moves monthly at hourly resolution does nothing.
The distribution across the registry on 2026-08-25 is 40 entries on httpDaily,
28 on onChain, 27 on pushFeed, 7 on httpVolatile, 5 on slowIndex, 3 on
optimistic, and 1 on produce, summing to 111.
A healthy relayer must never fire the breaker
Tripping freezes the pairable and every market quoted against it, and only governance can undo that. So the relayer carries its own pre-check:
export function wouldTripBreaker(candidateUsdE6, twapUsdE6, breakerBps)
It mirrors the program exactly, with floored integer bps and tripping on strictly-greater. A legitimate move that large is a paging event, so the relayer holds and says so rather than pushing through.
The risk badge
Each pairable carries a badge the program writes and the UI reads. That badge is what will let registration go permissionless later without the registry turning to sludge.
provider_countu8How many independent sources the relayer is configured to median. A claim about setup, and only as true as whoever registered it.
last_source_countu8How many sources actually agreed on the most recent accepted push. The
configured count above is an assertion in a JSON file and drifts from the code
the moment anyone edits either. This one is written by the program from what
the relayer passed with the price, so the badge stops being a promise and
becomes a record. When it sits below provider_count, sources are down and
the UI can say so.
push_countu64How many pushes this pairable has ever accepted.
breaker_tripsu32How many times the breaker has fired.
last_dev_bpsu16Deviation of the last accepted push from the TWAP, in bps.
npm run sync-badges derives provider counts from the actual bindings and
reports the drift; --write corrects it.
The gates, and the order they run in
Everything in relayer/src/gates.ts is pure and offline, separated from the
fetching specifically so it can be tested without a network. The governing rule:
a gate failure means hold the last on-chain value. Never push a zero, never
push a guess.
Order matters, and it is:
A parse failure reading 0 would drag a median down before the MAD filter ever saw it, so bounds come first.
Observations older than maxSourceAgeSecs drop out, when that gate is set.
A crossCheckOnly observation never votes on the value. It is separated
before anything is averaged and only gets to veto afterwards.
Drop points further than k median absolute deviations. Only meaningful from three observations up. With two there is no majority for an outlier to differ from, and dropping one would turn a disagreement into a confident single-source push. Two-provider disagreements are caught by the spread gate instead, which holds.
On what survives. Then the cross-check veto, banded loose by default at 3000bps.
Values are carried as bigint
SpaceX was quoted as a whole company at $350B before SPCXx gave it a share to
hold, and $350B is 3.5e17 in USD e6, inside u64 and past
Number.MAX_SAFE_INTEGER. Math.round(v * 1e6) would quietly lose the low
digits on exactly the entries whose unit is largest, so toUsdE6 goes through a
fixed decimal string instead.
peard