Docs v2 · testnet

Protocol · Resolver network

Independent resolvers turn sources into facts

A resolver is a small Node.js daemon holding one authorized key. It finds Novak events on-chain, reads the real source for each one, submits what it saw with an evidence hash, sits on dispute committees, and gets paid from market fees when it was right.

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

1

Discover

EventCreated logs

2

Observe

adapter reads mainnet

3

Submit

outcome + evidence hash

4

Vote

if drawn onto a committee

5

Claim

TreasuryVault thirds

Duty

Discovery

What it does

Scans the Registry's EventCreated and the Composer's CompositeEventCreated logs from the deployment's startBlock, in chunks that halve on RPC limits.

Runs on

every node

Code

node/discovery.ts

Duty

Resolve

What it does

For each open event it hasn't answered: decode the spec, run the matching adapter, submitObservation(outcome, evidenceHash). Abstains when the adapter abstains.

Runs on

every node

Code

node/resolve.ts

Duty

Committee voting

What it does

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.

Runs on

every node (RESOLVER_VOTER, default on)

Code

node/voter.ts

Duty

Keeper

What it does

Pokes every permissionless transition: finalize, expire, escalateNonConvergence, escalateTier2, voidAfterTier2Timeout, tryResolve, settle markets, vault allocate / sweep, withdraw.

Runs on

one node (RESOLVER_KEEPER=true)

Code

node/keeper.ts

Duty

Rewards

What it does

Claims this node's resolver third and committee third from the TreasuryVault (claims also run allocate).

Runs on

every node

Code

node/rewards.ts

Duty

Evidence server

What it does

GET /health and GET /evidence/:hash — the full JSON behind each evidence hash.

Runs on

every node (RESOLVER_HTTP_PORT)

Code

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

chainlink.price-at.v1

Question

Feed F ≥ / ≤ threshold X at time T? (v2, occurredAt = T)

Reads

Chainlink AggregatorV3 on mainnet; binary search for the latest round with updatedAt ≤ T

Abstains when

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

Source name

rh.corporate-action.v1

Question

Stock token S had a multiplier change ≥ N bps take effect in [start, end]? (v2)

Reads

ERC-8056 UIMultiplierUpdated(old, new, effectiveAt) logs on mainnet

Abstains when

the window hasn't ended and no qualifying change has taken effect yet

Source name

rh.trading-status.v1

Question

Stock token S is NOT tradable in session X when observed? (v1)

Reads

Robinhood asset registry (api.robinhood.com/rhj/assets), tradingCapabilities

Abstains when

the API is unreachable

Source name

macro.fomc.v1

Question

Fed funds target upper bound on day D ≥ / ≤ X bps? (v2, occurredAt = D)

Reads

FRED series DFEDTARU (public CSV, no key)

Abstains when

FRED hasn't published day D yet

Why logs, not historical state?

The public Robinhood Chain mainnet RPC is not an archive node — historical 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. 1

    Window opens

    Observations are rejected before the event's openTimestamp and after its observationDeadline. Only authorized resolvers can submit, once each.
  2. 2

    Each resolver submits

    submitObservation(eventId, outcomeData, evidenceHash). Version-2 payloads are abi.encode(bool, uint64 occurredAt) and occurredAt can't be in the future. The Registry records each resolver's boolean for rewards.
  3. 3

    Quorum reached

    When quorumThreshold resolvers submitted the identical payload (same hash), that payload becomes the proposed outcome and the dispute window starts.
  4. 4

    Final, or escalated

    No dispute in the window → the keeper calls finalize. Quorum never reached by the deadline (a split) → escalateNonConvergence sends 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

RESOLVER_PRIVATE_KEY

Default

—

Meaning

The authorized resolver key (required). Needs testnet ETH for gas and committee bonds.

Variable

NOVAK_CHAIN_ID

Default

46630

Meaning

Chain the contracts are on (31337 for local anvil).

Variable

RPC_URL_ROBINHOOD_TESTNET

Default

public testnet RPC

Meaning

Where observations are written.

Variable

RESOLVER_SOURCE_RPC_URL

Default

public mainnet RPC

Meaning

Where sources are read. Use an Alchemy URL in production — the public one is rate-limited.

Variable

RESOLVER_ID

Default

resolver

Meaning

Name in logs and /health.

Variable

RESOLVER_KEEPER

Default

false

Meaning

Exactly one node should set true.

Variable

RESOLVER_VOTER

Default

true

Meaning

Vote when drawn onto a committee.

Variable

RESOLVER_HTTP_PORT

Default

0 (off)

Meaning

Port for /health and /evidence/:hash.

Variable

RESOLVER_POLL_INTERVAL_MS

Default

15000

Meaning

Loop interval.

Variable

RESOLVER_LOG_CHUNK

Default

500000

Meaning

Initial block range per getLogs call.

Start it

terminal
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 probe

Becoming a resolver

A key must be authorized on the Registry (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.