Architecture
Read mainnet, decide once, serve everyone
The big picture
1 · Sources
Robinhood Chain mainnet (4663) + public APIs
- 53 Chainlink feeds (stocks, SGOV, crypto)
- ERC-8056 stock tokens — multiplier logs
- Robinhood asset API — trading status
- FRED — Fed target rate
2 · Resolver network
independent Node.js daemons
- discover events from logs
- observe via adapters + evidence hash
- vote in dispute committees
- keeper: finalize / escalate / settle
- claim fee rewards
3 · Contracts
Robinhood Chain testnet (46630)
- EventRegistry — specs, quorum, finality
- DisputeManager — bonded committees
- EventComposer + EventBus — compose & read
- TreasuryVault — fee thirds
- DistributionMarket · Market · StockLendingGuard
Why split mainnet and testnet? Chainlink's Robinhood stock feeds and the real ERC-8056 stock tokens only exist on mainnet, so resolvers read the real data there and write their observations to testnet, where everything else runs on test money (MockUSDG). Moving the contracts to mainnet later changes an RPC URL, not the design.
The five layers
Click a layer to see its job and the functions it exposes. A layer only talks to the one directly beneath it.
Source
Robinhood Chain mainnet + public APIs (read-only)Raw facts: Chainlink stock & SGOV feeds, ERC-8056 stock-token multipliers, Robinhood's asset API, the Fed's target rate.
Novak never writes here. Resolver adapters read it (resolver/adapters/): chainlink.price-at.v1, rh.corporate-action.v1, rh.trading-status.v1, macro.fomc.v1. Mainnet is not an archive node, so adapters read logs and round history rather than historical eth_call.
Resolvers & quorum
EventRegistry.solAuthorized resolvers submit what they observed plus an evidence hash; N-of-M identical answers propose the outcome.
Observations are rejected before openTimestamp and after observationDeadline. v2 payloads carry occurredAt. Each resolver's boolean is recorded so the TreasuryVault can pay the ones who were right. Nobody observed it → Expired.
Disputes & finalization
DisputeManager.solA proposed outcome waits out its dispute window; a bonded challenge hands it to resolver committees.
Tier 1: up to 7 resolvers, 1 hour, 66% of the committee decides. No decision → Tier 2: up to 15, 2 hours. Still none → Voided, bonds refunded. Never a token vote. Payouts are pull-based (withdraw()).
Event Bus & composition
EventBus.sol & EventComposer.solFinalized events become standing facts that can be read and combined.
EventComposer builds AND / OR / NOT / BEFORE / WITHIN composites with canonical IDs and bounded depth; a voided operand propagates. EventBus serves primitives and composites through one read API with Pending / Available / Voided.
Applications
DistributionMarket · Market · StockLendingGuard · your contractConsumers read only the Event Bus (via Settlement) and push fees to the TreasuryVault.
Range markets (LMSR) settle on a ladder of threshold events; yes/no markets settle on one event; the lending guard pauses liquidations. Each has a regression test proving it holds no Registry, Composer or resolver reference.
Side by side: an event oracle, not another price feed
What each oracle design gives a prediction market. Chainlink is one of Novak's data sources, not an alternative to it. For how an answer is decided and defended, see Oracle architecture.
Contracts and their roles
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
Contract
Job
Reads
Called by
| Contract | Job | Reads | Called by |
|---|---|---|---|
| EventRegistry | Event specs, observations, quorum, finalize / expire, per-resolver outcomes | — | anyone (create), resolvers (observe), keeper |
| DisputeManager | Bonds, committee draw, Tier-1/Tier-2 votes, VOID, pull payouts | Registry | disputers, committee resolvers, keeper |
| EventComposer | AND / OR / NOT / BEFORE / WITHIN composites, cached resolution | Registry | anyone, keeper (tryResolve) |
| EventBus | One read API for primitives + composites: readOutcome, getAvailability | Registry, Composer | every consumer (through Settlement) |
| Settlement | Thin adapter: (availability, bool outcome) for one or many events | EventBus | markets |
| TreasuryVault | Fee thirds per event, insurance reserve, dispute-bond sweep, claims | Registry, Composer, DisputeManager | markets (deposit), resolvers (claim) |
| DistributionMarket | LMSR range markets over threshold ladders | Settlement only | traders, keeper (settle) |
| Market | USDG yes/no parimutuel markets | Settlement only | traders, keeper (settle) |
| StockLendingGuard | canLiquidate(token): pause while a risky event is open | EventBus only | lending protocols |
The one rule
Applications depend on the Event Bus — never on a resolver, the Registry or the Composer.
Settlement reference (which holds an IEventBus), so the resolver network can add sources, change quorum rules or add resolvers without redeploying a single market. Consumers may additionally push fees into ITreasuryVault; the vault — protocol infrastructure — is the one that reads the Registry and DisputeManager to decide who gets paid.Consumer
Holds only
Regression test
Consumer
Holds only
Regression test
Consumer
Holds only
Regression test
Consumer
Holds only
Regression test
| Consumer | Holds only | Regression test |
|---|---|---|
| Market | Settlement | Market.t.sol::test_market_onlyHoldsSettlementReference |
| DistributionMarket | Settlement | DistributionMarket.t.sol::test_holdsOnlySettlementReference |
| StockLendingGuard | IEventBus | StockLendingGuard.t.sol::test_guard_onlyHoldsEventBusReference |
| ExternalPredictionMarket (example) | IEventBus | ExternalPredictionMarket.t.sol::test_holdsOnlyEventBusReference |
One range market, end to end
- 1
Create the ladder
SDKThe creator builds N−1 threshold events (“NVDA ≥ $230 at T”, “≥ $232”, …) withbuildPriceLadderSpecsand creates them in one transaction withcreateEvents. - 2
Open the market
DistributionMarketcreateMarket(question, boundaryIds, tradingClosesAt, b)pulls the LMSR subsidyb·ln Nin USDG. Every range starts at 1/N. - 3
Trade until close
tradersBuy or sell any range; prices move with the LMSR and always sum to 1. A 1% fee accrues per trade. - 4
Observe at T
resolversAfter T each resolver finds the Chainlink round in effect at T on mainnet and submits TRUE/FALSE per boundary with an evidence hash. - 5
Quorum → dispute window → final
RegistryIdentical answers from the quorum propose the outcome; if nobody disputes before the window ends, the keeper finalizes it. - 6
Settle
keeperWinning range = number of TRUE boundaries. Fees go to the TreasuryVault, tagged with the boundary events; leftover subsidy returns to the creator. - 7
Redeem & claim
everyoneWinners redeem 1 USDG per share; resolvers who reported the final answer claim their third from the vault.
Off-chain pieces
Resolver node (resolver/)
One process per resolver key. Duties run on a poll loop: discovery, resolve, committee voting, keeper (one node with RESOLVER_KEEPER=true), rewards. It serves /health and /evidence/:hash so anyone can check what a resolver saw. See Resolver network.
Frontend data routes (frontend/src/app/api/)
Route
Returns
Cache
Route
Returns
Cache
Route
Returns
Cache
Route
Returns
Cache
| Route | Returns | Cache |
|---|---|---|
| /api/feeds | Latest round for every Chainlink Robinhood feed (list from Chainlink's directory, one mainnet multicall) | 60 s |
| /api/feeds/history | Recent rounds for one feed + realized volatility (drives charts and insights) | 300 s |
| /api/rates | Fed funds target range (FRED) + effective rate (NY Fed) | 30 min |
| /api/rh-assets | Corporate-action calendar: Robinhood's asset registry + each token's ERC-8056 multiplier state | 5 min |
SDK (@novakoracle/sdk)
Generated ABIs and deployment addresses (never hand-edited), a typed NovakClient, source-spec encoders, the 53-feed Chainlink catalog and ladder helpers. See the SDK reference.