Protocol · Resolver network
Independent resolvers turn sources into facts
Source adapters
4
price-at · corporate action · trading status · Fed rate
Duties per node
5
discover · resolve · vote · keep · claim
Quorum
N-of-M
exact match of the outcome payload
Live today
3
authorized resolvers · quorum 2 of 3 (team-run)
What one resolver does
Discover
EventCreated logs
Observe
adapter reads mainnet
Submit
outcome + evidence hash
Vote
if drawn onto a committee
Claim
TreasuryVault thirds
Duty
What it does
Runs on
Code
Duty
What it does
Runs on
Code
Duty
What it does
Runs on
Code
Duty
What it does
Runs on
Code
Duty
What it does
Runs on
Code
Duty
What it does
Runs on
Code
| Duty | What it does | Runs on | Code |
|---|---|---|---|
| Discovery | Scans the Registry's EventCreated and the Composer's CompositeEventCreated logs from the deployment's startBlock, in chunks that halve on RPC limits. | every node | node/discovery.ts |
| Resolve | For each open event it hasn't answered: decode the spec, run the matching adapter, submitObservation(outcome, evidenceHash). Abstains when the adapter abstains. | every node | node/resolve.ts |
| Committee voting | For each Disputed event where this node was drawn onto the active tier: re-observe with the same adapter and vote, posting the tier bond. | every node (RESOLVER_VOTER, default on) | node/voter.ts |
| Keeper | Pokes every permissionless transition: finalize, expire, escalateNonConvergence, escalateTier2, voidAfterTier2Timeout, tryResolve, settle markets, vault allocate / sweep, withdraw. | one node (RESOLVER_KEEPER=true) | node/keeper.ts |
| Rewards | Claims this node's resolver third and committee third from the TreasuryVault (claims also run allocate). | every node | node/rewards.ts |
| Evidence server | GET /health and GET /evidence/:hash — the full JSON behind each evidence hash. | every node (RESOLVER_HTTP_PORT) | node/server.ts |
Source adapters
An event names its source by sourceId = keccak256(name). Every adapter is deterministic — all honest resolvers reading the same source for the same spec produce the same answer and the same evidence hash — and abstains (submits nothing) rather than guess.
Source name
Question
Reads
Abstains when
Source name
Question
Reads
Abstains when
Source name
Question
Reads
Abstains when
Source name
Question
Reads
Abstains when
| Source name | Question | Reads | Abstains when |
|---|---|---|---|
| chainlink.price-at.v1 | Feed F ≥ / ≤ threshold X at time T? (v2, occurredAt = T) | Chainlink AggregatorV3 on mainnet; binary search for the latest round with updatedAt ≤ T | T hasn't passed on mainnet yet; the round at T is older than maxStaleness (stock feeds hold the last price off-hours); T predates the current phase |
| rh.corporate-action.v1 | Stock token S had a multiplier change ≥ N bps take effect in [start, end]? (v2) | ERC-8056 UIMultiplierUpdated(old, new, effectiveAt) logs on mainnet | the window hasn't ended and no qualifying change has taken effect yet |
| rh.trading-status.v1 | Stock token S is NOT tradable in session X when observed? (v1) | Robinhood asset registry (api.robinhood.com/rhj/assets), tradingCapabilities | the API is unreachable |
| macro.fomc.v1 | Fed funds target upper bound on day D ≥ / ≤ X bps? (v2, occurredAt = D) | FRED series DFEDTARU (public CSV, no key) | FRED hasn't published day D yet |
Why logs, not historical state?
eth_call fails. Adapters use immutable logs (multiplier changes) and Chainlink's own round history instead, which any node can re-read later to check an answer.Quorum: how observations become an outcome
- 1
Window opens
Observations are rejected before the event'sopenTimestampand after itsobservationDeadline. Only authorized resolvers can submit, once each. - 2
Each resolver submits
submitObservation(eventId, outcomeData, evidenceHash). Version-2 payloads areabi.encode(bool, uint64 occurredAt)andoccurredAtcan't be in the future. The Registry records each resolver's boolean for rewards. - 3
Quorum reached
WhenquorumThresholdresolvers submitted the identical payload (same hash), that payload becomes the proposed outcome and the dispute window starts. - 4
Final, or escalated
No dispute in the window → the keeper callsfinalize. Quorum never reached by the deadline (a split) →escalateNonConvergencesends it to a committee. Nobody observed at all →expire.
Evidence
Each observation carries evidenceHash = keccak256(canonical JSON) of what the resolver read — feed, round ID, answer, timestamps, or the matched log. The node keeps the JSON and serves it at /evidence/:hash, so a disputer (or a judge) can see exactly what was observed. Because the JSON is canonical, honest resolvers produce the same hash.
Getting paid
Every market fee is attributed to the events it settled on. Once an event is decided, the TreasuryVault gives one third to the resolvers whose recorded boolean matches the final outcome, split equally. Resolvers who were wrong — or silent — get nothing from that event. See Treasury & fee thirds.
Run a resolver
Configuration
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
Variable
Default
Meaning
| Variable | Default | Meaning |
|---|---|---|
| RESOLVER_PRIVATE_KEY | — | The authorized resolver key (required). Needs testnet ETH for gas and committee bonds. |
| NOVAK_CHAIN_ID | 46630 | Chain the contracts are on (31337 for local anvil). |
| RPC_URL_ROBINHOOD_TESTNET | public testnet RPC | Where observations are written. |
| RESOLVER_SOURCE_RPC_URL | public mainnet RPC | Where sources are read. Use an Alchemy URL in production — the public one is rate-limited. |
| RESOLVER_ID | resolver | Name in logs and /health. |
| RESOLVER_KEEPER | false | Exactly one node should set true. |
| RESOLVER_VOTER | true | Vote when drawn onto a committee. |
| RESOLVER_HTTP_PORT | 0 (off) | Port for /health and /evidence/:hash. |
| RESOLVER_POLL_INTERVAL_MS | 15000 | Loop interval. |
| RESOLVER_LOG_CHUNK | 500000 | Initial block range per getLogs call. |
Start it
pnpm install
pnpm --filter @novakoracle/sdk build # the resolver imports the SDK
pnpm --filter novak-resolver build # start runs the compiled dist/
RESOLVER_PRIVATE_KEY=0x… RESOLVER_ID=r1 RESOLVER_HTTP_PORT=8787 \
pnpm --filter novak-resolver start
# check the live sources it will read
pnpm --filter novak-resolver probeBecoming a resolver
setResolverAuthorization, owner-only today) before its observations count. There is no staking token: resolvers post flat ETH bonds when they vote in a dispute. Committees are drawn from the authorized pool; above the tier size they're drawn by commit-reveal, which every node takes part in automatically.