Automation and Cranks
On this page
Permissionless calls advance series and other scheduled work when their checks pass. Automation depends on callers, gas, price availability and the configured limits; no operator is required to act at a promised time.
Design
fact Note Systems has no keeper daemon, cron job or privileged bot. Every state transition of a series is reachable in one of three ways, all of which are on-chain and permissionless:
| Layer | Who triggers it | What it does | Reward |
|---|---|---|---|
| Explicit cranks | Anyone | finalizeStrike, observe, settle, sweepFees, RollPolicy.roll, DeskPolicy.fill | keeperRewardQuote / RollPolicy.bounty / DeskPolicy.bounty |
| Lazy execution | Whoever is already interacting | claim, redeem, redeemShield, settle and refund first run any due strike or observation | keeperRewardQuote to msg.sender |
| Chainlink-compatible upkeep | Any automation network or bot | NoteAutomation.checkUpkeep / performUpkeep batch the above | rewards forwarded to msg.sender |
If nobody cranks, the next user who touches the series does; the price observed is the same either way, because OracleAdapter selects the closing print deterministically (see Oracle and MarketCalendar).
Permissionless Cranks
fact None of the following require a role.
| Function | When | Reward |
|---|---|---|
NoteCore.finalizeStrike(seriesId) | block.timestamp >= observations[0] and status Subscription | keeperRewardQuote (default 2 USDG) on success |
NoteCore.observe(seriesId) | block.timestamp >= observations[nextObs] and status Live | keeperRewardQuote on success |
NoteCore.settle(seriesId, account) | After strike, for any account with an unsettled deposit | none (but may trigger a lazy crank, see below) |
NoteCore.sweepFees(seriesId) | Any time sweepableFees(seriesId) > 0, i.e. accruedFees above the keeper reserve (NoFees otherwise) | none |
RollPolicy.roll(underlying) | Template enabled, feed fresh, cooldown elapsed, core accepting series, no series yet for today's trading-day close, fewer than maxOpenSeries later closes open | min(bounty, RollPolicy USDG balance) |
DeskPolicy.fill(seriesId) | Series in Subscription, inside the fill window (last fillLead before subscriptionEnd), inside the published rule for its underlying, S > D, cooldown elapsed, Desk has room and the sized fill is at least minFill | min(bounty, DeskPolicy USDG balance) |
Minter.mintWeekly() | Once per epoch week; each gauge is minted and notified in isolation, a failing gauge is skipped (GaugeSkipped) and its share is carried forward for a later permissionless Minter.retryGauge(gauge) | none |
Desk.harvest(seriesId) | After a coupon or settlement; also queued by NoteAutomation once the series has settled | none directly; one keeperRewardCap via NoteAutomation when the harvest releases the position |
RevenueRouter.sellNoteForQuote | Any time (the buyback auction) | the auction price itself |
The strike/observation reward is paid from the series’ accruedFees and capped by them: min(keeperRewardQuote, accruedFees). setKeeperReward is bounded by maxKeeperReward() (MAX_KEEPER_REWARD_UNITS = 100 quote units, 100 USDG). sweepFees leaves keeperReserve(seriesId) behind, one keeper reward per crank still due on the series (every close during Subscription, the closes from nextObs while Live, nothing once terminal), so an early sweep cannot remove the rewards of the remaining cranks; the reserve becomes sweepable when the series reaches a terminal status.
The sink is paid first and then notified; a failing notifyFee emits FeeNotifyFailed(seriesId, feeSink, amount) and does not block the sweep. A Deferred observation pays nothing until it succeeds. The reward is credited to keeperOwed[keeper] and withdrawn with NoteCore.claimKeeperReward(); NoteAutomation.performUpkeep claims it before forwarding, and a direct cranker claims it in a separate call. This keeps a keeper address that rejects transfers from blocking an observation.
Timing
Observation timestamps are official US closes, so every crank time is known in advance. observe reverts with TooEarly(closeTs) before the timestamp and with InvalidStatus if the series is not Live. The OracleAdapter rejects observe (GraceNotElapsed) until closeTs + closeGrace (5 min) has passed, then selects the last round published no later than that instant, whoever calls and whenever.
Cranking at OracleAdapter.resolvableAt(closeTs) or shortly after is ideal; calling later changes nothing except the reward race, unless more than maxRoundWalk rounds have been published since the close.
Reading the Queue
uint256 n = core.seriesCount();
for (uint256 id = 0; id < n; id++) {
(bool strikeDue, bool observationDue, ) = core.dueWork(id);
if (strikeDue) core.finalizeStrike(id);
else if (observationDue) core.observe(id);
}
dueWork(seriesId) -> (strikeDue, observeDue, settleable) is readiness-based: it returns true only when the corresponding crank would succeed right now (close reached, oracle grace elapsed, oracle not paused, observation not deferred-and-pending). It returns (false, false, settleable) while NoteCore is paused.
observationDue(seriesId) -> bool is the cheaper calendar-only companion for leg listings: it is true while the series is Live, the next observation is unprocessed and the current time is past OracleAdapter.resolvableAt(effectiveObservationTs(seriesId, nextObs)) (the effective close plus the close grace). It reads no feed and ignores deferral or pause state, so it tells a marketplace or a wallet that a coupon event is pending on the leg it is about to trade, not whether observe will succeed this second.
Lazy Execution
fact claim, redeem, redeemShield, settle and refund call an internal _crankIfDue first. It finalises a due strike or processes up to MAX_LAZY_OBSERVATIONS = 4 due observations, paying keeperRewardQuote per successful step to msg.sender exactly as an explicit crank would, then continues with the user’s action against the updated state.
- A lazy crank never makes the user’s action revert: if the strike/observation is not ready (grace not elapsed, oracle paused, observation deferred) it is simply skipped.
- If the user’s action itself reverts (for example
NothingToClaim), the whole transaction reverts, including the crank. Nothing is left half-done. - The result is byte-identical to the explicit path:
test/fuzz/core/LazyCrank.fuzz.t.soldrives a lazy and an explicit twin series through random price paths and asserts equal balances, buckets and statuses. - Users who are not interested in cranking pay the extra gas only when work is actually due;
dueWorklets front-ends warn about it.
RollPolicy
fact RollPolicy is the only address besides the owner allowed to call NoteCore.createSeries (NoteCore.seriesCreator, set by governance via setSeriesCreator). It holds one template per underlying and turns it into a concrete series on demand:
| Template field | Meaning |
|---|---|
feed | Chainlink proxy used as the series feed |
autocallBps, barrierBps, couponFloorBps, couponCapBps, refBps | Series economics, validated against NoteCore limits |
notionalCap, minTicket | Subscription limits |
subscriptionTradingDays | Subscription ends at the official close of the N-th trading day after today |
strikeLagTradingDays | The strike is the official close N trading days after the subscription close |
observationCount, observationSpacingTradingDays | Observations are official closes spaced N trading days apart (weekends, holidays and early closes come from MarketCalendar) |
maxOpenSeries | How many series of the stock may have a deposit close later than today's (1 to 10); a roll is refused with OpenCap beyond it. The series closing today does not count, so maxOpenSeries equal to subscriptionTradingDays gives one new series per trading day |
maxFeedAge | roll refuses a feed whose latest round is older than this |
enabled | Guardian can disable an underlying instantly |
roll(underlying) (nonReentrant, whenNotPaused) opens the series of the current trading day.
Gates in order: TemplateDisabled, FeedStale (the feed’s latest print, read through a guarded call that treats a revert or malformed return data as no data, is stale, incomplete or invalid, and the oracle adapter’s reference close is also older than maxFeedAge), RollCooldown (minRollInterval, default 1 hour, max 30 days), NotSeriesCreator (the policy is no longer NoteCore.seriesCreator), CorePaused, CoreDeprecated (the core is no longer the registry’s active deployment), TemplateExceedsCoreLimits (the stored couponCapBps or observationCount is above the core’s current maxCouponCapBps or maxObservations, which governance can lower after the template was set), CalendarGap (the calendar has no official close within 14 days at some step of the schedule walk), AlreadyRolled (a series closing at the derived subscription close already exists: a second roll on the same trading day, or a roll on a weekend or holiday, which derives the same close as the last trading day before it) and OpenCap (maxOpenSeries series of that stock already have a close later than the end of today, UTC; the series on its last session does not count, whatever the calendar says about today after it was created).
It computes the dates from the core’s calendar (subscription close subscriptionTradingDays trading days ahead, strike strikeLagTradingDays trading days after the close, observations every observationSpacingTradingDays trading days), calls createSeries, emits Rolled(underlying, seriesId, caller, bountyPaid) and pays min(bounty, balance) USDG to the caller.
Live and settled series never block a roll; the cadence is maxOpenSeries against subscriptionTradingDays (equal values give one new series per stock per trading day, maxOpenSeries 1 gives one per subscription period, the next opening on the closing day of the current one, which is the testnet setting since 14 Oct 2026), and either way depositors find an open series on any trading day. rollAll() runs the same gates for every stock with a template, opens each series that is due, skips the rest without reverting and pays one bounty per series opened; rollRange(start, count) does the same for a window of the template list and reverts InvalidRange when the window is empty or runs past it. previewRoll(underlying) returns (params, canRoll, reason) with reason one of Paused, TemplateDisabled, FeedStale, RollCooldown, NotSeriesCreator, CorePaused, CoreDeprecated, CoreLimits, CalendarGap, AlreadyRolled, OpenCap.
Every gate createSeries applies is mirrored in the preview, so canRoll == true means the roll succeeds on the core in the same block; params is zeroed whenever canRoll is false, and the view does not revert on a stale or broken feed or on a calendar gap, so bots can simulate before sending. openSeriesOf(underlying), openCount(underlying), capCount(underlying), seriesOf(underlying) and calendar() expose the calendar state.
A template’s whole schedule (subscriptionTradingDays + strikeLagTradingDays + (observationCount - 1) * observationSpacingTradingDays) is bounded to 90 trading days. setTemplate rejects the quote token and any underlying whose decimals() is not 18 (InvalidTemplate("underlying is quote"), InvalidTemplate("underlying decimals")), the same rule createSeries enforces.
Governance funds the bounty pot with fund(amount) and can sweep it back. registerSeries carries an existing Subscription series into the list (close within 90 days, later than the last entry, fewer than 10 open); unregisterLast removes the tail entry once its series is Cancelled or Unwound, which is how a wrongly registered far close is cleared.
fact Cost of a roll grows with the schedule length: each trading day of the schedule is one or more calendar reads. Measured on a regular calendar, a template with the longest allowed schedule (90 trading days) costs about 1.37M gas to preview and about 1.73M gas to roll; the deployment’s default template (5 + 1 + 15 trading days) is well under a tenth of that. rollAll runs the walk once per template, so a deployment with many long-schedule templates uses rollRange(start, count) windows rather than one rollAll; checkUpkeep performs the same walk per registered underlying and is sized with rollRange-style sharding in mind.
Rolled series have no SeriesGauge by default; governance adds one when desired.
DeskPolicy
fact DeskPolicy is the only address allowed to call Desk.fillCoupon (Desk.filler, a binding that changes only through the protocol governance with the published delay). It holds one rule per underlying and a USDG bounty balance, and turns unmatched SHIELD into a Desk fill on demand:
| Rule field | Meaning |
|---|---|
minBarrierBps, maxBarrierBps | The series barrier must lie inside this range (both strictly between 0 and 100%) |
minCouponFloorBps | The series coupon floor must be at least this |
maxObservations, maxTenor | At most this many observations and this many seconds from subscriptionEnd to the last one |
minCouponBps | The lowest coupon the Desk accepts on the underlying; the fill is sized so the discovered coupon stays at or above it |
enabled | The guardian can disable an underlying instantly |
Parameters: fillLead (window before subscriptionEnd in which fills are allowed, default 24 hours, 1 hour to 7 days), minFill (smallest fill, default 100 USDG, 100 to 10,000 USDG and never below the bounty), minFillInterval (per series cooldown, default 1 hour, 10 minutes to 24 hours) and bounty (per successful fill, at most 1,000 USDG and never above minFill, paid min(bounty, balance)). Anyone may fund the bounty balance.
Bounty grinding is bounded: over any W seconds with k series in Subscription at most k × (W / minFillInterval + 1) bounties can be earned, each backed by a fill of at least minFill, and every bounty comes from the policy balance, never from the Desk.
fill(seriesId) (nonReentrant, whenNotPaused) checks NotSubscription, RuleInactive, OutsideFillWindow, SeriesOutsideRule (barrier, coupon floor, observations or tenor), FillCooldown and DeskUnavailable (Desk or core paused, or the series feed unusable), then reads Desk.previewFill for the SHIELD gap S - D and the Desk’s room (the three cap headrooms and free idle USDG net of the withdrawal queue). The window also closes at the effective strike close when an early close brings it before subscriptionEnd (reason WindowClosed).
The target is the largest COUPON deposit that keeps refBps * S / D at or above minCouponBps (the whole gap when the series floor already meets the hurdle, nothing when the hurdle is above the series cap), further capped by notionalCap and by the notional the SHIELD prefund can pay for (NoteCore.leastFundedShield); the fill is min(target, room) and must reach max(minFill, minTicket) (NothingToFill otherwise, preview reason BelowMinFill).
The policy then calls Desk.fillCoupon(seriesId, amount, minCouponBps): the Desk posts a reserved deposit on NoteCore (depositCouponReserved), and at strike the core matches it only as far as the discovered coupon stays at or above the hurdle, refunding the excluded part to the Desk (ReserveExcluded); so however the SHIELD side moves between the fill and the strike, the matched part of a Desk fill never earns less than the hurdle.
The Desk independently enforces its caps, its idle and amount <= S - D (FillExceedsGap), so the vault can never be pushed past the SHIELD side whatever a caller computes. previewFill(seriesId) returns (amount, canFill, reason) without reverting, and canFill is true exactly when fill would succeed in the same block.
The Desk is therefore a price-sensitive counterparty rather than a backstop: SHIELD demand that would push the coupon below the hurdle stays unmatched, and a series with no COUPON deposits at all still strikes when the rule and the caps allow the fill.
Chainlink-compatible Upkeep
fact NoteAutomation implements the Chainlink AutomationCompatibleInterface (checkUpkeep(bytes) -> (bool, bytes) / performUpkeep(bytes)). checkUpkeep scans up to maxSeriesPerCheck (default 256) series via dueWork (the newest maxSeriesPerCheck ids, [seriesCount - maxSeriesPerCheck, seriesCount), when checkData is empty, or the range abi.encode(from, to) for sharded upkeeps, clamped to maxSeriesPerCheck ids from from; a range beyond the last series is clamped to an empty window rather than reverting), the Minter week and every registered underlying’s previewRoll, the registered Desk’s active series (a Harvest is due once a series with deployed principal has left Subscription and Live) and encodes a list of actions {Strike | Observe | MintWeekly | Roll | Harvest}. Strike and Observe actions are queued only while the core is not paused; MintWeekly only while the Minter is not paused, the week is not yet minted and the NOTE token is not paused; Roll only while both the core and the RollPolicy are not paused. Every module read in the scan is wrapped in try/catch, so one module whose view reverts drops out of the list without blanking the rest. performUpkeep executes at most maxActionsPerPerform (default 24) actions inside try/catch (one failing action never blocks the others, UpkeepAction(kind, id, success, reason) is emitted per action). An action counts as successful only when it changed state: a Strike or Observe must move the series status or nextObservationTs (a replayed or deferred call is reported with reason NoProgress), a Harvest must clear the Desk’s deployed principal for that series, MintWeekly and Roll must not revert. The caller is paid min(USDG balance, successes × keeperRewardCap) (RewardsForwarded), so a batch of no-ops pays nothing and splitting a batch into single actions pays the same as one batch; keeperRewardCap defaults to 25 USDG and is set by the owner with setKeeperRewardCap(cap) (KeeperRewardCapSet). USDG above the cap stays in the contract and the owner can move it with sweep(to) (Swept).
status Chainlink Automation and Gelato are not yet live on Robinhood Chain. Until they are, anyone can run checkUpkeep/performUpkeep from a wallet, a Safe module, a Defender/OpenZeppelin relayer or a simple bot; the interface is unchanged when a network becomes available. Nothing in the protocol depends on it: the lazy path guarantees liveness on its own.
Registration guidance. A single registration with empty checkData stays useful at any series count because it always covers the newest ids, and strikes and observations are only ever due on recent series (settled series produce no work). With daily issuance, the number of series that can need a strike or an observation at the same time is about templates x schedule trading days (Subscription and Live series both count).
Add sharded registrations with explicit abi.encode(from, to) windows once that product exceeds maxSeriesPerCheck (for example more than two templates on a 90 trading-day schedule), or to keep a dedicated upkeep on a long-lived older series.
Mainnet Chainlink Data Feeds (Robinhood Chain)
fact 57 feeds are published for Robinhood Chain mainnet (chain id 4663). All are 8-decimal proxies with a 24 h heartbeat and 0.5 % deviation threshold; the equity feeds follow us_equities_24/5 market hours. Source: contracts/deploy/chainlink/feeds-robinhood-mainnet.json (mirrors the Chainlink feed directory).
| Feed | Proxy | Heartbeat | Threshold | Decimals |
|---|---|---|---|---|
| AAPL / USD | 0x6B22A786bAa607d76728168703a39Ea9C99f2cD0 | 86400 s | 0.5% | 8 |
| AMD / USD | 0x943A29E7ae51A4798823ca9eEd2ed533B2A22C72 | 86400 s | 0.5% | 8 |
| AMZN / USD | 0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C | 86400 s | 0.5% | 8 |
| ASML / USD | 0xB4106147E8cce40b7d46124090d373A71b70f87D | 86400 s | 0.5% | 8 |
| BABA / USD | 0x62Cc8F9b5f56a33c9C8A60c8B92779f523c4E984 | 86400 s | 0.5% | 8 |
| BTC / USD | 0xa2c5184bF03d373Dc9dE4876eb4Bce595B460251 | 86400 s | 0.5% | 8 |
| BTC.B / USD | 0x5BB5e6a17a477d5B6Fec77b4322daD4A66bFb732 | 86400 s | 0.5% | 8 |
| CBBTC / USD | 0x0009cD492adf8167f9eEBf1293556A673530a21a | 86400 s | 0.5% | 8 |
| CLSK / USD | 0x810c12D3a554Bc47fd39597Fe3b3AAC4941F50eF | 86400 s | 0.5% | 8 |
| COIN / USD | 0xA3a468A452940B7D6b69991207B508c609a98Ef2 | 86400 s | 0.5% | 8 |
| CRCL / USD | 0x6652eDf64bA3731C4F2D3ce821A0Fb1f1f6b482a | 86400 s | 0.5% | 8 |
| CRWV / USD | 0xe1b3aABCAFAd1c94708dc1367dcfF8Aa4407487C | 86400 s | 0.5% | 8 |
| DELL-USD | 0x1C6c8cADBe02E19129c39dDB92281cE4c0bf206b | 86400 s | 0.5% | 8 |
| ENA / USD | 0x2A291496b3aa19d8948e442Ef28Ee952f3Ee97E8 | 86400 s | 0.5% | 8 |
| ETH / USD | 0x78F3556b67E17Df817D51Ef5a990cDaF09E8d3A9 | 86400 s | 0.5% | 8 |
| EURC / USD | 0xfF2B10c1973eD10c841434f98e456d8f3a0D7DD8 | 86400 s | 0.5% | 8 |
| EWY / USD | 0xEFdf54610B62A7753Ec30bDc380847c12D32e1D1 | 86400 s | 0.5% | 8 |
| GME / USD | 0x27C71df6A64fB476468EdF256CF72c038baB5B67 | 86400 s | 0.5% | 8 |
| GOOGL / USD | 0xF6f373a037c30F0e5010d854385cA89185AE638b | 86400 s | 0.5% | 8 |
| INTC / USD | 0x3f390C5C24628Ac7C489515402235FeAD71D1913 | 86400 s | 0.5% | 8 |
| IONQ / USD | 0x22EfeC4919baf55F360E0EDee4AbEB26DE4971eb | 86400 s | 0.5% | 8 |
| LBTC / USD | 0xa621344AdAEE699491597Fd8890E0C59a5BFBE59 | 86400 s | 0.5% | 8 |
| LINK / USD | 0xe86e3422Aa9B5e8ee9f3E41a63975bC387A8bce9 | 86400 s | 0.5% | 8 |
| META / USD | 0x7C38C00C30BEe9378381E7B6135d7283356D71b1 | 86400 s | 0.5% | 8 |
| MSFT / USD | 0x45C3C877C15E6BA2EBB19eA114Ea508d14C1Af2E | 86400 s | 0.5% | 8 |
| MSTR / USD | 0x396118bdFB181e6240E74D243F266B061c0edc3D | 86400 s | 0.5% | 8 |
| MU / USD | 0x425EEFdCf05ed6526C3cE61Af99429A228a6d596 | 86400 s | 0.5% | 8 |
| NBIS / USD | 0xE1D87B116Ba0fe898998f1D140339D1fA1E09705 | 86400 s | 0.5% | 8 |
| NVDA / USD | 0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15 | 86400 s | 0.5% | 8 |
| ORCL / USD | 0x0e6a64a2B58A6693a531E6c555f3A5d042eEA844 | 86400 s | 0.5% | 8 |
| PLTR / USD | 0x820ABedFF239034956B7A9d2F0a331f9F075eB4c | 86400 s | 0.5% | 8 |
| QQQ / USD | 0x80901d846d5D7B030F26B480776EE3b29374C2ae | 86400 s | 0.5% | 8 |
| RGTI / USD | 0x2A045cF1C49c61c166C036d2f06FA2D2d984f765 | 86400 s | 0.5% | 8 |
| RKLB / USD | 0x045477BF65Aef6f4F2386ad0164579e48381CC74 | 86400 s | 0.5% | 8 |
| SGOV-USD | 0xa0DF4ee0fFf975306345875E3548Fcc519577A11 | 86400 s | 0.5% | 8 |
| SLV / USD | 0x209b73908e92Ae021826eD79609845451Ecba2ce | 86400 s | 0.5% | 8 |
| SNDK / USD | 0xfb133Fa4B7b385802B693a293606682Df47109A3 | 86400 s | 0.5% | 8 |
| SPCX / USD | 0xB265810950ba6c5C0Ff821c9963014a56fD8Bffb | 86400 s | 0.5% | 8 |
| SPY / USD | 0x319724394D3A0e3669269846abE664Cd621f9f6A | 86400 s | 0.5% | 8 |
| SYRUPUSDC / USD | 0x8765c3B9Cda41d1029E780D0c1C37C8200DC4675 | 86400 s | 0.5% | 8 |
| SYRUPUSDC / USDC Exchange Rate | 0x6317f016FA3e312C4625dee51d32b43a223011f8 | 86400 s | 0.05% | 18 |
| SYRUPUSDT / USDT Exchange Rate | 0xBB688c0184Ce03fEdac89D71ccE752Ab21bC2999 | 86400 s | 0.05% | 18 |
| TSLA / USD | 0x4A1166a659A55625345e9515b32adECea5547C38 | 86400 s | 0.5% | 8 |
| TSM / USD | 0x874cF94aa8eC88Fd9560094dD065f2fB3E41Fc2F | 86400 s | 0.5% | 8 |
| USAR-USD | 0xA994d3684e8400A6c8078226925779FdeE682DD9 | 86400 s | 0.5% | 8 |
| USDC / USD | 0x9e6f4605992a899eE2999999F3Ec80C41F452546 | 86400 s | 0.5% | 8 |
| USDE / USD | 0xb9fB4e65744E4178894f7C61CF80E8a48A5f224a | 86400 s | 0.5% | 8 |
| USDG / USD | 0x61B7e5650328764B076A108EFF5fa7282a1B9aD2 | 86400 s | 0.5% | 8 |
| USDS / USD | 0x2D88D75b625633dCcd65d9d53BfDD3Aea2d8e84f | 86400 s | 0.5% | 8 |
| USDT / USD | 0xbf3550B6fAe1671da7C238Af12e03Ac586BEf3B1 | 86400 s | 0.5% | 8 |
| USO / USD | 0x75a9c76Ef439e2C7c2E5a34Ab105EcFe3766431c | 86400 s | 0.5% | 8 |
| WBTC / USD | 0x62107b0d3adA75fc1697fD342d99eed947a3aA5E | 86400 s | 0.5% | 8 |
| WEETH / EETH Exchange Rate | 0xb63f44E40aA811Cc69Fc55da786a5F3834100B4A | 86400 s | 0.05% | 18 |
| WEETH / USD | 0xf882e1D50352aecB0Ac85378378918BCf40511e7 | 86400 s | 0.5% | 8 |
| WSTETH / STETH Exchange Rate | 0x8E3Eb706B170c8FD1DdcD402932D952887736f9A | 86400 s | 0.05% | 18 |
| WSTETH / USD | 0x3F5040B50FB37934573B210fE54B53a6F1A792E8 | 86400 s | 0.5% | 8 |
| syrupUSDG / USDG Exchange Rate | 0xDd194C66aDcb422F188a04434e4824D70c151cF0 | 86400 s | 0.05% | 18 |
Note: there is currently no HOOD / USD feed in the directory. Pyth Pro does publish HOOD (feed id 1182), so a HOOD series on mainnet would need governance to accept a PythProFeed as the primary; until then it is not templated. Every one of the 35 stock feeds above has a matching Pyth Pro feed id and a live stock token contract; the joined and on-chain-verified registry is contracts/deploy/oracle/feeds.mainnet.json, and the Pyth side is described on the Oracle page.
Testnet Caveat
testnet only Robinhood Chain testnet (chain id 46630) has no Chainlink stock feeds. The testnet deployment uses eight MockChainlinkFeed contracts whose prices are pushed by the feed admin (deployer, KEEPER_ROLE on the mock). This is a test fixture, not part of the protocol: on mainnet the series feed is the Chainlink proxy from the table above and nobody can write to it. Testnet series also run in SHORT mode (subscription 3 trading days, 4 observations spaced 5 trading days) so a full cycle completes in about four weeks.
Failure Handling
- Competition. Two callers submitting for the same observation: the first succeeds, the second reverts (the index has advanced). Submit with a modest gas price and accept the occasional revert;
NoteAutomationswallows it insidetry/catch. - Paused core.
observe,finalizeStrikeand lazy cranks are inactive while paused (dueWorkreturns no due work). Observations are not lost; they run when unpaused, and the adapter still selects the same closing print as long as fewer thanmaxRoundWalkrounds were published since. - Deferred observations. Neither the lazy path nor
NoteAutomationretries a Deferred observation blindly;dueWorkreports it due again only when the oracle would accept it, or once governance has cancelled it (in which case the nextobserverecordsObservationSkippedand pays the reward). - Paused RollPolicy / disabled template. No new series is created for that underlying; existing series are unaffected.
- Paused DeskPolicy / disabled rule / paused Desk. No Desk fill for that underlying; the series strikes on organic deposits or cancels at
subscriptionEndexactly as it would without a Desk.NoteAutomationreports the skippedFillwith the policy’s reason and continues with the rest of the batch. - Roll gated by the core. When governance lowers
maxCouponCapBpsormaxObservationsbelow a stored template, or movesseriesCreatoraway from the policy,previewRollreportsCoreLimitsorNotSeriesCreatorandrollreverts with the matching error instead of a core revert; the template is updated withsetTemplateor the role restored, and rolls resume. ACalendarGapreason means the calendar needs the coming trading days registered before a roll can build a schedule. - Collapsed observation. When two observations roll onto the same close, the
observecall for the second index (explicit or lazy) records it as collapsed (ObservationCollapsed) and advancesnextObs, with no oracle read and no keeper reward. The call is cheap and never deferred;NoteAutomationreports it asNoProgressonly if another caller consumed the index first.
Keep exploring, or revisit this topic from the navigation.