Integrate
Quotes are signed off-chain and posted on demand. Anyone may relay a quote; only the signature is trusted. The contract is live on Robinhood Chain mainnet, an Arbitrum Orbit L2, so the same Solidity deploys to Arbitrum unchanged.
1 · From TypeScript, use the SDK
The enum ordering, the digest encoding and the band arithmetic all have to match the contract exactly, and all three are easy to get subtly wrong by hand. @hoodoracle/sdk ships them, and defaults to refusing a price that is not an observed print.
npm install @hoodoracle/sdk viem
import { HoodOracle, QuoteRejected } from "@hoodoracle/sdk";
const oracle = new HoodOracle(); // mainnet, no config needed
try {
// Throws unless this is a live print inside 150bps.
const price = await oracle.price("HOOD", { maxBps: 150 });
liquidate(position, price);
} catch (e) {
// On a Saturday: "provenance is DERIVED: the tape was shut and
// this price is a model output, not an observed print"
if (e instanceof QuoteRejected) return;
throw e;
}
// Value collateral at the pessimistic edge, debt at the other.
const floor = await oracle.conservativePrice("HOOD", "collateral");React bindings at @hoodoracle/sdk/react. Prices are bigint at 8 decimals throughout, because the band comparison has to be exact — see the SDK readme for why floats change answers here.
2 · Read the consumer interface
The function a liquidation path should call is getPriceIfTraded. It refuses to return a modelled weekend price at all, rather than returning one and hoping the caller checks.
interface IHoodOracle {
struct Quote {
uint128 price; // 8 decimals
uint64 confidenceBps;
uint8 session; // 0 REG 1 PRE 2 POST 3 CLOSED 4 HOLIDAY
uint8 provenance; // 0 TRADED 1 DERIVED 2 STALE
uint8 sourceCount;
uint64 maxDeviationBps;
uint64 lastTradeTime;
uint64 publishTime;
}
function getQuote(string calldata t) external view returns (Quote memory);
function getPriceIfTraded(string calldata t, uint64 maxBps) external view returns (uint128);
function getBandedPrice(string calldata t, bool lower) external view returns (uint128);
function isLive(string calldata t, uint64 maxBps) external view returns (bool);
}3 · Write policy that was previously impossible
contract LendingMarket {
IHoodOracle public oracle;
// Liquidation demands a live print inside 50bps. A weekend
// quote reverts here rather than liquidating on a model.
function liquidate(address user, string calldata ticker) external {
uint128 px = oracle.getPriceIfTraded(ticker, 50);
_liquidate(user, px);
}
// Collateral is always valued at the pessimistic edge of the
// band, so a wide weekend band automatically reduces borrowing
// power instead of being ignored.
function collateralValue(string calldata ticker, uint256 qty)
public view returns (uint256)
{
uint128 conservative = oracle.getBandedPrice(ticker, true);
return (uint256(conservative) * qty) / 1e8;
}
// New borrows pause while the tape is shut.
function borrow(string calldata ticker, uint256 amount) external {
require(oracle.isLive(ticker, 100), "market shut");
_borrow(msg.sender, amount);
}
}4 · Relay a quote on-chain
The on-chain value is only as current as the last relay, and anyone may post. From the SDK that is oracle.postQuote(wallet, signed); by hand it is the tuple below, which has to be packed in exactly this field order or the signature will not recover.
import { createWalletClient, http } from "viem";
import { arbitrumSepolia } from "viem/chains";
const r = await fetch("https://your-host/api/quote/HOOD").then((x) => x.json());
await wallet.writeContract({
address: HOOD_ORACLE,
abi: hoodOracleAbi,
functionName: "postQuote",
args: [
r.quote.ticker,
{
price: BigInt(Math.round(r.quote.price * 1e8)),
confidenceBps: BigInt(r.quote.confidenceBps),
session: r.quote.session,
provenance: r.quote.provenance,
sourceCount: r.quote.sourceCount,
maxDeviationBps: BigInt(Math.round(r.quote.maxDeviationBps)),
lastTradeTime: BigInt(r.quote.lastTradeTime),
publishTime: BigInt(r.quote.publishTime),
},
r.signature,
],
});5 · Batch, and find out what is stale
HoodOracleKeeper sits beside the oracle. It mints no authority — every quote it forwards is still checked against the oracle's own signer allow-list, and it holds no funds and has no owner — so anything done through it could have been done without it, just in more transactions.
keeper 0xc984336bf8f5218c601bbb1a83a070262b694aee
interface IHoodOracleKeeper {
// Posts several quotes in one transaction. A quote the oracle
// refuses is reported false, not thrown, so one raced ticker
// cannot discard the rest of the batch.
function postQuotes(string[] calldata tickers, Quote[] calldata qs, bytes[] calldata sigs)
external returns (bool[] memory posted);
// Free. maxAge = 0 uses the oracle's own maxQuoteAge.
function needsUpdate(string[] calldata t, uint64 maxAge) external view returns (bool[] memory);
function status(string[] calldata t, uint64 maxAge) external view returns (Status[] memory);
}Batching saves about 21% of the gas, but the reason to use it is that all eight quotes land in one block. Posted separately they land across eight, so a consumer reading mid-round gets a snapshot that never existed: HOOD from one block and TLT from forty later, when both were priced against a single proxy reading.
needsUpdate matters for a different reason. Which tickers are stale used to be known only to the scheduler posting them, behind a shared secret — one cron job as a single point of failure for a feed anyone is allowed to write to. Now anyone can run a keeper.
import { HoodOracleKeeper } from "@hoodoracle/sdk";
const keeper = new HoodOracleKeeper({ address: KEEPER });
// What should be posted again? Free to ask, no permission needed.
const stale = await keeper.needsUpdate(["HOOD", "COIN", "TLT"]);
// Check what the chain would accept before paying for it.
const willLand = await keeper.simulate(signedQuotes, account);
await keeper.postQuotes(wallet, signedQuotes);Guards the contract enforces
| Guard | Why |
|---|---|
UnknownSigner | Only allow-listed signers are accepted. |
NotNewer | Blocks replay of an older quote, including re-posting a stale favourable price. |
QuoteTooOld / QuoteFromFuture | Bounds clock skew in both directions. 60s of forward tolerance, configurable age limit backwards. |
BandTooWide | A quote past the publish ceiling is rejected rather than stored. |
ZeroPrice | Upstream returns Price: 0 for unsupported tickers. That must never reach storage. |
InvalidEnum | Out-of-range session or provenance bytes are refused, so a consumer's switch cannot fall through. |
Contract source: contracts/HoodOracle.sol