Repository navigation
Epoch Processing Pipeline
RunTimeAdmin edited this page Nov 5, 2026
·
34 revisions
**Referenced Files in This Document**
- [index.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/index.js)
- [epoch-policy.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/epoch-policy.js)
- [chain.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/chain.js)
- [scoring.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/scoring.js)
- [payments.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/payments.js)
- [payment-scan.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/payment-scan.js)
- [store.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/store.js)
- [merkle.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/merkle.js)
- [external.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/external.js)
- [jobs.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/jobs.js)
- [response-cache.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/response-cache.js)
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
This document explains the oracle’s epoch processing pipeline that transforms raw payment-backed attestations into normalized reputation scores. It covers:
- Two-phase processing: stateless reads followed by sequential writes with nonce management.
- Payment scanning that backfills historical settlement data and maintains chain synchronization.
- Challenge window coordination between primary and checker operators.
- Divergence detection when operators compute different scores for the same agent.
- ERC-8183 job registry integration as a read-only evidence source.
- Performance optimizations including chunked processing, concurrent RPC calls, and caching.
- Error handling, retry logic, and recovery procedures for failed epochs.
The oracle is implemented as a Node.js service with clear separation of concerns:
- Orchestrator and HTTP API:
index.js - Epoch decision logic:
epoch-policy.js - Chain interaction and retries:
chain.js - Scoring model:
scoring.js - Payment verification and decay math:
payments.js - Pull-based payment scanner:
payment-scan.js - Persistent state and divergence records:
store.js - Merkle evidence commitment:
merkle.js - External ERC-8004 feedback:
external.js - ERC-8183 job registry reader:
jobs.js - Response cache for expensive evidence responses:
response-cache.js
graph TB
Index["index.js<br/>HTTP API + epoch orchestrator"] --> Policy["epoch-policy.js<br/>decideAction / decideCheckerAction"]
Index --> Chain["chain.js<br/>RPC wrapper, retries, proposals"]
Index --> Payments["payments.js<br/>verification, decay, web-of-trust"]
Index --> Scan["payment-scan.js<br/>pull Transfer logs"]
Index --> Store["store.js<br/>attestations, flags, events, divergences"]
Index --> Merkle["merkle.js<br/>evidence roots and proofs"]
Index --> External["external.js<br/>ERC-8004 feedback"]
Index --> Jobs["jobs.js<br/>ERC-8183 jobs (read-only)"]
Index --> Cache["response-cache.js<br/>LRU evidence cache"]
Diagram sources
- index.js:1-68
- epoch-policy.js:1-21
- chain.js:1-193
- scoring.js:1-8
- payments.js:1-27
- payment-scan.js:1-40
- store.js:1-11
- merkle.js:1-16
- external.js:1-18
- jobs.js:1-33
- response-cache.js:1-34
Section sources
- Epoch orchestrator (
index.js)- Loads configuration, initializes chain and external modules, runs periodic epochs, exposes HTTP endpoints for attestations, evidence, divergence, jobs, badges, and scoring previews.
- Refuses to start on a numeric setting that is set but unusable, and on an
ORACLE_MODEother than unset,primaryorchecker, since a misspelt mode would otherwise run a primary. - Coordinates payment scanning before scoring to ensure both operators converge on the same evidence set. Before that, where
PAYER_OUTCOMES_PEERSis set, it imports the payer-signed outcomes the other operators hold, so each epoch scores every outcome a payer signed, not only those posted to this operator.
- Epoch policy (
epoch-policy.js)- Pure functions for deciding whether to propose, finalize, skip, or flag divergence.
- Validates epoch interval and divergence tolerance to prevent misconfiguration from silently breaking checks.
- Holds the clock-skew check that stops an epoch when the chain's clock and the host's disagree, and the rule
/healthuses to call scoring stale. - Decides when a pending proposal can be finalized in one place,
finalizableAtOf: the contract's storedfinalizableAt, orproposedAtplus the live window for a proposal made before that field existed (it reads zero), as the contract judges it. The primary, the checker, the divergence log and the watcher all use it, so a window changed after a proposal neither delays its finalization nor makes the oracle send a transaction the contract refuses withChallengeWindowActive.
- Chain layer (
chain.js)- Wraps RPC calls with exponential backoff and transient error detection (
read-retry.js), and asks for a log read the node refuses for its size in halves (log-spans.js). - Manages proposal finalization, pending score inspection, fee gating, and operator standing.
- Wraps RPC calls with exponential backoff and transient error detection (
- Scoring model (
scoring.js)- Computes six factors: fee, success, age, external, community, propagation.
- Uses chain clock to ensure deterministic results across operators.
- Payment system (
payments.js)- Verifies ERC-20 transfers, applies decay, diversification, self-payment checks, and web-of-trust multipliers.
- Produces measured fee volume and activity windows used by scoring.
- Payment scanner (
payment-scan.js)- Pulls Transfer logs over block ranges, plans credits, and persists checkpoints.
- Includes canary validation to prove query correctness when no payments are found.
- State store (
store.js)- Persists attestations, flags, links, payment events, used transactions, and divergence records.
- Provides pruning and cooldown mechanisms, and keeps the previous save as a backup it falls back to.
- Evidence commitment (
merkle.js)- Builds Merkle trees over payment events and generates proofs for auditors.
- External feedback (
external.js)- Reads ERC-8004 feedback (as of the start of the UTC day when the registry is on the oracle's own chain, as on Arc testnet) from the addresses that paid the agent, at most the 100 most paid and the newest 10 rows of each, normalizes recognized tags, counts only reviewers that pass
reviewers.js, and caps the external score at 25. The factor is kept per snapshot unless the evidence changes, and a node failure is thrown, which skips the agent, rather than read as 0 (external.js:44-48, external.js:178-191, index.js:473-490). A counterparty's standing, as the web of trust reads it, is further limited by its matured total.
- Reads ERC-8004 feedback (as of the start of the UTC day when the registry is on the oracle's own chain, as on Arc testnet) from the addresses that paid the agent, at most the 100 most paid and the newest 10 rows of each, normalizes recognized tags, counts only reviewers that pass
- ERC-8004 links (
link-scan.js,link-claims.js)- With
EXTERNAL_LINK_FROM_BLOCKset and the registry on the oracle's own chain, each epoch reads the registry'ssigvaraMetadataSetlogs since the last scan and keeps the claims made by the agent's operator (index.js:595-607). An epoch whose scan has not reached the day's snapshot block skips the agents its unchecked logs name; one that cannot list those logs, or read the registry's chain, scores nothing (index.js:1154-1219).
- With
- Job registry (
jobs.js)- Observes ERC-8183 escrow lifecycle events and converts them to evidence-compatible records without feeding scores directly.
- Response cache (
response-cache.js)- Byte-bounded LRU cache keyed by revision token to avoid recomputing expensive evidence payloads.
Section sources
- index.js:79-278
- epoch-policy.js:16-69
- chain.js:172-193
- scoring.js:130-155
- payments.js:46-104
- payment-scan.js:84-118
- store.js:22-89
- merkle.js:18-50
- external.js:26-43
- jobs.js:53-76
- response-cache.js:35-43
The epoch pipeline follows a two-phase design, after a pre-flight check:
- Pre-flight
- Confirm the RPC serves the chain named by
EXPECTED_CHAIN_ID(when set) and the chain the state file was saved on, warn about transactions from this wallet still pending from an earlier epoch, and compare the chain's latest block time with the host clock. - A gap past
MAX_CLOCK_SKEW_SECONDS(600 by default) means one of the two clocks is wrong, because every payment is dated and every score aged by the block time the RPC reports. No epoch runs until they agree. A failed pre-flight aborts the epoch, which is logged and counted, and the next tick tries again.
- Confirm the RPC serves the chain named by
- Phase 1: Stateless reads
- Fetch registered agents and their info concurrently in chunks.
- Read pending scores and fee coverage.
- Run payment scan to backfill historical settlements.
- Phase 2: Sequential writes
- Guard reads per chunk to validate current status and bonding.
- Decide action based on challenge window and mode (primary vs checker).
- Compute scores and the evidence root, charge epoch fees (primary only, after a dry run of the proposal), and propose or finalize.
sequenceDiagram
participant Timer as "Epoch Timer"
participant Index as "index.js"
participant Chain as "chain.js"
participant Scan as "payment-scan.js"
participant Store as "store.js"
participant Policy as "epoch-policy.js"
participant Contract as "SigvaraReputation"
Timer->>Index : trigger runEpoch()
Index->>Chain : assertChainId(), clock-skew check
Index->>Chain : getRegisteredAgents()
Index->>Scan : runPaymentScan(agents)
Scan->>Chain : provider.getLogs(...)
Scan-->>Index : payments found
Index->>Store : creditPayment(...)
Index->>Chain : getChallengeWindow(), getLatestBlockTimestamp()
Index->>Policy : decideAction(pending, window, now)
alt Primary proposes
Index->>Contract : proposeReputation(scores, evidenceRoot)
else Checker agrees and the window passed by the grace
Index->>Contract : finalizeReputation(didHash)
end
Diagram sources
- index.js:681-701
- index.js:1040-1225
- index.js:1248-1259
- index.js:1294-1378
- index.js:1696-1756
- payment-scan.js:415-439
- store.js:647-702
- epoch-policy.js:16-21
- chain.js:813-825
Section sources
- Concurrency control
- Phase 1 uses chunked concurrency to avoid RPC bursts while preserving input order. A chunk is sized in calls to fit one JSON-RPC batch of at most 20 (
chunkForReads), which is 10 agents at two reads each or 6 at three with fee gating active, and the epoch waitsREAD_PACE_MS, 1.5 seconds, between chunks (epoch-policy.js:295-308, index.js:1247-1259). - Phase 2 enforces sequential writes because all proposals share the oracle wallet nonce; concurrent submissions would collide.
- Phase 1 uses chunked concurrency to avoid RPC bursts while preserving input order. A chunk is sized in calls to fit one JSON-RPC batch of at most 20 (
- Guard reads
- Before each write, fresh agent info and bonding status are batched per chunk to minimize stale-state risk. This step waits
READ_PACE_MSbefore every chunk, since its first follows phase 1 (index.js:1292-1296).
- Before each write, fresh agent info and bonding status are batched per chunk to minimize stale-state risk. This step waits
- Fee gating
- Optional per-epoch fee charging occurs before proposing; checker mode never charges to avoid double-debiting, and startup refuses
ORACLE_MODE=checkertogether with a fee registry. - A dry run of the proposal comes first, so an agent the contract would refuse is not charged, and one whose dry run cannot be made is skipped uncharged for the epoch. An agent already charged this epoch is not charged again.
- Optional per-epoch fee charging occurs before proposing; checker mode never charges to avoid double-debiting, and startup refuses
- Proposal strategy
- Primary may overwrite its own stale proposal.
- Checker uses compare-and-swap (
proposeIfEmpty) to avoid overwriting another party’s proposal. - On main, a pending proposal whose proposer has lost its authority cannot be finalized. The primary replaces it with a fresh proposal, and a checker treats it as an empty slot once a dry run shows the contract would accept its own proposal there. Both are counted as
proposalsStranded.
- Counterparty standing
- A payer’s standing is read once per epoch and retried when the node throttles or drops the read. A read that still fails skips that agent for the epoch rather than scoring it as no standing.
flowchart TD
Start(["Epoch start"]) --> PreFlight["Pre-flight: chain id, clock skew"]
PreFlight --> Scan["Run payment scan<br/>backfill settlements"]
Scan --> Phase1["Phase 1: Concurrent reads<br/>getAgentInfo, getPendingScore, isCovered"]
Phase1 --> Phase2Start["Phase 2: Sequential writes"]
Phase2Start --> Guards["Guard reads per chunk<br/>getAgentInfo, isBonded"]
Guards --> Decision{"Primary or Checker?"}
Decision --> |Primary| Compute["Compute score + evidence root"]
Compute --> Charge["Dry-run the proposal, then charge epoch fee (if configured)"]
Charge --> Propose["proposeReputation"]
Decision --> |Checker| Audit["Audit against proposedAt clock"]
Audit --> Compare{"Agree within tolerance?"}
Compare --> |No| RecordDivergence["Record divergence"]
Compare --> |Yes| FinalizeCheck{"Window passed by the grace?"}
FinalizeCheck --> |Yes| Finalize["finalizeReputation"]
FinalizeCheck --> |No| Skip["Skip, or leave it to the primary"]
Propose --> End(["Epoch done"])
Finalize --> End
RecordDivergence --> End
Skip --> End
Diagram sources
Section sources
- Backfilling
- Scans ERC-20 Transfer logs for agent addresses in configurable chunks and recipient batches (50 recipients to a call), with
PAYMENT_SCAN_PACE_MS(500 ms by default) between calls so a long catch-up finishes instead of looping on refusals. A span the node refuses for its size is asked for in halves, at the same pace (payment-scan.js:430-433, log-spans.js:44-62). - Plans credits using pure logic, then persists via store with deduplication keyed by transaction, recipient, and payer.
- Skips a payment from the agent's address, or from its operator at the payment's own block, as
self_payment. The registry scan readsOperatorTransferredin one filter withAgentRegisteredto know who that was (chain.js:247-268, payment-scan.js:388-398). - Skips a payment in a block before the one the agent registered in as
before_registration; the registration's own block counts (payment-scan.js:245-249). - Off by default (
PAYMENT_SCAN_ENABLED=1), and it must be on for every operator or for none.
- Scans ERC-20 Transfer logs for agent addresses in configurable chunks and recipient batches (50 recipients to a call), with
- Safety and canary
- Stays behind chain head by minimum confirmations (at least 6) to avoid reorg issues.
- Runs a canary check when no payments are credited to verify the filter shape still finds the newest settlement the operator holds, of any agent. A canary that cannot find it (
failed) holds the checkpoint, so the whole range is scanned again at the next epoch; one that cannot run (unavailable) does not.
- Checkpointing
- Advances only up to the earliest unresolved block timestamp to avoid losing future credits.
- Payment chains (ADR 0009)
- Off by default: with
PAYMENT_CHAIN_IDSunset none of this runs (payment-chains.js:37-39). - A chain that is
onis scanned before the scored chain's scan, in steps ofMAX_BLOCKSsaved as each completes, until it reachesfinalizedor has spent 10 minutes (index.js:211-213, index.js:922-924, index.js:1099-1102). - A chain in
shadow, the default mode, is scanned beside the epoch, one run at a time, and credits nothing, so the epoch does not wait for it (index.js:978-989, index.js:1113). On 4 November 2026 Elysium testnet ran this way on the checker only (0009-payments-on-base.md:236-237). - An operator whose scan of an
onchain has not passed that chain's delay, or whose scored-chain scan is too far behind for a proposal to land in time, scores nothing that epoch and counts it failed. Scans that fall too far behind while the epoch runs leave the remaining agents unscored, and/healthshows either aspaymentChainHold(index.js:1156-1191, index.js:1818-1822).
- Off by default: with
flowchart TD
A["Start scan range [from, to]"] --> B["Batch recipients (max per call)"]
B --> C["Chunk blocks (chunkSize)"]
C --> D["provider.getLogs(asset, topics, fromBlock, toBlock)"]
D --> E["Decode Transfer logs"]
E --> F["planCredits(grouped by tx+recipient+sender)"]
F --> G{"Is already credited?"}
G --> |Yes| H["Skip"]
G --> |No| I["Credit payment if valid"]
I --> J["Persist checkpoint after unresolved-safe boundary"]
H --> K["Next chunk"]
J --> K
K --> L["Canary check if credited == 0"]
L --> M["Report metrics and summary"]
Diagram sources
- payment-scan.js:123-137
- payment-scan.js:193-263
- payment-scan.js:354-386
- payment-scan.js:415-439
- store.js:617-702
Section sources
- payment-scan.js:123-137
- payment-scan.js:193-263
- payment-scan.js:354-386
- payment-scan.js:415-439
- store.js:617-702
- Primary behavior
- If a proposal exists and its review window has ended (
finalizableAt), it finalizes the existing proposal and proposes a new one. - If finalizing is refused because the proposer has lost its authority (on main), it counts the proposal as stranded and proposes a fresh one in its place.
- Otherwise, it skips until the window closes.
- If a proposal exists and its review window has ended (
- Checker behavior
- Re-reads the pending slot immediately before writing to avoid races.
- Recomputes the expected score at the proposal’s
proposedAttime, over the evidence as it stood then, to compare apples-to-apples. A payment that settled after the proposal is one its proposer could not have had, so it is left out, and an outcome a payer signed later reads as unreported when that makes the root match the one the proposal committed to. For a proposal made before evidence had a fixed order, it tries the operator's stored order too, since that proposal committed to its proposer's (evidence-as-of.js:40-44, index.js:351-362). With a payment chainon, it also tries each whole-minute cut-off in the half hour before the proposal, and waits until its own scans have passed the proposal's time (evidence-as-of.js:32-38, index.js:1490-1497). - Finalizes an agreed score, or proposes into an empty slot, only once the primary is
CHECKER_GRACE_MINUTESlate: the window past by that long, or the slot empty that long. On main, a proposal whose proposer has lost its authority counts as an empty slot and is covered at once. - Records divergence details including factor deltas and evidence roots for auditability, and whether its evidence as of the proposal matched the proposal's root (
evidenceMatchesProposal).
sequenceDiagram
participant Checker as "Checker Operator"
participant Chain as "chain.js"
participant Store as "store.js"
participant Policy as "epoch-policy.js"
participant Contract as "SigvaraReputation"
Checker->>Chain : getPendingScore(didHash)
Chain-->>Checker : {exists, proposedAt, finalizableAt, data, evidenceRoot}
Checker->>Policy : decideCheckerAction(pending, own, window, now, tolerance, grace)
alt Agrees and window open
Policy-->>Checker : skip
else Agrees, window passed, primary not yet late
Policy-->>Checker : defer-finalize
else Agrees and window passed by the grace
Policy-->>Checker : finalize
Checker->>Contract : finalizeReputation(didHash)
else Disagrees beyond tolerance
Policy-->>Checker : diverged
Checker->>Store : recordDivergence(...)
end
Diagram sources
Section sources
- Factor-level comparison
- Compares each of the six stored factors and reports deltas where they differ.
- Uses both total difference and maximum single-factor difference against a bounded tolerance (
DIVERGENCE_TOLERANCE, 3 by default and at most 10). A factor that cannot be compared counts as a divergence, not as agreement.
- Evidence diagnostics
- Stores each side’s Merkle evidence root and event counts, under
evidence(ownEvidenceRoot,proposedEvidenceRoot,ownPaymentEvents,ownDistinctPayersandcompareWith), so auditors can diff evidence sets. - Provides a named endpoint for fetching the other side’s evidence.
- Stores each side’s Merkle evidence root and event counts, under
- Persistence and pruning
- Divergences are persisted per agent with bounded history and age-based pruning.
classDiagram
class DivergenceRecord {
+number at
+number proposedAt
+number pendingTotal
+number ownTotal
+object factors
+object? evidence
}
class Store {
+recordDivergence(didHash, divergence, proposedAt, now, evidence)
+getDivergences(didHash)
+pruneDivergences(maxAgeMs, now)
}
Store --> DivergenceRecord : "persists"
Diagram sources
Section sources
- Read-only observation
- Decodes standardized job lifecycle events and assembles per-job records.
- Converts terminal jobs into evidence-compatible records without feeding scores directly.
- Safety and rejection rules
- Refuses self-paid and self-evaluated jobs; expired/refunded jobs produce no evidence.
- Configuration and visibility
- Disabled unless a registry address is provided; health endpoint reports feed status and chain id.
- Exposes
/jobs/<address>for diagnostic queries with bounded scan windows.
flowchart TD
A["GET /jobs/<provider>"] --> B["Enabled?"]
B --> |No| C["Return 501 disabled"]
B --> |Yes| D["scanLogs(registry, topics, chunked)"]
D --> E["decodeLog -> assembleJobs"]
E --> F["toEvidence(providerAddress)"]
F --> G["Return counts, dropped reasons, evidence"]
Diagram sources
Section sources
- Factors
- Fee score: measured volume divided by fee unit, capped.
- Success score: decay-weighted success ratio with pseudo-observations, over the outcomes a payer signed or a token holder recorded.
- Age score: tenure-based logarithmic curve weighted by recency; 0 for an agent with no payments when verification is on.
- External score: normalized ERC-8004 feedback, capped at 25.
- Community score: penalty based on age-weighted flags.
- Propagation score: inherited trust from counterparties’ hard standing.
- Caps
- The six factor caps (20, 15, 30, 25, 5 and 5) live in
oracle/score-caps.js, and a test compares them with the contract’sMAX_*_SCOREconstants.
- The six factor caps (20, 15, 30, 25, 5 and 5) live in
- Determinism
- Uses chain clock for time-dependent factors to ensure identical computation across operators.
flowchart TD
A["Inputs: registeredAt, attestations, flags, externalScore, measuredFeeScore, activity, propagation, now"] --> B["feeScore = min(20, volume/feeUnit)"]
A --> C["successScore = floor(success/(total+prior)*15)"]
A --> D["ageScore = log2(spanDays+1)*3 * recency"]
A --> E["externalScore = clamp(0..25)"]
A --> F["communityScore = max(0, 5 - flags*2)"]
A --> G["propagationScore = min(5, sum(hardStanding/25))"]
B --> H["total = sum(factors)"]
C --> H
D --> H
E --> H
F --> H
G --> H
Diagram sources
- scoring.js:11-19
- scoring.js:35-38
- scoring.js:57-60
- scoring.js:90-98
- scoring.js:100-110
- scoring.js:130-155
Section sources
- scoring.js:11-19
- scoring.js:35-38
- scoring.js:57-60
- scoring.js:90-98
- scoring.js:100-110
- scoring.js:130-155
Key dependencies and relationships:
-
index.jsdepends on all core modules to orchestrate the epoch and serve HTTP endpoints. -
chain.jsdepends on ethers and contract ABIs; provides retry wrappers and proposal/finalization methods. -
payment-scan.jsis pure apart from the log readerindex.jshands it, which wrapschain.js’s backoff. It sharespayments.js’s transfer topic andcountsAsPaymentrule, andindex.jsdoes the crediting and checkpointing throughstore.js. -
payments.jsis pure except for config reading; used by scoring and attestation paths. -
merkle.jsis pure hashing and proof generation used to commit evidence roots. -
external.jsis optional and gated by environment variables. -
jobs.jsis optional and read-only, integrated via HTTP endpoint. -
response-cache.jscaches serialized evidence bodies keyed by revision token.
graph LR
Index["index.js"] --> Chain["chain.js"]
Index --> Policy["epoch-policy.js"]
Index --> Payments["payments.js"]
Index --> Scan["payment-scan.js"]
Index --> Store["store.js"]
Index --> Merkle["merkle.js"]
Index --> External["external.js"]
Index --> Jobs["jobs.js"]
Index --> Cache["response-cache.js"]
Scan --> Payments
Index --> Score["scoring.js"]
Diagram sources
- index.js:1-68
- chain.js:1-193
- payment-scan.js:42-47
- store.js:19-21
- merkle.js:18-19
- external.js:26-43
- jobs.js:53-76
- response-cache.js:35-43
Section sources
- index.js:1-68
- chain.js:1-193
- payment-scan.js:42-47
- store.js:19-21
- merkle.js:18-19
- external.js:26-43
- jobs.js:53-76
- response-cache.js:35-43
- Chunked processing
- Phase 1 reads use
mapChunked(items, size, mapper, paceMs)to cap concurrent calls and preserve order.paceMsis a wait between chunks, not before the first or after the last. - Payment scans split recipients and block ranges to avoid RPC limits. A span a node refuses for its size is read in halves.
- Phase 1 reads use
- Concurrent RPC calls
- Batch independent reads using
Promise.allwithin chunks to reduce round trips. The providers inchain.jsandexternal.jssend at most 20 calls in a batch (MAX_BATCH_CALLSinread-retry.js,batchMaxCount). - Payer standing lookups are grouped and fetched concurrently, 10 at a time and 1.5 seconds apart (
PAYER_READ_CHUNK,PAYER_READ_PACE_MS).
- Batch independent reads using
- Caching strategies
- In-memory payer score cache cleared per epoch.
- External factor kept per snapshot, so the epoch, the checker's re-score and every
GET /scoreshare one reading, unless the evidence changes (index.js:473-490). - Byte-bounded LRU response cache for evidence payloads keyed by revision token.
- Merkle leaves cached against each frozen payment event and roots against each frozen event array, so an agent whose evidence has not changed reuses its root; leaves are also built by hand instead of through the ABI coder (5,000 events took 1,450 ms before and 480 ms after).
- Badge cache with TTL and short TTL for unavailable states.
- RPC resilience
- Exponential backoff for view calls and log scans.
- Distinction between throttling and real errors to avoid resubmitting potentially mempool’d transactions.
- A sent transaction is waited for
TX_WAIT_SECONDS(60 by default), replaced once at twice the fee with the same nonce, and then given up on (counted astxUnconfirmed). The oracle will not sign a fee aboveMAX_FEE_GWEIor a worst-case cost aboveMAX_TX_COST.
Section sources
- index.js:415-471
- index.js:1227-1259
- index.js:1838-1845
- index.js:3093-3130
- chain.js:217-231
- read-retry.js:50-63
- response-cache.js:35-43
- Epoch failures
- Aborted epochs are logged and counted; the guard prevents overlapping runs and protects against nonce collisions.
- A chain mismatch, or a chain clock more than
MAX_CLOCK_SKEW_SECONDSfrom the host’s, aborts the epoch the same way. A payer standing or an ERC-8004 registry the node cannot answer for skips only the agent it belongs to. - Payment scan failures do not abort the epoch; they are retried next cycle with checkpoint resume.
- RPC throttling and rate limits
- Throttled proposals and finalizations are skipped and retried next epoch; metrics distinguish throttling from real errors.
- View calls retry transient network errors with exponential backoff.
- State persistence
- Health endpoint probes writability and last persist result; failures surface as unhealthy status. It also answers 503 once two epoch intervals pass with no successful epoch, counted from process start until the first one succeeds.
- State file writes are atomic (temp + rename + fsync) to avoid corruption. The previous save is kept as
<state>.bak; an unreadable file falls back to it (stateSource: backupon/health), and the process stops rather than starting empty when neither is usable.
- Divergence investigation
- Use
/divergenceendpoints to inspect factor deltas, evidence roots, and recommended comparison endpoint. - Ensure checker epoch is shorter than half the challenge window to observe proposals.
- To follow one agent, the site's Score epochs page (sigvara.xyz/epochs) shows its pending proposal, every past proposal and what became of it, the payments behind its score and whether the checker disagreed, read from the chain, Sigvara's event index (the Arc explorer's when the index does not answer) and both operators' public endpoints.
- Use
- Payment scan canary
- When no payments are credited, the canary verifies the filter shape; “failed” indicates a broken query, not quietness, and the checkpoint stays where it was so the range is scanned again next epoch.
Section sources
- index.js:681-701
- index.js:1107-1112
- index.js:1762-1795
- index.js:2412-2484
- store.js:529-569
- index.js:2822-2875
- payment-scan.js:269-344
The oracle’s epoch pipeline ensures deterministic, auditable reputation scoring through:
- Two-phase processing that separates stateless reads from sequential writes.
- Pull-based payment scanning that aligns operator evidence sets.
- Clear primary/checker roles coordinated by challenge windows.
- Robust divergence detection with actionable diagnostics.
- Optional ERC-8183 job observation for future evidence integration.
- Careful performance tuning and resilient error handling to maintain reliability under RPC constraints.
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions