peard
The registry side

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.

ResolverHowCoversIn the registry
PushFeedPyth / Switchboard / Chainlinkcrypto, equities, FX, metals, rates27
HttpSwitchboard on-demand or Chainlink Functions over N providers, median with a deviation gateretail SKU prices, the Coke case, plus Spotify, Steam, weather, box office, FRED and BLS53
OnChainread an account or contract directlyNFT floors, TVL, gas, staking yield28
OptimisticUMA-style bonded assertion with a dispute windowanything a human can verify but no API serves2
Pinneda curated constant governance movesjokes, and numbers that are conventions1

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.

1
EMA/TWAP step
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.

2
A breaker that freezes rather than moves

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.

3
A staleness gate on every value-moving path

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:

PresetTWAP windowMin push intervalBreakerMax price age
pushFeed300s15s1500bps1h
httpDaily1h300s2500bps2 days
httpVolatile1800s120s4000bps1 day
onChain600s60s3000bps2h
optimistic1 day1h5000bps30 days
produce1 week6h6000bps14 days
slowIndex1 day1h3500bps30 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_countu8

How many independent sources the relayer is configured to median. A claim about setup, and only as true as whoever registered it.

last_source_countu8

How 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_countu64

How many pushes this pairable has ever accepted.

breaker_tripsu32

How many times the breaker has fired.

last_dev_bpsu16

Deviation 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:

1
Sanity band

A parse failure reading 0 would drag a median down before the MAD filter ever saw it, so bounds come first.

2
Staleness

Observations older than maxSourceAgeSecs drop out, when that gate is set.

3
Cross-check split

A crossCheckOnly observation never votes on the value. It is separated before anything is averaged and only gets to veto afterwards.

4
MAD filter

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.

5
Spread gate

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.