The keeper

An offchain worker that, once every 24 hours, triggers each treasury's conversion and then its airdrop. It decides nothing. Its loop passes every five minutes by default, and since 2026-10-06 it converts a vault's ETH once per airdrop window, at its first pass after the window closes: see "The conversion cadence" below. Its airdrop step is written since 2026-10-05: see below.

What it does

  • Once every 24 hours, after the window closes, at 13:00 UTC by default, before the US open all year, spots the vaults holding at least the conversion threshold, 0.1 ETH by default, at its first pass after the close; a vault under it then waits for the next window, even if trades take it over the threshold later that day. The cycle's purchases run during the session that follows, and its sends, by default, once that session is over. Cash a vault still holds from an earlier cycle, USDC or USDG, goes on, threshold or not: since the sixth audit loop, on 2026-10-06, a vault's USDC once after each of its ETH conversions and at most once per window otherwise, a mirror vault's USDG at every pass (see "The conversion cadence")
  • Proposes swap routes and computes minimum amounts from a fresh quote; since 2026-10-05 the vaults hold those minimums on what actually arrives. Since 2026-10-06 each vault on Ethereum is quoted on its own router, the one it was created with
  • Sizes each step, so the venue can fill it within the vault's bound (below)
  • Calls the conversion, the bridge batch, then the purchase on the remote chain; since 2026-10-05 it is the only one that can send the bridge batch (bridgeReady). On the canonical rail it asks for the batch's fee at twice the latest base fee (quoteBridgeAt), and the excess comes back to it. Since the ninth audit loop it falls back on an older hub's quote only when the hub itself answers that it has none, never when a node stays silent: a silence holds the batch back a pass on the canonical rail, whose fee follows the base fee, with an alert, and on the USDG rail with a warning only. Since the tenth audit loop, on 2026-10-06, a batch on the USDG rail carries at most the adapter's cap of markets, 17 by default, which it reads at every batch: the markets waiting longest go first, and the others keep their USDC in their vaults for its next pass, so none waits for good (see The Robinhood rail). Until then every ready market went in the one batch, whose fixed compose gas a large batch could exhaust
  • Pre-deploys each new market's mirror vault on Robinhood Chain before its first batch (predeploy), which since 2026-10-05 only it and StockFun's owner can do. The remote hub learns the keeper from a batch, so before the first one StockFun's owner pre-deploys the first market's vault. The keeper pre-deploys only when its Robinhood Chain key is the one the remote hub knows as keeper, or its admin's. A market whose vault the keeper cannot pre-deploy (before the first batch, after a change of keeper the remote hub has not learnt yet, or when the pre-deployment fails) is left out of the batch, with an alert, and the other markets cross. A market whose mirror vault it cannot read that day waits for the next cycle, as one sent blind could make the whole batch fail
  • Monitors deliveries: a transfer that has not arrived, a ticket to replay, a mirror vault not yet deployed. A transfer it could not measure on Robinhood Chain as it left is confirmed by the remote hub's own events only, never by a balance read later, and on the canonical rail, since 2026-10-06, every transfer is: the remote hub pays its records out of pooled cash, so a balance can grow with another batch's money. It reads those events a few chunks at a time, from where each transfer's search stopped; since the seventh audit loop, on 2026-10-06, only up to a few blocks below the chain's latest one, so an event in the newest blocks is read on a later cycle rather than missed behind a node a block or two late. Since the eighth audit loop it follows a bridge batch only once the block that holds it is that many blocks deep (KEEPER_LOG_LAG_BLOCKS), from its receipt read again then: a batch's identifiers on Robinhood Chain come from that block, and a reorganization of Ethereum's newest blocks that moved the batch left the keeper watching identifiers that never exist. Since the tenth audit loop its alert for a transfer not credited in time says, on the USDG rail, where the batch's message stands on Robinhood Chain's endpoint: not delivered there yet; composed, the credit following; or stored, its USDG on the remote hub and recorded nowhere until someone runs the last step again by hand with more gas, the alert giving the stored message's hash. Until then it gave only what the remote hub records, whose zero read as "nothing landed" when a last step short of gas had left the cash on the hub
  • Relays at once, to its alert webhook, every emergency transfer it sees, on every vault, the bridge hub, the airdrop contract, the remote hub and every mirror vault. Since the tenth audit loop it names the contracts it watches in groups, one log request per group, each naming at most KEEPER_LOG_MAX_SELECTORS addresses and event values (1,000 by default), and passes a stretch of blocks only once every group of it is read. Until then one request named them all: once the market registry outgrew what an endpoint accepts (2,000 addresses and event values on Robinhood Chain's nodes, nine addresses on Sepolia's public endpoint), every request was refused and no emergency transfer was relayed again
  • Since the tenth audit loop, reads what each pass needs of every market (a vault's ETH, its USDC and its window, the debts, the last airdrop) through Multicall3, a hundred markets a call, at the same block each read used alone; a read that does not come back is made alone, as before, never taken for a zero. Until then every market ever created cost its reads one by one at every pass, idle or not, and 2,000 idle markets made a pass last as long as the interval between two
  • On the canonical rail, since the sixth audit loop, watches both tickets of every batch until it knows each one redeemed, whatever its transfer's credit, every cycle, in session or not, and replays one still live. A ticket not known redeemed six hours after its batch left, by default (KEEPER_TICKET_ALERT_HOURS), is alerted once, and so is one gone at or after its deadline with no redemption seen, or whose creation failed. Until then the keeper stopped watching a batch's tickets once its markets were credited, which could leave a deposit that missed its automatic execution to expire. Since the seventh audit loop it reads each ticket in the order the Arbitrum SDK does: the receipt of its creation first, then its automatic execution, then whether it still exists at a block no earlier than its creation; since the eighth audit loop, at a block a few under the latest one (KEEPER_REMOTE_LOG_LAG_BLOCKS), or its creation block when that is later, a block every node of an endpoint has. A deposit created between two of its reads, or seen by two nodes a block apart, is never taken for redeemed while it is still live
  • Alerts after repeated failures on a vault, counting its Ethereum step and its Robinhood Chain step separately, so a vault that fails every day on one side is reported. Since the seventh audit loop it waits at most ten seconds for its alert webhook and reads its answer: an alert the webhook does not take is kept, a hundred at most, in its state file, and sent again at the next cycles, so it arrives late rather than never. The message carries the fields Slack and Discord each read. Since the eighth audit loop one entry is kept per distinct alert: an alert raised again while it waits (a vault failing at every cycle) is counted, not kept twice. The message says when the alert was first and last raised and how many times, and an alert delivered late starts by saying so. Since the ninth audit loop the kept alerts go in the order they were last raised: an alert raised again moves behind the others, so what is said last of a subject is its latest state (a watch that fails, recovers and fails again while the webhook is down is delivered "failing" last, where it used to end on "recovered"). Past a hundred, only an alert raised at every cycle its cause lasts (a vault failing to convert, the bridge batch failing) is dropped first, so a one-off alert, an emergency transfer say, and the latest word of each episode are never pushed out. Since the tenth audit loop an error's text, in its log, its alerts and its state file, carries an RPC address's scheme and host only, never the rest of it, where providers put the key
  • Since the ninth audit loop, for a minute after one of its own transactions is mined, reads what that transaction changed at its block, never at the latest one, and estimates there the gas of what it sends next. The testnet run found why: a bridge batch sent right after the same pass's conversion was simulated on a node of a load-balanced endpoint one block behind the conversion, saw nothing to bridge, and raised a false alert. A node behind that block now answers an error, and the batch waits one pass with a warning. Every transaction of the keeper also goes with its gas estimate times 1.25, since Ethereum's Glamsterdam upgrade made the calls inside its transactions heavier
  • Since the ninth audit loop, a time read from a chain that no date can hold (a feed's start, a window's end, a ticket's deadline) reads "an unknown time" instead of failing the read that met it
  • Since the seventh audit loop, checks before its first cycle that each RPC serves the chain its configuration names, and refuses to start otherwise. Found later, a wrong chain stops that chain's work with an alert; a chain whose identifier cannot be read waits, and the other chain's work goes on. Since the eighth audit loop both chain identifiers must be set (below), and a state file written for another chain is left untouched until the keeper has read which chain its RPC serves (see "Restarts")
  • Since the eighth audit loop, reads the two price-feed guards of the mirror vaults' oracle on Robinhood Chain: while the oracle holds every price back for the sequencer, the remote purchases wait, with one alert, and a stock in a corporate action waits alone (see "When the oracle holds prices back")
  • Pays the cash waiting on the remote hub with a sweep, on both rails: records whose tokens have landed, and what reached the hub while it was paused. On the USDG rail it holds off while the remote hub awaits StockFun's owner's settlement of an emergency transfer, which it reports once, and resumes when the books are settled. Since 2026-10-05 it also sweeps a share a mirror vault refused, once that vault takes the cash again; on the canonical rail it sends that sweep only when it would pay something
  • Triggers the BuybackBurner, counting the buyback balance credited on the hook, which the burner pulls before it buys
  • Once per window, after the US session by default, sends each market's stocks to the airdrop contract, without ever setting the split: see below
  • Since 2026-10-05, every cycle, whatever the session, pays what the hook and the liquidity lock keep for a recipient that refused it: what the hook owes a market's vault (payTreasury), and the shares of a fee collection the lock keeps for a vault or a creator (payOwed). Each payment is simulated first and sent only when it pays something. Both calls are open to anyone
  • Since 2026-10-05, collects the LP fees of a pool created with one (collectFees, open to anyone), at most once a day per pool. StockFun pools charge no LP fee by default, so by default there is nothing to collect

The conversion cadence

The founder decided on 2026-09-27 that a vault's ETH converts once per daily cycle, just before the airdrop, and only if the vault holds the threshold at that check. The keeper holds to it since 2026-10-06; until then it converted a vault's ETH at every pass of the session once the vault held the threshold.

  • The window. The cycle is the airdrop's window: it closes when the airdrop contract says (24 hours closing at 13:00 UTC by default), the $STOCKFUN market included; while the factory names no airdrop contract, on that default schedule
  • One check per window. The loop still passes every five minutes by default, each pass ending with a cycle report. The threshold is checked at the first pass after the window closes that reaches the vault. At or above it, the ETH converts, once: the vault's own record of its last ETH conversion, read from the chain, tells the next passes, so a restart changes nothing. Below it, the ETH waits for the next window, even if trades take it over the threshold later that day; the keeper keeps that check in its state file. Since 2026-10-06 it keeps it as the end of the window the check was made for, never as the time on its own clock, which a clock a little off around the close could set apart from the chain's window; a check an older keeper wrote counts as none, so in the window of the upgrade such a vault is checked again. Since the sixth audit loop the check reads the vault's balance after the window's close: a pass that ran across the close used to record the balance it had read before it, and a vault that had crossed the threshold in time skipped that window
  • A conversion that cannot go at the check (a quiet ETH/USD feed, a venue that cannot fill within the vault's bound, a failed transaction, a window the keeper cannot read) is tried again at the next passes of the same window. A window it cannot read counts as a failure of the ETH step, alerted after repeated failures, and the vault's USDC still moves
  • The USDC, since the sixth audit loop. The USDC already in a vault goes on, threshold or not, at its own pace: its purchase on Ethereum, or its release into a bridge batch, goes once after each of the vault's ETH conversions, and at most once per window otherwise (a gift, a refund, what a failed or halved step left). Only a step that went through counts; one that fails is tried again at the next passes. The founder's decision of 2026-09-27 thus reaches the USDC: until then a few units of USDC sent to a vault before each pass made the keeper send a purchase, or a whole bridge batch with its fee, at every pass
  • What goes at every pass: the purchases on Robinhood Chain. A mirror vault's balance cannot tell a delivery from a gift, and the caps on each purchase spread a large delivery over several passes on purpose. The ETH a halved conversion leaves (the venue could not fill the whole balance within the bound) waits for the next window
  • Local and testnet runs set KEEPER_CONVERT_ONCE_PER_WINDOW to false, and then convert, buy and bridge at every pass; it stays on, its default, everywhere else

Elsewhere on this page, "every cycle" means at every pass of the loop, as in the cycle report; only the airdrop's daily cycle is the window.

One failure at a time

Since 2026-10-05 the contracts let one leg, one stock, one market or one delivery fail without stopping the others, and report what failed with an event. The keeper reads those events and alerts on what sticks; its own work is split the same way.

  • A purchase leg. Each stock's leg is planned on its own: a leg that cannot be planned is skipped, and the other legs go. A leg the vault reports failed (LegFailed) keeps its cash reserved for its stock; the keeper never counts it as converted, and alerts after three failures in a row of that stock in that vault (KEEPER_ALERT_AFTER_FAILURES). When no leg of a vault can be planned at all, the vault fails as a whole, as before
  • A market of a bridge batch. A market the bridge hub leaves out of a batch (MarketSkipped) keeps its USDC in its vault for a later batch. Left out of three batches in a row (KEEPER_BRIDGE_ALERT_AFTER_SKIPS), it is alerted once, with the decoded reason and its fix. Since the tenth audit loop a market the adapter's cap leaves out of a batch waits for the next pass, said in the log, and leads that batch; a batch the adapter refuses (above its cap, or an adapter upgraded without its batch settings) is alerted with the reason named and the batch's own markets
  • A refused delivery. A share the cash token refused to a mirror vault (DeliveryRefused) is alerted once, with its fix, until the remote hub owes that market nothing of it
  • An unreadable vault. A vault whose own views do not answer, after a broken upgrade for instance, is left out of that cycle; the other vaults convert. It is alerted once after three cycles in a row (KEEPER_ALERT_AFTER_FAILURES)
  • A debt that stays. While a vault, or the hook for a creator's share, still refuses the ETH it is owed, the keeper alerts once per episode that the pool awaits StockFun's owner (a fixing upgrade), and logs the end of the episode once the debt is paid, by the keeper or by anyone else

What only StockFun's owner can settle is "awaiting the admin": one alert per episode, never counted as a failure, and the rest goes on meanwhile.

The cycle report gains the debts paid and still owed, the legs that failed, the pools whose LP fees were collected, and, for the airdrop step, the cycles opened, the held-aside searches run, the vaults that sent and the deliveries still in transit. Since 2026-10-06 a send into a window without eligible holdings, which the airdrop contract holds aside, is counted apart (airdropHeldAside), no longer with the vaults that sent; on the canonical rail it counts the ticket replays sent (redeemed) and the tickets still watched (ticketsWatched). Since the seventh audit loop it also counts the markets whose airdrop send waits to be worth its cost (airdropBelowCost) and the alerts kept for the webhook (alertsUndelivered), and says when a chain's identifier is not checked yet (deferred: chain-unverified).

When the oracle holds prices back

Since 2026-10-06 the oracle of the mirror vaults can hold a price back (see The Robinhood rail), and since the eighth audit loop the keeper reads it before it quotes anything:

  • The sequencer. Once per cycle it asks the oracle what its sequencer check says. While the sequencer is down, back up for no longer than the grace period, or its uptime feed cannot be read, no stock can be priced: the vault's purchase waits, nothing is quoted and nothing counts as a failure. One alert opens the episode, kept in the state file so a restart does not raise it again, and one says when the purchases resume. The check is off on Robinhood Chain until Chainlink publishes an uptime feed for it, so today none of this can happen
  • A corporate action. A stock whose token pauses its oracle is left out of the purchase up front, said in the log, its USDG kept for it; it never counts toward the failure alert, and the basket's other stocks are bought. A leg the vault reports failed for that reason, between the keeper's plan and its purchase, is expected and not counted either
  • The reason, said. Where a leg's bound cannot be read because a guard holds its price back, the keeper logs the reason, on either chain, rather than a vault that cannot price the leg. A guard it cannot read (an oracle from before the guards) holds nothing back: each leg's own bound decides, as before. Since the ninth audit loop the oracle's other errors are decoded too: a stale feed reads StalePrice in the log and in a failed leg's reason, where it read an undecoded code

The airdrop step

The contract side is coded since 2026-10-04, and the keeper's step since 2026-10-05. Every cycle, before the session check, the keeper first follows the deliveries already sent. Then, while the factory names an airdrop contract, it takes each market in turn, the $STOCKFUN market included:

  1. Once per window. A vault is done for the window when its last send (lastAirdropAt) is at or after the end of the window a send would land in now (currentCycleEnd). Both are read from the chain, so a restart changes nothing. By default the keeper sends only once the US session of the day is over, so that the day's purchases go in one send, into the window that ended that day, and only when the vault holds stocks; since the seventh audit loop, only the stocks worth what sending them costs (below). Since the tenth audit loop, with that default, a vault read after the close with nothing to send is read again only when the next session opens, or the window moves, on the rails whose purchases follow the New York session (not Ondo's): what it holds comes from its purchases, made in sessions. Not while a purchase of its own still awaits its receipt, which may land during the evening: that vault is read at every pass, and what the purchase brings goes in the window that ended that day
  2. Held-aside stocks first, even with nothing to send. If the market holds stocks aside, assignUnassigned, at most five calls per cycle, until they are placed, which opens the cycle of the window that takes them, or no ended window is left to check. Since 2026-10-06 the keeper runs this search whether or not the vault has anything to send, and whatever the vault's pause: until then it ran only before a send, so a market that traded once and went quiet kept its first airdrop, held aside, out of its holders' reach until a new trade or someone ran the search by hand. A search that is not finished holds back, to the next cycle, a send into a window without eligible holdings. Since the sixth audit loop, while the vault has nothing to send, a search that would place nothing (no window can take the stocks yet) is sent only once the market's search lags the latest window by maxWindowsPerAssign windows, 30 by default: one search a month instead of one a day for good. A search that places the stocks always goes. Since the seventh audit loop "nothing to send" means nothing worth sending, by the same rule as the send: on the bridge rail, the dust the OFT cannot carry, which every send leaves in the mirror vault, counts for nothing, and the limit holds there too
  3. The cycle, only before a send. openCycle, so that each delivery only adds to it; never for a window nothing is sent to, and since the seventh audit loop only for a send worth its cost, the opening included. A window without eligible holdings is no error: the stocks are then held aside
  4. The send. On the local rail, sendToAirdrop on the market's TreasuryVault. On the bridge rail, sendToAirdrop on the mirror vault, paying its quoted LayerZero fee (quoteSendToAirdrop) plus a margin, 10 % by default; the vault refunds the excess. Since the ninth audit loop the send and every quote of it carry the gas its deliveries get on Ethereum, which the keeper chooses (below). Before that send the keeper checks that the remote hub sends to the factory's airdrop contract, that each stock has an adapter and that the adapter answers, and that the stock's OFT on Ethereum is registered on the airdrop contract. Since the seventh audit loop the send lists only the stocks worth their cost, its fee quoted again for them
  5. The deliveries. On the bridge rail, each stock's delivery is followed until LayerZero's endpoint on Ethereum has run its last step, which credits the airdrop contract. Since the ninth audit loop a delivery stuck on that endpoint is run again by the keeper itself (below). A delivery not credited within 60 minutes by default raises one alert, which carries the commands that run its steps by hand; anyone can run them

Each stock goes on its own: a stock whose balance cannot be read (an issuer freeze), one without an adapter or whose OFT is not registered, one whose adapter does not answer (peers(): no contract at its address, or not a LayerZero app; since 2026-10-06, until then it failed the market's whole send), since the eighth audit loop one whose LayerZero fee its adapter cannot quote (below), and one the vault reports it could not send (AirdropSendFailed) are left out, with one alert, and the others go. Each market goes on its own too: one that keeps failing is alerted once after three failures in a row, and the next market goes on. Since 2026-10-06 a held-aside search that keeps failing has an alert of its own, which gives its fix: anyone can call assignUnassigned for that market.

"Awaiting the admin", here, covers a paused airdrop contract, a stock the airdrop contract is short of after an emergency transfer (that stock waits in the vaults, the others go), and a remote hub that sends to another airdrop contract than the factory's, and, since the ninth audit loop, a remote hub whose delivery gas bounds are not set, or whose ceiling is under what a delivery needs.

What is worth sending

Since the seventh audit loop, on 2026-10-06, the keeper sends only what is worth what sending it costs. Until then any stock above zero went: a gift just above the dust to the vault of a market nobody trades made the keeper open that window's cycle and pay a LayerZero message, or an Ethereum send, every day.

  • What a send carries. A stock whose balance can be read and is above zero; on the bridge rail it must also have an adapter and a quote of its own above zero. The vault's quote is zero alike for the dust the OFT cannot carry and for a stock whose adapter cannot quote its send, so since the eighth audit loop the keeper asks the stock's adapter itself, on the send the vault would build: the dust is never sent and never alerted on; a stock whose quote fails, whose own send would fail, is left out with one alert that says why; and a stock the keeper cannot ask goes as before, its send deciding. Until then such a stock was taken for dust and dropped from every send without a word
  • Its value. Each stock is valued with its vault's own oracle, at its price feed's last answer, stale or not, since this is an estimate and never a bound; the costs, paid in ETH, are turned into dollars with the same oracle's ETH/USD feed
  • Its own cost. Each stock must be worth at least its own cost: its LayerZero fee on the bridge rail, where each stock is a message of its own, or its share of the send's gas on the local rail. A gift not worth its own message never rides along with a real send
  • The whole send. The stocks that pass must together be worth the whole send, plus the window's opening (openCycle) when its cycle is not open yet. On the local rail, since the eighth audit loop, the opening is counted once: the send's own estimate, taken while the cycle is closed, already holds it, so only the opening transaction's base cost is added; counted twice, it held back for days a send worth a little more than it cost. Otherwise none goes, and the cycle is not opened
  • What waits. What does not go stays in the vault and goes in a later window, with what accumulates. A market whose send waits is counted in the cycle report and logged once a window
  • What cannot be read. A price, a fee or a gas cost the keeper cannot read lets the send go, as before the rule: a read that fails never holds a real send back

KEEPER_AIRDROP_MIN_VALUE_BPS sets the multiple: 10,000, once the cost, by default, so a send worth at least what it costs goes, whatever its size above it; a higher value holds smaller sends back until they are worth that multiple, and 0 sends whatever is carried. The same rule decides whether a vault has something to send for the held-aside search above. The audit took it as its recommended default; it decides only when a stock goes, never how much, what or where.

The delivery's gas on Ethereum

On 2026-10-06 Sepolia activated Ethereum's Glamsterdam upgrade, which makes a new storage slot cost about five times its former gas, and every airdrop delivery of the LayerZero testnet run's first cycle ran out of gas at LayerZero's executor: a send paid for the gas of the delivery's last step only, and the gas of its first step was whatever the owner of the stock's LayerZero adapter enforces (the issuer, on mainnet). Since the ninth audit loop, on the founder's decision of the same day:

  • The keeper simulates each delivery on Ethereum before the send: the stock's token contract receiving it, and the airdrop contract crediting it, the way the delivery will find them. It asks the heaviest stock's need times 1.25, less the gas the stock's LayerZero adapter already enforces for the first step
  • When a simulation cannot run, it takes what the market's last deliveries used on Ethereum, then the remote hub's defaults
  • The remote hub bounds it. StockFun's owner sets a default, a floor and a ceiling for each of the two steps (see The Robinhood rail); whatever the keeper asks is held between them. An ask the ceiling cuts below a delivery's need is "awaiting the admin": the send still goes, and a delivery that gets stuck is run again (next section)
  • Nothing changes for the holders. The keeper decides only the gas a delivery is given, never how much is sent, where or to whom
  • Chosen again only when needed. A pair chosen from every stock's simulation is kept for the window while the stocks, the state of the window's cycle and whether a delivery may land past the next window's end stay the same; one a simulation could not give is chosen again at every pass. Since the tenth audit loop a send carrying only the dust the bridge cannot carry reads nothing of the delivery's environment, and a pair simulated once the window's cycle is open, a state that never goes back, is reused without reading it again

A delivery stuck on Ethereum

A delivery short of gas loses nothing: it stays on LayerZero's endpoint on Ethereum, its first step verified and not run, or its last step stored and not run, until anyone runs it again with more gas. Since the ninth audit loop the keeper does it itself:

  • It reads each delivery's step on the endpoint every cycle. Once the executor's turn is over (its failure, believed only from LayerZero's own executor, or ten minutes), it runs the stuck step again from its Ethereum key, with the simulated need times 1.25, and never more than KEEPER_AIRDROP_REEXECUTION_MAX_GAS, 4,000,000 by default
  • One it cannot send (its simulation fails, its need is above the bound, its key lacks ETH) is alerted once, with the command to run it by hand, and tried again every cycle
  • One that fails again on chain, the step still stuck, is the second failure: alerted, with the command, and never sent again by the keeper. It decides that only on a read of the step that answers, and only once the failed run's block is a few blocks deep (KEEPER_LOG_LAG_BLOCKS), so neither a node a block behind nor a reorganization of that block can make it give up
  • The keeper's Ethereum key pays these runs: under Glamsterdam a last step that opens a cycle needs about a million gas

The keeper chooses when, and the gas of a delivery within the owner's bounds, nothing else. A send that arrives after the next closing time is measured over the next day's window.

Seven settings drive the step: KEEPER_AIRDROP (on by default), KEEPER_AIRDROP_AFTER_SESSION (on by default; off on a testnet that ignores the session), KEEPER_AIRDROP_FEE_MARGIN_BPS (1,000), KEEPER_AIRDROP_TRANSIT_MINUTES (60), since the seventh audit loop KEEPER_AIRDROP_MIN_VALUE_BPS (10,000), and since the ninth KEEPER_AIRDROP_REEXECUTION_MAX_GAS (4,000,000) and KEEPER_AIRDROP_LZ_EXECUTOR (empty: LayerZero's executor on the keeper's chain, the only caller whose failure alerts the keeper believes).

Restarts: the state file

Since 2026-10-05 the keeper writes what it follows from one cycle to the next to a state file (KEEPER_STATE_DIR): the airdrop deliveries and the bridge transfers in transit, the transactions awaiting a receipt, its alert episodes and runs of failures; since the sixth audit loop also the canonical rail's tickets it watches, where each transfer's search stopped, and when each vault's USDC last went; since the seventh, the alerts its webhook has not taken yet. A file an older keeper wrote loads as it is. It writes the file after every cycle and every send, and reads it once at start. A restart forgets none of it: a delivery whose last step fails after a restart is still alerted, a transaction sent before it is never sent again, and an episode already alerted is not alerted twice. A file written for another factory is set aside, and the keeper starts empty, with a warning. Since the eighth audit loop a file written for another chain is first left as it is, neither read nor written over, until the keeper has read which chain its RPC serves: if it serves the configured chain, the file was another chain's and is set aside then; if not, the configured identifier is the mistake, the keeper refuses to start, and once it is corrected the keeper takes its file back, the deposits it was watching included. Until then a mistyped identifier cost the file before the keeper refused to start. The kept alerts are one per distinct alert since the same loop; a file an older keeper wrote loads as it is. Since the ninth audit loop the file also keeps each airdrop delivery's packet and its runs by the keeper, the gas the market's last deliveries used, and the keeper's last replay of each canonical ticket; a file an older keeper wrote still loads as it is.

Since 2026-10-06:

  • Each send is on disk before its wait. Every transaction goes at the nonce the chain gives the keeper's address right before it, and is written to the state file, with its hash and nonce, as soon as the hash comes back, before its receipt is awaited: a keeper killed while it waits reads it from its hash at its restart instead of sending it again
  • A lost transaction is let go. One that no node knows any more four times KEEPER_RECEIPT_ALERT_MS after it was sent (two hours by default) is dropped with one alert, so it no longer holds back sends of its kind; its nonce is still free, and the keeper's next transaction takes it. The alert for a transaction still not mined says how to replace it
  • Written whole. A disk too full to take the file is an error, alerted, and the last good file stays; until then a truncated copy could replace it
  • One keeper per folder. The keeper takes a lock in its state folder (keeper.lock): a second keeper on the same folder refuses to start and names the first. A lock whose keeper is gone is taken over; one left by a keeper on another host is not, and the operator deletes it once that keeper no longer runs. A dry run takes no lock

Sizing each step

Since 2026-10-01 the vault converts the amount the keeper names, not its whole balance. The keeper sizes it:

Step Amount
ETH → USDC The whole balance, halved while the quote falls short of the vault's bound, never below the conversion threshold; since 2026-10-06 the rest waits for the next window
A stock on Uniswap pools on Ethereum The stock's reservation, halved at most four times; a leg that still falls short waits for the next cycle, its reservation intact
A stock on the optional Ondo rail The stock's reservation, capped at the session's notional limit. Since 2026-10-06 a leg Ondo prices under the vault's bound is never sent: the free quote is read before any attestation, and once an attestation comes back under the bound, none is asked for that stock until the next window
A stock on Robinhood Chain The stock's reservation, capped on its own by a fixed ceiling and, when its pool can be measured, by a share of that pool's depth

What is not spent waits in the vault for the next cycle (for the ETH, the next window); a stock's cash stays reserved for that stock.

Since the security pipeline of 2026-10-01, every purchase holds back n−1 units on the basket's last stock, n being the number of stocks, on every rail that buys stocks. The vaults give the weights' rounding remainder to the last stock, so a few units of cash arriving between the keeper's read and its transaction can lower that one reservation by up to n−2 units; spending the amount read would make the whole call revert, and anyone could cause that with a dust transfer. What is held back stays reserved for the next call. Anything else that calls the vaults should keep the same margin. A fix in the contracts, which changes the documented allocation rule, awaits the owner's decision. Since 2026-10-06 a vault that holds no more than those few units of USDC (four at most, a basket holding five stocks at most) is no longer visited for them: they wait for the next USDC its ETH brings.

The $STOCKFUN burn is sized the same way: a slice whose price impact exceeds the keeper's ceiling is halved, never below the keeper's threshold, and the rest burns on later cycles. Since the security pipeline of 2026-10-01, a slice the $STOCKFUN pool cannot fill whole is halved too; before, the burn failed for that cycle. Any other failure is not retried.

What it cannot do

Pick an asset outside the basket. Move one stock's reserved cash to another stock. Loosen an oracle bound. Send an asset anywhere other than where the contract sends it. Withdraw anything. Choose who receives an airdrop or how much it carries, or allocate a share to itself.

Its trigger is reserved to prevent sandwiching, not because the keeper is trusted: every call it makes executes inside the vaults' bounds.

The cycle

flowchart TD S[Daily cycle, after 13:00 UTC] --> C{Market open?

NYSE calendar} C -->|no| W[Wait] C -->|yes| P{Contract paused?} P -->|yes| W P -->|no| A{First check of the vault

in this window: ≥ 0.1 ETH?} A -->|no, the ETH waits

for the next window| N[Next market] A -->|yes| E[ETH → USDC, 50 bps bound] E --> B[USDC → USDG → bridge] B --> R{Arrived on the remote chain?} R -->|no| M[Monitor, replay the ticket] R -->|yes| K[Buy the stocks, 200 bps bound] K --> AD[Send to the airdrop contract] AD --> N

The hour, the threshold and the bounds in the diagram are the default settings. The keeper passes every five minutes; a vault's ETH is checked once per window, at its first pass after the close, while the USDC it already holds goes on once after each conversion and at most once per window otherwise. The send to the airdrop contract runs once per window, after the session by default; the debts, the LP fees, the deliveries and, since the sixth audit loop, the canonical rail's tickets are handled every cycle, whatever the session.

Its configuration

Everything goes through environment variables: RPCs for both chains, the keeper's private key, contract addresses, interval, and the safety levers — dry run, run once, require market open. Since 2026-10-05 also: the airdrop step's settings, above; the LP fee collection, KEEPER_COLLECT_FEES (on by default), KEEPER_COLLECT_FEES_HOURS (24) and KEEPER_COLLECT_FEES_MIN_WEI (0); the alert after repeated skips, KEEPER_BRIDGE_ALERT_AFTER_SKIPS (3); and the state file's folder, KEEPER_STATE_DIR. Since 2026-10-06, KEEPER_CONVERT_ONCE_PER_WINDOW (on by default): a vault's ETH converts once per window, and, since the sixth audit loop, its USDC goes once after each conversion and at most once per window otherwise; a local or testnet run turns it off and converts, buys and bridges at every pass. Since the sixth audit loop also KEEPER_TICKET_ALERT_HOURS (6): on the canonical rail, the hours after which a ticket not known redeemed is alerted. KEEPER_STOCK_ROUTER, the router the factory names for new vaults, only picks which market session gates a cycle; each vault is quoted on its own router.

Since the seventh audit loop the chain identifiers, KEEPER_CHAIN_ID and KEEPER_REMOTE_CHAIN_ID, are checked against the RPCs before the first cycle: 1 with 4663 on mainnet, 11155111 with 46630 on the testnet. Since the eighth audit loop both must be set: KEEPER_CHAIN_ID always, KEEPER_REMOTE_CHAIN_ID with the bridge, and the keeper refuses to start without them (until then, left out, KEEPER_CHAIN_ID read 11155111 and KEEPER_REMOTE_CHAIN_ID 4663). Since the tenth audit loop KEEPER_LOG_MAX_SELECTORS (1,000 by default, at least 5) bounds the addresses and event values one log request names, as Robinhood Chain's nodes count them; behind Sepolia's public endpoint, which refuses ten addresses or more, it is set to 10. Two settings say how far below the latest block its log searches stop: KEEPER_LOG_LAG_BLOCKS (2, on Ethereum) and KEEPER_REMOTE_LOG_LAG_BLOCKS (12, on Robinhood Chain); since the eighth audit loop the first is also how deep a bridge batch's block must be before the keeper follows it, and the second how far below the latest block a ticket is read. The oracle's guards need no setting: the keeper reads them on each mirror vault's oracle. A whole-number setting left empty takes its default, and one out of its range stops the keeper at start.

The keeper's key is a hot key, funded for gas only, and distinct from the deployer's.

Its limits

  • On the USDG rail, a delivery the cash token refused before the keeper started, or while it was stopped, shows only as a warning when it sweeps; the canonical rail finds such a share in the remote hub's own state
  • Until the ninth audit loop the keeper did not run a failed delivery's last step (lzCompose) itself; since then it runs a stuck step again, bounded, and a second failure is left to a person, with the command
  • The emergency watch does not keep its position across a restart: events emitted while the keeper was stopped are not scanned
  • A keeper killed in the few milliseconds between a send and its write to the state file may send that transaction once more at its restart; the contracts' own checks make most such repeats fail, at the cost of their gas
  • A transfer's search for its arrival never reads a block twice: a reorganization that moves an arrival into a block already read leaves that transfer to the transit alert. Since the seventh audit loop every log search stops a few blocks below the latest one, which delays a credit, and its transit alert, by those few blocks
  • Since the eighth audit loop a bridge batch is followed once its block is a few blocks deep, a cycle later than before, and the next batch waits for it; a reorganization deeper than that is not covered, as for the log searches. A ticket redeemed in the last few blocks still reads live there until the latest block is that many blocks past its redeem: the next cycle on a busy chain, more cycles on a quiet one. Since the ninth audit loop one the keeper's own replay deleted is neither replayed again nor alerted meanwhile, after a restart too; one replayed by someone else in those blocks is tried again, fails in simulation, and can still be alerted as live past the alert's hours
  • The minute during which the keeper reads at the block of its own last transaction does not cover a node more than a minute behind its peers
  • On the local rail, the opening of the window is counted once, without what the second transaction repeats (some 5 % of the cost): a send can go worth up to that much less than it costs
  • Since the tenth audit loop, a stock given to a vault after the close, outside any purchase, goes with the next session's send, into a later window: the keeper reads a vault with nothing to send again only at the next session. A purchase whose receipt the keeper read in the session's last pass could also be read empty at the first pass after the close from a node lagging behind it, which needs a pass interval shorter than that node's lag (about five seconds on Sepolia), never the default five minutes

Preflight

Before any real run, a preflight command checks, without writing a single transaction, that the networks respond, that the addresses carry code, that the USDG OFT's LayerZero peers match, that USDG is not paused by its issuer, Paxos, and that basket weights are what they should be.

Since the eighth audit loop it also checks the oracle's guards. Its manifest must say how the sequencer check is set, either way: no uptime feed and no grace period (the check off, as today), or a feed with a grace period. It reads that feed when there is one, and each stock token's oracle pause: a token in a corporate action is a warning, which does not fail the check; a token without the signal fails. After deployment it checks that the deployed oracle matches the manifest, that its sequencer state lets prices through, and that every stock's pause check is on. The keeper's inspect command prints the same guards: the sequencer check and its state, and each stock's pause check and whether its token pauses now.

Since the ninth audit loop it also runs on the LayerZero testnet run's deployment (Sepolia and Robinhood Chain's testnet), from that run's two files, with RPCs named for that pair, so a mainnet RPC is never asked about the testnet; any other pair of chains, a mix included, is refused. On the testnet it reads no Uniswap v3 router, which that chain does not have, and holds the ETH/USD feed to the heartbeat the run's oracle was given. On both pairs it checks that the remote router names the v3 router the manifest says. Run on the testnet deployment: 68 checks, all passing.

It refuses to run without RPCs rather than report a false success.