This is an unaudited initial service implementation. Local tests and independent code review do not replace external cryptographic review or establish a public-chain delivery SLA.
Epoch sources are recipes in an owner-managed, append-only registry, documented in docs/epoch-protocol.md: six are built in, from Hyperliquid, dRPC, TickerLayer and Nodary, and each epoch uses a catalog of 1 to 10 registered recipes with their signers. The epoch ID, catalog hash and start-1 block hash select the query. The owner can register recipes and schedule a replacement catalog for epochs at least two ahead (CatalogScheduled); a catalog never applies to the current or next epoch, so prepared snapshots and open requests keep their recipes and signers, and the initial catalog stays bound into protocolConfigurationHash and keeper pins. Registering a recipe and changing a catalog are trust events: verifiers must take each epoch's recipes and signers from catalogAt or CatalogScheduled, and each recipe's definition from getRecipe or RecipeRegistered, rather than from the initial getters. The first authenticated local response is retained through retries; it is published only for live allowed demand. Idle snapshots remain local.
Only the owner registers recipes, and a registered recipe never changes. A recipe's data template decides which signed bytes the registry accepts for its query, so the owner vouches for it. A lax template, such as a single DECIMAL segment or a HEX segment without surrounding literals, accepts any record of that shape: a different field of the same response, an error body a gateway happens to sign, or a value from another listing, provided the recipe's signer signed it for that query. No template can admit an unsigned record, a record signed for another query hash, a signature from an Airnode other than the catalog slot's signer, an attestation outside the 240-second freshness bound or more than 128 bytes; those checks apply to every recipe. The gateway body is not checked onchain. Keepers refuse a recipe whose body does not canonicalize to its canonical request, and a body that asks a gateway for something else still cannot produce a signature over the registered query hash. scripts/admin.ts register-recipe checks the canonical hash, the signature and the template against a real signed response before it prints the registration.
Paid requests escrow before publication. Their final target is max(requestBlock, epochCommitBlock+1), so the final block hash is unknown when the source packet is committed. The request epoch, request block, target/hash, key, consumer, seed and mapping bind the VRF proof. A packet published later cannot substitute a known earlier target hash. An epoch commitment cannot be overwritten by the current implementation.
API3 signatures authenticate exact wrapper-provided bytes and query, not the truth or independence of the underlying source. Raw values can repeat; a different epoch hash does not create new physical entropy. The 240-second attestation bound uses chain timestamps. A persisted response is never refreshed. The only alternative source is the deterministic fallback: attempt n is the source n slots after the selected one and can be committed only from epoch start + n × 20 blocks. By withholding earlier sources for that long the committer can choose among as many signed records as the epoch has sources (five with the rollout catalog, at most ten), but, as with the selected source, the request target hash is still unknown when the record is committed. A larger catalog therefore gives a withholding committer more records to choose from, in exchange for surviving more provider outages; it does not let anyone see the randomness before the target block. Oversized responses fail rather than being cropped.
Each provider is a separate trust domain with one Airnode signer for all its recipes; in the rollout catalog dRPC serves two slots and the others one each. A compromised or faulty provider gateway or Airnode operator can sign any record its recipes' templates accept, and a provider outage makes all of its slots yield no packet; interleaving the catalog so that neighbouring slots never share a signer means each fallback moves to another provider. The signatures prove which signer produced the bytes, not that the values are true, current or independent of each other.
- dRPC block hashes (recipes 1 and 5): a third-party attestation of an Ethereum mainnet or Base block hash, what the dRPC JSON-RPC provider returns for Multicall3 getLastBlockHash() at
latest, the hash of the block before its head. Nothing proves the block canonical or final; the provider's view can lag or sit briefly on a fork. Ethereum proposers can bias a hash only by withholding their own block at the cost of its reward and fees; Base blocks are produced by its sequencer, a single operator that chooses block contents and timing. Neither can see the D20 epoch selection, the later Arc target block hash or the VRF output. - Hyperliquid (recipe 0): the exchange's own BTC daily notional volume, which moves often but is observable market data that Hyperliquid and large traders influence.
- TickerLayer (recipes 2 and 3): the last BTCUSD and ETHUSD trade reported by that provider.
- Nodary (recipe 4): Nodary's own ETH/USD feed value served first-party by its Airnode; it controls both the data and the signature.
Market records from different providers track the same underlying prices and can move together; block hashes are unrelated to them. AirnodeHub gateways can cold-start: a slow or failed fetch retries the same query and, after 20 blocks, falls back to the next slot. After three consecutive transient failures of one Airnode's gateway the keeper skips that Airnode's sources for two minutes, so they fall back at the next window without waiting on it; this changes only which permitted source a keeper tries, never what the registry accepts. The first valid response is kept and never refreshed.
The owner can allow up to four backup committers (setBackupCommitter), for example a follower keeper on a second server. A backup committer has exactly the primary committer's publication power: it can commit any epoch under the same selection, fallback-window, freshness, template and signature rules, and so the same ability to withhold sources or choose among signed records when several sources are open. It gains no other role, and cannot rotate itself or the primary. The coordinator pays the keeper share of a request to the wallet that submitted its accepted proof when the registry authorizes that wallet (committer or backup committer), so a backup committer earns what it serves and the owner chooses who can earn; any other submitter's fulfillment still pays committer(). Its key is a separate operational key to protect and fund, and removing it takes effect at its next transaction. A follower keeper also holds the coordinator's VRF key, because every proof must verify against that one key, so a second host widens the VRF key's exposure exactly as much as the first. A follower decides when to send from public chain state only: the age of unserved work and whether the registry committer's confirmed nonce has advanced recently. It keeps no lease and trusts no heartbeat, so a dishonest or compromised primary cannot suppress it beyond its delays, and it cannot be induced to race a working primary by anything other than real chain activity. A host running followers for several networks as Docker instances holds each of those networks' VRF and follower keys in separate volumes under one Docker daemon, whose administrators can read all of them.
A fixed key and fixed input have a unique valid VRF output under the cryptographic assumptions. The operator can withhold a proof or disclose it privately. Block producers and RPC trust remain part of the model; no unbiased beacon or threshold network is claimed. The Rust worker uses a separate VRF key and transaction wallet. Host compromise can expose both; an HSM or distributed signer is not implemented.
Both service addresses are ERC1967 proxies using owner-authorized UUPS upgrades. Implementation initializers are locked, and each proxy initializes atomically with an explicit owner. Ownership transfers require two-party handoff; renounceOwnership is disabled on both proxies, so upgrade authority can move only through an accepted two-step transfer and can never be abandoned by mistake. Owner may rotate the keeper/fee recipient, adjust the keeper share, tune bounded pricing, lower the expiry refund ratio to at most half retention, register recipes, allow backup committers and schedule future signer catalogs; current code provides no setter that rewrites a request, a published epoch, a registered recipe or the VRF key.
Upgrade authority can replace those rules. A stable proxy address is not immutable protocol logic. Treat governance/owner control as a material trust assumption. Review storage layout, namespaced dependency storage and preserved economic/cryptographic behavior before an upgrade; test live pending requests, earned fees, keeper/refund credits, epochs and historical replay. Local storage baselines detect unreviewed layout drift; they are not an automated proof that a new implementation is safe. Since the recipe-registry upgrade, recipes are registry data rather than code: adding one needs no upgrade, but it remains an owner trust event. That upgrade keeps every declared slot and type of both deployed layouts (640b60c on Arc Mainnet, 96cc722 on Arc Testnet), reserves the retired slot 8 and takes one slot from the gap; its reinitializer registers the built-in recipes inside upgradeToAndCall, runs once per proxy and refuses a registry whose slot 8 or scheduled catalogs would give old recipe ids a new meaning. scripts/admin.ts accepts an upgrade target only when its runtime code equals the freshly compiled implementation at that address.
Since the Arc Mainnet release every protocol change ships as an in-place upgrade of the live proxies: storage-compatible with the deployed layouts, with new state initialized by a reinitializer inside upgradeToAndCall, with requests pending at upgrade time still served, and without breaking the consumer ABI of the coordinator and D20VRFConsumer. Upgrade tests start from the implementation bytecode recorded from both live networks (test/fixtures), not from rebuilt sources.
Keeper startup, signing and broadcast paths pin both proxy slots and implementation runtime hashes. Unreviewed changes stop sends. These observations do not guarantee that governance cannot upgrade after a preflight; operational upgrades should drain/stop the keeper and update reviewed pins deliberately. Historical verification needs the implementation history of both proxies at the relevant transactions, not merely today's code.
Valid proof acceptance is timely at timestamp<=deadline, with deadline=requestedAt+60 seconds. Callback failure still earns the fee; same-result retry cannot reroll or pay another keeper share. Proof verification without an accepted transaction does not establish fulfilled service.
Each request pays the quote computed in its own transaction, max(minFee, feeMultiplier × block.basefee × (fulfillGasOverhead + callbackGasLimit)), and stores it as feePaid; keeper share, treasury share and refund settle from that stored amount, so a pricing change never touches open escrow. The owner can move pricing only within fixed bounds (minFee at most 10 native units, multiplier 0–20, overhead 100,000–2,000,000 gas), and the initial minimum fee stays bound into protocolConfigurationHash. Overpayment is credited to the refund address as withdrawable refund credit and is never revenue. Off-chain quoting must use the header base fee through quoteFeeAt: eth_call commonly reports block.basefee as 0, so quoteFee is exact only inside the requesting transaction.
Only verified service funds keeper payouts and treasury earnedFees. Payout goes to the submitting wallet when the registry authorizes it as committer or backup committer, and to the configured committer for any other submitter, so an arbitrary submitter can never be paid. If the registry authorization view reverts, the payment falls back to committer() rather than being skipped. Failed keeper transfers become backed credits. Pending fees and refund credits cannot be withdrawn as revenue. Refund destinations are fixed, and failed native transfers retain credit controlled only by that recipient. An expiry refund pays feePaid × refundBps / 10000 using the ratio snapshotted into the request at creation, so setRefundBps (owner, no less than 5,000, default 10,000) only affects future requests and can never retroactively cut an open escrow; the retained remainder becomes earnedFees. Service refunds do not reverse application-specific payments.
Callbacks and withdrawals are non-reentrant; callback gas is bounded, return data is not copied, and sufficient outer gas must remain for settlement bookkeeping. Application fairness still requires fixing its own inputs/list/rules and preventing outcome-dependent rerolls.
The SQLite WAL/FULL journal saves proof/calldata and signed transactions before broadcast. One wallet nonce lane reconciles receipts, identical-byte retries, fee replacements and cancellation. A reboot rescans pending request IDs rather than relying on live events. Expired requests are skipped and remain refundable. Unknown consumed nonces fail closed instead of being forgotten.
Durable request classification uses finalized state; receipt resolution additionally validates canonical block and transaction identity. A persisted finalized checkpoint changing stops processing for investigation. This is not automatic recovery from a consensus finality violation. Arc Testnet is the currently approved finality profile; see the finality boundary before deploying elsewhere.
Unused snapshots retire after 50 epochs only when demand/recovery guards permit. Committed terminal payloads can be compacted; chain events retain public evidence. An unpublished snapshot has no chain archive. Back up the full state/lock/WAL and key volumes consistently; do not delete bindings to bypass recovery checks.
Deployment selects a reviewed network from chains.json, pins the deterministic factory code and checks each CREATE2 address against exact init code and salt. Private environment loading reads only explicitly authorized fields without logging values. Owner and keeper are separate. All funding/fee values in the pilot are test values; public-chain cost, finality, refund behavior and operational performance require measured validation before pricing or release.