Repository navigation
Deployment and Configuration
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
This document describes how to deploy and operate the Sigvara oracle service, including environment configuration, Docker deployment for development and production, required infrastructure, checker mode operation, fee gating, external trust integration, health checks, monitoring, and security considerations. The oracle computes a normalized six-factor reputation score from payment-verified attestations, watchdog flags, tenure, counterparty standing, and optional ERC-8004 feedback, then proposes the result on-chain with a Merkle root over the evidence used.
The repository also includes a second operator running in checker mode that independently recomputes scores, compares them against pending on-chain proposals, and exposes divergence information for the slashing committee. A separate watcher component monitors checker divergence endpoints and alerts when actionable disagreements occur, and when the checker itself stops checking.
Section sources
The oracle is a Node.js service under oracle/. It reads blockchain state through an RPC endpoint, persists attestation and flag state to a mounted volume, and exposes HTTP endpoints for health, metrics, scoring previews, evidence retrieval, attestation submission, the payer-signed outcomes other operators import, flags, links, jobs, and checker diagnostics.
graph TB
subgraph "Oracle Service"
App["index.js"]
Store["Persistent State<br/>Attestations, Flags,<br/>Payment Events, Divergences"]
Chain["Chain Layer<br/>RPC Reads/Writes"]
External["External Feed<br/>ERC-8004 (Optional)"]
Jobs["Jobs Feed<br/>ERC-8183 (Optional)"]
PaymentScan["Payment Scanner<br/>Pull Mode (Optional)"]
end
subgraph "Infrastructure"
RPC["Blockchain RPC Endpoint"]
DB["Mounted Volume<br/>/data/oracle-state.json"]
Proxy["Reverse Proxy / TLS"]
Metrics["Prometheus / Monitoring"]
end
App --> Chain
App --> External
App --> Jobs
App --> PaymentScan
App --> Store
Chain --> RPC
Store --> DB
Proxy --> App
Metrics --> App
Diagram sources
- oracle/index.js:1-42
- oracle/index.js:293-340
- docker-compose.oracle.yml:1-24
- oracle/Caddyfile.oracle.example:37-142
Section sources
- Oracle process: loads environment configuration, initializes chain and external feed accessors, runs periodic epochs, handles HTTP endpoints, and persists operational state.
- Persistent store: stores attestations, flags, links, payment events, scan checkpoints, divergences, cooldowns, and last persistence metadata.
- Chain layer: reads agent identity, status, pending scores, challenge windows, epoch fees, and writes finalized or proposed scores through the oracle wallet.
- External trust feed: optional ERC-8004 identity/reputation lookup for the externalScore factor.
- Jobs feed: optional read-only ERC-8183 job registry observation.
- Payment scanner: optional pull-based scanning of ERC-20 Transfer logs to credited payments without success outcomes.
- Checker mode: independent recomputation and comparison against pending on-chain proposals; finalizes an agreed score, or proposes into an empty slot, only once the primary is
CHECKER_GRACE_MINUTESlate. - Fee gating: optional gating where only agents with active epoch fee coverage are scored, and uncovered agents are skipped.
Section sources
- oracle/index.js:79-283
- oracle/index.js:293-340
- oracle/index.js:604-665
- oracle/index.js:1004-1189
- oracle/index.js:1191-1245
- oracle/index.js:1391-1536
The oracle integrates multiple signal sources and writes computed scores to the protocol’s reputation contract. Production deployments typically place a reverse proxy in front of the service to expose only the intended public surface while keeping write and administrative endpoints loopback-only.
sequenceDiagram
participant Client as "Client"
participant Proxy as "Reverse Proxy"
participant Oracle as "Oracle Service"
participant Chain as "Blockchain RPC"
participant Reputation as "SigvaraReputation"
Client->>Proxy : GET /health
Proxy->>Oracle : GET /health
Oracle-->>Proxy : 200/503 with operational signals
Proxy-->>Client : Health response
Client->>Proxy : POST /attest
Proxy->>Oracle : POST /attest
Oracle->>Chain : Verify payment / read agent address
Chain-->>Oracle : Transaction receipt / agent info
Oracle->>Store : Record payment / cooldown
Oracle-->>Proxy : Accepted or error
Proxy-->>Client : Attestation result
Diagram sources
The oracle validates required variables at startup and exits if they are missing. Recommended production settings include admin tokens, persistent state paths, and attestation cooldowns. Optional settings control epoch cadence, network binding, block scanning, fee gating, external trust feeds, checker mode, divergence tolerance, and payment verification behavior. Every numeric setting is also checked before anything starts: a value that is set and is not a plain number in range stops the process with the setting named, and a blank value means unset.
| Variable | Required / Default | Purpose |
|---|---|---|
RPC_URL |
Required | Ethereum JSON-RPC endpoint for the target chain |
ORACLE_PRIVATE_KEY |
Required unless ORACLE_PRIVATE_KEY_FILE is set |
Private key for the oracle wallet |
ORACLE_PRIVATE_KEY_FILE |
Empty | Path to a file holding that key, in place of ORACLE_PRIVATE_KEY (setting both stops the process). A file secret keeps the key out of docker inspect and the container's environment, and a malformed key is refused with a message that never contains it |
IDENTITY_ADDRESS |
Required | SigvaraIdentity contract address |
REPUTATION_ADDRESS |
Required | SigvaraReputation contract address |
EXPECTED_CHAIN_ID |
Recommended | The chain this oracle is for: 5042 Arc mainnet, 5042002 testnet. With it set, nothing is read for scoring or signed while the RPC reports another chain, and a state file saved on another chain is refused. Unset, the oracle signs for whatever chain RPC_URL serves and warns at startup |
EPOCH_HOURS |
Default 24
|
Hours between automatic epoch runs |
DIVERGENCE_TOLERANCE |
Checker default 3
|
Numeric tolerance used when comparing checker vs pending scores. In checker mode a non-number, a negative value or one above 10 stops the process; a primary ignores it |
HOST |
Default 127.0.0.1
|
HTTP bind address |
PORT |
Default 3030
|
HTTP port |
TRUSTED_PROXIES |
Empty | Addresses or CIDR ranges whose X-Forwarded-For is believed besides loopback, for example the proxy's Docker network. Without it, behind a proxy, every visitor shares one rate-limit bucket |
ORACLE_SCOPES_<NAME> |
Unrestricted | Limits the token ORACLE_TOKEN_<NAME> to some of attest, flag, link and epoch
|
TX_WAIT_SECONDS |
Default 60
|
How long a transaction is waited for before it is replaced once, at the same nonce and twice the fee (5 to 3600) |
MAX_FEE_GWEI |
Default 1000
|
Highest maxFeePerGas the oracle will sign |
MAX_TX_COST |
Default 1
|
Most one transaction may cost at its worst, in the native token |
MAX_CLOCK_SKEW_SECONDS |
Default 600
|
How far the chain's latest block time may be from the host's clock before the epoch is not run |
FROM_BLOCK |
Default 0
|
Block to start scanning AgentRegistered events from |
LOG_CHUNK_SIZE |
Default 2000
|
Max blocks per eth_getLogs call |
FEE_REGISTRY_ADDRESS |
Empty | Enables fee-gated scoring when set and on-chain fee is non-zero |
EXTERNAL_RPC |
Empty | RPC for ERC-8004 external score lookups |
EXTERNAL_IDENTITY_ADDRESS |
Empty | ERC-8004 Identity contract |
EXTERNAL_REPUTATION_ADDRESS |
Empty | ERC-8004 Reputation contract |
EXTERNAL_PUBLIC_FUNDERS |
Empty | Wallets that fund everyone, such as a faucet or an exchange, comma-separated; they end a reviewer's funding trace. Both operators must list the same ones |
EXTERNAL_LINK_FROM_BLOCK |
Empty | The block to read links made on chain from (the ERC-8004 identity's sigvara metadata key), only when the registry is on the chain the oracle scores (index.js:117-121, index.js:526-530). Both operators must use the same block. While it is set, a registry whose chain cannot be read stops all scoring |
ORACLE_MODE |
Not checker by default | Set to checker for the second bonded operator |
CHECKER_GRACE_MINUTES |
Checker default twice EPOCH_HOURS
|
How late the primary must be before a checker finalizes an agreed score or fills an empty slot. Refused when not a number or shorter than the checker's own epoch; set it above the primary's epoch |
ORACLE_ADMIN_TOKEN |
Empty | Bearer token for authenticated endpoints. A non-loopback bind with no write credential at all is refused at startup |
ORACLE_STATE_PATH |
Default /data/oracle-state.json
|
Path to persistent state file |
ATTEST_COOLDOWN_MS |
Default 3600000
|
Minimum time before same attester can re-attest same agent |
PAYMENT_VERIFICATION |
Default off
|
off or required; mainnet should use required
|
PAYMENT_ASSET |
Empty when verification off | ERC-20 asset used for verified payments |
PAYMENT_MIN_AMOUNT |
Default 0
|
Smallest payment that counts, per sender, in the asset's base units. Must be above 0 when verification is required, or the process refuses to start |
PAYMENT_MIN_CONFIRMATIONS |
Default 1
|
Confirmations before settlement is accepted |
PAYMENT_FEE_UNIT |
Default 100000000
|
Base units of volume per point of fee score |
PAYMENT_HALF_LIFE_DAYS |
Default 90
|
Decay half-life for payment evidence |
PAYMENT_MAX_PER_PAYER |
Default 4
|
Cap on points per payer |
PAYMENT_TRUST_WEIGHT |
Default 1
|
Counterparty standing weight |
PAYER_OUTCOMES |
Default off
|
Payer-signed outcomes: off, positive (only "delivered") or both. Needs PAYMENT_VERIFICATION=required, EXPECTED_CHAIN_ID and REPUTATION_ADDRESS. Set it the same on every operator |
PAYER_OUTCOMES_PUBLISH |
Default off
|
on serves each payer's signed payload in /evidence and lists them at GET /outcomes. Stays off on mainnet until the retention policy covers copies held by other operators |
PAYER_OUTCOMES_PEERS |
Empty | Base URLs of the other operators to import payer-signed outcomes from at the start of each epoch. Off on mainnet for the same reason |
PAYER_OUTCOMES_IMPORT_GRACE_HOURS |
Default 6
|
How long after an outcome's deadline an import from a peer may still arrive |
PAYER_OUTCOME_WINDOW_DAYS |
Default 14
|
How long after the payment a payer-signed outcome may replace a token holder's |
PAYER_NEGATIVE_MIN_AMOUNT |
Default PAYMENT_MIN_AMOUNT
|
Smallest payment leg whose payer's "not delivered" counts |
PAYMENT_SCAN_ENABLED |
Default 0
|
Enable pull-based payment scanning |
PAYMENT_SCAN_FROM_BLOCK |
Default FROM_BLOCK
|
First block for scan catch-up |
PAYMENT_SCAN_CHUNK |
Default LOG_CHUNK_SIZE
|
Blocks per scan request |
PAYMENT_SCAN_CONFIRMATIONS |
Default 6
|
Safety margin behind head for scans |
PAYMENT_SCAN_PACE_MS |
Default 500
|
Milliseconds between scan requests; 0 opts out |
PAYMENT_CHAIN_IDS |
Unset | Chains to read payments from beside the scored one (ADR 0009), comma-separated ids. Off by default: unset, no chain is read (payment-chains.js:37-39). Must not name the scored chain, and needs PAYMENT_VERIFICATION=required, PAYMENT_SCAN_ENABLED=1 and EXPECTED_CHAIN_ID, or the process refuses to start (payment-chains.js:140-142, payment-chains.js:171-175, index.js:182-191) |
PAYMENT_CHAIN_<id>_RPC_URL |
Required per chain | The chain's RPC, https, or http only to this host or a private network. Never logged or served. It must answer eth_getCode at a payment's own block, and a read that takes more than 30 seconds fails. Bring your own: Base Sepolia's public endpoint limits eth_getLogs to 200 blocks (payment-chains.js:99-114, README.md:1117) |
PAYMENT_CHAIN_<id>_ASSET |
Required per chain | The ERC-20 payments settle in (payment-chains.js:148-149) |
PAYMENT_CHAIN_<id>_FROM_BLOCK |
Required per chain | First block. Both operators use the same one (README.md:1119) |
PAYMENT_CHAIN_<id>_FEE_UNIT |
Required per chain | Base units of this asset per point. Amounts are converted into the scored chain's fee units before they are scored (payment-chains.js:323-337) |
PAYMENT_CHAIN_<id>_MIN_AMOUNT |
Required per chain, above 0
|
As PAYMENT_MIN_AMOUNT, in this asset's base units (payment-chains.js:158-159) |
PAYMENT_CHAIN_<id>_MODE |
Default shadow
|
shadow scans beside the epoch, reports what it would credit and credits nothing. on is scanned in the epoch, before scoring, and credits. A shadow scan keeps its own cursor, so switching on starts at FROM_BLOCK (payment-chains.js:150, payment-chains.js:201-204, README.md:1121) |
PAYMENT_CHAIN_<id>_DELAY_SECONDS |
Default 3600, at least 1800
|
How old a payment must be before it enters a score. Must be longer than the chain's finality lag, or every epoch is held (payment-chains.js:46-49, README.md:1122) |
PAYMENT_CHAIN_<id>_CHUNK, _PACE_MS, _MAX_BLOCKS
|
Defaults 200, 500, 36000
|
Blocks per getLogs call, milliseconds between calls, and blocks one scan covers at most (payment-chains.js:50-56) |
JOBS_REGISTRY_ADDRESS |
Unset | Enables read-only ERC-8183 job observation |
JOBS_RPC |
Default RPC_URL
|
Separate RPC for jobs registry |
JOBS_FROM_BLOCK |
Default 0
|
Earliest block for jobs scan |
JOBS_MAX_SPAN |
Default 50000
|
Widest range one jobs request may scan |
Payment chains on the testnet operators on 4 November 2026: Base Sepolia ran on neither. The primary read none, and the checker read Elysium testnet (99801) in shadow, which credits nothing and changes no score (0009-payments-on-base.md:236-237, operations-runbook.md:561-563). Base mainnet is decided at the mainnet launch (0009-payments-on-base.md:230). oracle/.env.example holds both chains' settings, commented out (.env.example:325-353). Both operators need the same values before a chain goes on, and switching on or back has an order (README.md:1164-1173). With a chain on, TX_WAIT_SECONDS must leave room for a charge and a proposal, or the process refuses to start (index.js:192-198).
Checker-specific constraints:
- Checker mode refuses to run with
FEE_REGISTRY_ADDRESSset because a checker does not bill: a registry from before the once-per-epoch charge would debit the agent a second time for one scoring epoch, and a current one would refuse the second charge while the two operators raced for the first. - Checker epoch cadence should be below half the on-chain challenge window so proposals are observed before expiring.
-
DIVERGENCE_TOLERANCEis validated at startup; an unparseable value would make comparisons silently agree. -
ORACLE_MODEmust be unset,primaryorcheckerexactly; any other value,Checkerincluded, stops the process.
Section sources
Use the development compose file to build the oracle image locally and mount a persistent state volume. The HTTP port is bound to localhost only, which requires a reverse proxy for public exposure.
Key behaviors:
- Builds from
./oracle/Dockerfile. - Uses
env_file: ./oracle/.env. - Publishes
127.0.0.1:3030:3030. - Mounts
oracle_state:/data. - Joins a shared external network named
sigvara-mesh.
flowchart TD
Start(["docker compose -f docker-compose.oracle.yml up"]) --> Build["Build oracle image"]
Build --> Run["Run container sigvara-oracle"]
Run --> Bind["Bind 127.0.0.1:3030"]
Run --> Volume["Mount /data for persistent state"]
Run --> Network["Join sigvara-mesh network"]
Bind --> Proxy["Reverse proxy forwards public endpoints"]
Volume --> Store["State survives restarts"]
Diagram sources
Section sources
Production uses a compose file that fetches one pinned commit of the repository inside the container, copies only the oracle source files, installs dependencies with npm ci, and runs node index.js. This ensures the running artifact matches a reviewed commit rather than a host-side checkout, and a restart runs the same commit again.
Key behaviors:
- Uses
node:24-alpine, pinned by digest. - Fetches the one commit named by
SIGVARA_COMMITin the environment file into/srcand refuses to start unless the checkout equals it./healthreports that commit. - Copies
oracle/*.js,package.json, andpackage-lock.jsoninto/app. - Removes test files.
- Runs
npm ci --omit=dev --no-audit --no-fund. - Exposes
127.0.0.1:3030:3030. - Mounts
oracle_state:/dataandoracle_app:/app. - Shares an external network named
sigvara-web.
flowchart TD
Start(["docker compose up"]) --> Clone["Clone Sigvara main branch"]
Clone --> Copy["Copy oracle source and lockfiles"]
Copy --> Install["Install dependencies with npm ci"]
Install --> Run["Exec node index.js"]
Run --> Health["Expose /health and /metrics via proxy"]
Run --> State["Persist state to oracle_state volume"]
Diagram sources
Section sources
docker-compose.mainnet.yml runs the primary, the checker and the watcher from one file. A host runs exactly one of them, chosen by COMPOSE_PROFILES in its .env (oracle, checker or watcher), and the three belong on three separate hosts. oracle/.env.mainnet.example and its checker and watcher siblings are the starting points; one role's file is not copied to another.
Key behaviors:
- Builds from
SIGVARA_REF, a full 40-character commit hash, not frommain. A deploy is a change to that line anddocker compose up -d; a restart reruns the same image and fetches nothing. The primary and the checker get the same commit unless a change is being rolled out one operator at a time, because differing scoring code shows up as divergence. - The build has no key in its environment, and nothing in the running container runs git, a package manager or a download at start.
- The signing key is a file secret (
secrets/oracle_key, owner uid 1000, mode 0400) read throughORACLE_PRIVATE_KEY_FILE. The checker uses a different key from the primary. - Runs as an unprivileged user on a read-only root filesystem with every capability dropped, a limit of 256 processes, 768 MB of memory and one CPU. The only writable places are
/dataand/tmp. -
HOST,PORT,ORACLE_STATE_PATHandORACLE_PRIVATE_KEY_FILEare fixed in the compose file and override.env. - Publishes
127.0.0.1:${HOST_PORT}(3030 for the primary, 3031 for the checker). Only the primary joins the external edge network (EDGE_NETWORK, defaultsigvara-edge) that it shares with the reverse proxy. - The watcher has no key, no volume and no inbound port, and its image health check is switched off.
- The mainnet examples set
EXPECTED_CHAIN_ID=5042,PAYMENT_VERIFICATION=required,PAYMENT_SCAN_ENABLED=1and aTRUSTED_PROXIESto fill in, and leave the epoch-fee registry, the ERC-8004 feed and the jobs reader unset at launch. They also leave payer-signed outcomes off (PAYER_OUTCOMES=off, with publishing and peers off until the retention policy covers copies held elsewhere). The scoring settings must be identical on the primary and the checker.
Section sources
- docker-compose.mainnet.yml
- oracle/.env.mainnet.example
- oracle/.env.mainnet.checker.example
- oracle/.env.mainnet.watcher.example
The checker is a second bonded operator that does not race the primary. It recomputes scores and records divergences, and it finalizes an agreed score, or proposes into an empty slot, only once the primary is CHECKER_GRACE_MINUTES late (default twice EPOCH_HOURS, two hours on testnet). Until that rule went live on both operators on 3 November 2026, the checker sent 35 of the first 262 proposals, each within about an hour after a finalization had emptied the slot, 33 of them the checker's own, and those had no second computation (epoch-policy.js:147-221, divergence-log.md:333-392).
Development compose:
- Builds from local source.
- Uses
./oracle/.env.checker. - Publishes
127.0.0.1:3031:3030. - Mounts
checker_state:/data.
Production compose:
- Fetches the pinned commit inside the container.
- Uses
npm ci. - Exposes
127.0.0.1:3031:3030. - Mounts
checker_state:/dataandchecker_app:/app.
Important constraints:
- Do not share the primary’s state volume.
- Do not set
FEE_REGISTRY_ADDRESSin checker mode. - Use a different RPC provider and hardware from the primary.
- Ensure the checker wallet is bonded and admitted, and has
ORACLE_ROLE. - On
mainthe bond can belong to a cold key that names the checker's wallet withsetSigner, so the host's key never holds it. The Arc testnet contracts have supported this since 26 October 2026, but both operators there still sign with the key that holds the bond.
flowchart TD
Start(["Deploy checker"]) --> Env["Set ORACLE_MODE=checker"]
Env --> Bond["Bond wallet and admit operator"]
Bond --> Role["Grant ORACLE_ROLE"]
Role --> NoFee["Unset FEE_REGISTRY_ADDRESS"]
NoFee --> Cadence["Set EPOCH_HOURS below half challenge window"]
Cadence --> Compare["Compare scores with tolerance"]
Compare --> Action{"Decision"}
Action --> |Agree| Finalize["Finalize once the window has passed by CHECKER_GRACE_MINUTES"]
Action --> |Disagree| Record["Record divergence"]
Action --> |Skip| Wait["Wait for next epoch"]
Diagram sources
Section sources
The example Caddy configuration publishes only the intended public endpoints: /health, /score/*, /evidence/*, /badge/*, /outcomes, and POST /attest. All other paths return 404. Write endpoints such as /flag, /flag/resolve, /link, and /epoch remain loopback-only.
Security-relevant proxy behaviors:
-
/attestis published but not opened: positive attestations carrying verified payments need no token, and record no outcome without one; an outcome the payer signed needs no token either; negative attestations require a bearer token. - Read endpoints are rate-limited by the oracle using socket address or
X-Forwarded-For, which it believes only from loopback or from a peer inTRUSTED_PROXIES. Behind a proxy in a container, setTRUSTED_PROXIESto the proxy network or every visitor shares one bucket. - CORS is restricted to the site origin for browser access.
- Cache headers protect freshness without caching writes.
- Security headers include HSTS, content-type sniffing prevention, and referrer policy.
flowchart TD
Public["Public Domain"] --> Caddy["Caddy Reverse Proxy"]
Caddy --> Health["GET /health"]
Caddy --> Score["GET /score/*"]
Caddy --> Evidence["GET /evidence/*"]
Caddy --> Badge["GET /badge/*"]
Caddy --> Outcomes["GET /outcomes"]
Caddy --> Attest["POST /attest"]
Caddy --> Deny["404 for everything else"]
Admin["Loopback Only"] --> Flag["POST /flag"]
Admin --> Resolve["POST /flag/resolve"]
Admin --> Link["POST /link"]
Admin --> Epoch["POST /epoch"]
Diagram sources
Section sources
GET /health returns operational signals suitable for production alerting. It returns 200 when healthy and 503 when critical conditions exist, such as unwritable state path, failed state persistence (while it lasts /attest answers 202 with durable: false instead of 200), or stale scoring. Scoring is stale after two epoch intervals with no successful epoch; before the first success the two intervals are counted from process start, so a primary that has never completed an epoch does not stay green.
Important fields:
-
ok: overall health. -
epochMs: configured epoch interval. -
uptimeSeconds: process uptime. -
lastSuccessfulEpochMsandtimeSinceLastEpochMs: scoring recency. -
storeWritableandstatePersisted: storage health. -
stateSource:file,fresh(there was no state file) orbackup(the state file could not be read and the previous save was used). -
chainIdandexpectedChainId: the chain the RPC was last seen on, and the oneEXPECTED_CHAIN_IDnames (nullwhen unset). -
commit: reported git revision of the cloned or built code, ornullwhen the deployment does not report one. It catches a box that missed a deploy; it is not a trust anchor. -
externalFeed: status of ERC-8004 external feed. -
paymentScanandpaymentScanBlock: status of pull-based payment scanning, and how far it has got. -
paymentChainsandpaymentChainHold: only with a payment chain configured. Each chain's mode, asset,fromBlock, fee unit, minimum, delay, cursor and last scan, so two operators can compare what they must share, and what held back the last epoch, ornull(index.js:2454-2460, payment-chains.js:362-380). -
jobsFeed: status of ERC-8183 job feed.
Section sources
GET /metrics exposes Prometheus-format metrics. Operators should scrape this endpoint and monitor:
- Epoch lifecycle counters.
- Propose and finalize attempts and successes.
- Attestation results.
- Flag operations.
- Rate limits.
- Checker comparisons and divergences.
- Proposal rejection count.
- Evidence cache hit rates.
- Payment scan canary failures.
- Payment chain scans, the ones that could not complete, and epochs held back by a scan that was behind (
sigvara_oracle_payment_chain_scans_total,sigvara_oracle_payment_chain_scan_errors_total,sigvara_oracle_epochs_held_by_payment_chain_total, metrics.js:350-363). - Transactions refused for their fee or sent and never seen mined (
sigvara_oracle_tx_refused_total). - Epoch-fee charges, including the ones skipped because the contract would have refused the proposal.
- Pending proposals stranded behind a proposer that lost its authority.
- The import of payer-signed outcomes from other operators: imported, refused, stopped early and conflicting (
sigvara_oracle_payer_outcomes_imported_total,sigvara_oracle_payer_outcome_import_refused_total,sigvara_oracle_payer_outcome_import_errors_total,sigvara_oracle_payer_outcome_conflicts_total).
Recommended alerts:
-
/healthreturning 503. - Epoch failure counter increasing.
- Attestation cooldown abuse spikes.
- Payment scan canary failures.
-
sigvara_oracle_payment_chain_scan_errors_totalrising, or any run ofsigvara_oracle_epochs_held_by_payment_chain_total: that chain's RPC, or the scored chain's, needs a look (index.js:1149-1152, metrics.js:71-72). - Checker divergence activity.
-
sigvara_oracle_tx_refused_totalrising, andsigvara_oracle_fee_charges_total{result="skipped"}staying high. -
stateSourceon/healthreading anything butfileafter a restart.
Section sources
When FEE_REGISTRY_ADDRESS is set and the on-chain epoch fee is non-zero, the oracle gates scoring on fee coverage. Agents without coverage are skipped during the epoch run. In checker mode, FEE_REGISTRY_ADDRESS must be unset: a checker does not bill, and a second operator charging would either debit the agent twice on a registry from before the once-per-epoch charge or race the primary for the single charge a current one allows. SigvaraEpochFees has never been deployed and is left out of the mainnet launch, so the mainnet examples leave this unset.
Operational implications:
- Gating is checked once per epoch.
- Covered agents proceed to scoring.
- Uncovered agents are logged and skipped.
- Before charging, the primary asks the contract whether the proposal would be accepted. If the answer is no, or it cannot be asked, the agent is skipped for the epoch and not charged.
- An agent the registry has already charged in the same epoch is scored without a second charge.
- Checker mode enforces no fee registry to prevent duplicate charges.
Section sources
External trust comes from ERC-8004 identity and reputation registries. When EXTERNAL_RPC, EXTERNAL_IDENTITY_ADDRESS, and EXTERNAL_REPUTATION_ADDRESS are all set, linked agents receive an externalScore derived from recognized feedback tags. If any of these are unset, externalScore remains zero.
Health reporting distinguishes three cases:
-
externalFeed: disabledwhen not configured. -
externalFeed: configuredwhen reachable. -
externalFeed: unreachablewhen configured but RPC cannot be reached. -
externalChainIdreports the actual chain ID reached by the provider.
Section sources
Checker mode provides independent recomputation and divergence detection. It:
- Recomputes every agent’s score.
- Compares its result against the pending on-chain proposal, scored at the proposal's
proposedAtover the evidence as it stood then. - Records divergences when disagreement exceeds tolerance.
- Finalizes an agreed score, or proposes into an empty slot through
proposeIfEmpty, only once the primary isCHECKER_GRACE_MINUTESlate: the window past by that long, or the slot empty that long, counted from the latest of the last finalized score, registration and the first epoch it found the slot empty. A pending proposal whose proposer has lost its authority counts as empty and is covered at once, on the Arc testnet since 26 October 2026 (the September contract reverted for any pending proposal). - Counts what it leaves to the primary in
sigvara_oracle_checker_deferred_total{action}. - Never finalizes a disputed score.
- Never overwrites a disputed proposal.
Startup validation:
- Rejects invalid
DIVERGENCE_TOLERANCE. - Refuses checker mode with
FEE_REGISTRY_ADDRESS. - Refuses a
CHECKER_GRACE_MINUTESthat is not a number or is shorter than the checker's own epoch. - Warns if checker epoch cadence is too slow relative to the challenge window.
Divergence endpoints:
-
GET /divergence: list recent divergences. -
GET /divergence/:didHash: divergence for a specific agent. - Returns 404 on a primary because absence means “this oracle does not check,” not “checked and found nothing.”
Section sources
- Node.js runtime compatible with the oracle package.
- Access to a blockchain RPC endpoint.
- An oracle wallet with
ORACLE_ROLEonSigvaraReputation. - For production, a reverse proxy terminating TLS.
- For checker mode, a separate wallet, hardware, RPC provider, and state volume.
- For fee gating, a deployed
SigvaraEpochFeescontract and funded agents. - For external trust, ERC-8004 registry addresses and a reachable RPC.
- Copy
.env.exampleto.envin theoracledirectory. - Configure
RPC_URL,ORACLE_PRIVATE_KEY,IDENTITY_ADDRESS, andREPUTATION_ADDRESS. - Install dependencies with
npm install. - Run the oracle with
node index.js. - Run tests with
node --test. - Deploy with
docker compose -f docker-compose.oracle.yml up -d.
Section sources
- Prepare a bare VPS with Docker.
- Place
docker-compose.oracle.vps.ymland.envin/docker/sigvara-oracle. - Configure all required and recommended environment variables, including
EXPECTED_CHAIN_IDand, behind a reverse proxy,TRUSTED_PROXIES. - Mount a persistent volume for
ORACLE_STATE_PATH. - Set
ORACLE_ADMIN_TOKENbefore exposing the service. - Fund the oracle wallet with native gas.
- Grant
ORACLE_ROLEonSigvaraReputation. - Set
PAYMENT_VERIFICATION=requiredandPAYMENT_ASSETfor mainnet-like trust. - Bond the oracle wallet in
SigvaraOracleBondifoperatorBondis set. - Configure a reverse proxy using the example Caddy configuration.
- Scrape
/metricswith Prometheus and alert on/health. - Copy the state volume off the host every night and run the restore drill once a quarter (operations-runbook.md:49-174).
Section sources
- Create a separate wallet and fund it with SVR bond.
- Deposit bond on
SigvaraOracleBond. - Admit the checker wallet as an operator.
- Grant
ORACLE_ROLEonSigvaraReputation. - Set
ORACLE_MODE=checker. - Set a separate
ORACLE_PRIVATE_KEY. - Leave
FEE_REGISTRY_ADDRESSunset. - Set
EPOCH_HOURSbelow half the challenge window. - Use a different RPC provider and host.
- Mount a separate state volume.
- Set
PAYER_OUTCOMES,PAYER_OUTCOMES_PUBLISHandPAYER_OUTCOMES_PEERSas the primary does, each operator naming the other inPAYER_OUTCOMES_PEERS. - Deploy with
docker compose -f docker-compose.checker.yml up -dor the VPS compose file. - Monitor
/divergenceand configure a watcher if needed.
Section sources
-
ORACLE_PRIVATE_KEYcontrols on-chain writes and must be kept secret. - The key is loaded from
ORACLE_PRIVATE_KEY_FILE(preferred in production: a mounted file secret stays out ofdocker inspect,/proc/<pid>/environand child processes) or fromORACLE_PRIVATE_KEY, and used by the chain layer for propose/finalize transactions. - The chain layer will not sign a transaction whose fee is above
MAX_FEE_GWEIor whose worst-case cost is aboveMAX_TX_COST. - Do not log private keys.
- Rotate keys through the protocol’s identity rotation process when applicable.
- Keep checker and primary wallets separate.
- On
mainan operator can keep its bond on a cold key and name the oracle's signing key withsetSigner.clearSigner()revokes that key in one transaction: it can no longer propose, and what it proposed cannot be finalized. The Arc testnet contracts have had the signer registry since 26 October 2026, and neither operator uses it yet.
Section sources
- Bind the oracle to
127.0.0.1unless explicitly exposed through a reverse proxy. - Use a reverse proxy to terminate TLS and restrict public endpoints.
- Ensure
X-Forwarded-Foris handled correctly so rate limiting works behind proxies: the oracle reads it from the right and believes it only from loopback or a peer inTRUSTED_PROXIES. - Keep write endpoints like
/flag,/flag/resolve,/link, and/epochloopback-only. - Exclude
/metricsfrom public proxy exposure if it reveals operator-specific telemetry.
Section sources
- Use distinct bearer tokens for different integrations when configuring admin credentials.
- Revoking a token means deleting its variable from
.envand recreating the container withdocker compose up -d. A plainrestartkeeps the old environment, so the token keeps working. - Positive attestations with verified payments do not require a token (without one, the payment is recorded and no outcome), and neither does an outcome the payer signed; negative attestations do.
- Avoid sharing a single admin token across services when revocation granularity matters.
- Restrict an integration's token to the writes it needs with
ORACLE_SCOPES_<NAME>; a token shorter than 32 characters is named in a startup warning.
Section sources
The oracle depends on:
- Node.js runtime version specified in the package manifest.
-
dotenvfor environment loading. -
ethersfor blockchain interaction. - Blockchain RPC endpoints for reading and writing protocol state.
- Mounted volumes for persistent state.
- Optional external RPC for ERC-8004 feedback.
- Optional ERC-8183 job registry RPC.
- Reverse proxy for TLS and public endpoint routing.
- Prometheus or compatible tools for metrics scraping.
graph TB
Package["@sigvara/oracle"]
Runtime["Node.js >= 18"]
DotEnv["dotenv"]
Ethers["ethers"]
RPC["Blockchain RPC"]
ExternalRPC["External RPC"]
JobsRPC["Jobs RPC"]
Volume["Persistent Volume"]
Proxy["Reverse Proxy"]
Metrics["Prometheus"]
Package --> Runtime
Package --> DotEnv
Package --> Ethers
Ethers --> RPC
Package --> ExternalRPC
Package --> JobsRPC
Package --> Volume
Proxy --> Package
Metrics --> Package
Diagram sources
Section sources
- Epoch reads are chunked to avoid releasing thousands of concurrent RPC calls at once.
- ethers batches nearby calls into fewer JSON-RPC requests.
- Payer score resolution is cached per epoch to reduce repeated chain reads.
- Payment scanning uses backoff and pacing to respect RPC rate limits.
- Evidence responses are cached to reduce expensive Merkle tree rebuilds.
- Checker mode recomputation avoids unnecessary writes and focuses on comparison.
- Health checks distinguish writable probe from actual persisted writes because large state files can fail after small probes pass.
- The state file is written in pieces of about 1 MB, so the memory peak of a save is the live state plus one piece. Size the container for that peak, not for the size of the file (the mainnet compose file limits it to 768 MB).
Check:
- Whether the state path is writable.
- Whether the last state write succeeded.
- Whether the last successful epoch occurred within two times the epoch interval (counted from process start if none has completed).
- Whether epochs are being refused: an epoch does not run while the RPC reports a chain other than
EXPECTED_CHAIN_ID, or a clock more thanMAX_CLOCK_SKEW_SECONDSfrom the host's. - Whether the commit reported by
/healthmatches the expected deployment.
Section sources
Common statuses:
- 200: accepted and saved.
- 202: accepted, but the oracle could not write its state file (
durable: false). Do not resend;/healthis 503 until a save works. - 400: malformed body, unknown agent, or a
successthat is not a boolean. - 401: no token, and the attestation is neither a positive one backed by a verified payment nor a payer-signed outcome.
- 402: payment did not check out, including one made before the agent registered (
before_registration, README.md:354). - 403: the token is restricted to other scopes.
- 409: settlement already credited, or its outcome already recorded. For a payer-signed outcome the body carries
sameOutcome, and foroutcome_recordedthe held outcome'sheldOutcomeHashwhen the payer signed the outcome held. - 429: cooldown active.
- 502: RPC failure; retry.
Verify:
-
PAYMENT_VERIFICATIONsetting. -
PAYMENT_ASSETwhen verification is required. - Transaction hash validity.
- Payer is not the agent or operator.
- Confirmation requirements.
Section sources
Investigate:
-
DIVERGENCE_TOLERANCEparsing and value. - Whether the checker epoch cadence is shorter than the challenge window.
- Whether both operators have identical payment evidence and the same payer-signed outcomes (
node scripts/check-live.mjs outcomes), and whether the record'sevidenceMatchesProposalis true. - Whether the proposal is still within the challenge window.
- Whether the checker is configured without
FEE_REGISTRY_ADDRESS.
Use:
-
GET /divergenceandGET /divergence/:didHash. - Compare evidence roots and payment events.
- Review
/healthfields for external feed and payment scan status.
Section sources
Check:
-
PAYMENT_SCAN_ENABLED. -
PAYMENT_ASSET. -
PAYMENT_SCAN_CONFIRMATIONS. -
PAYMENT_SCAN_PACE_MS. - Canary verdict: passed, failed, or unavailable.
- Whether both operators enable or disable scanning consistently.
- For a payment chain,
paymentChainson/health, and thatlastBlockTsmoves between epochs.canary: "unavailable"is what a shadow scan with nothing to credit reports. On Elysium,could not coalesce error (rpc -32005)means one call would return more than 10,000 logs, and the scan keeps failing untilCHUNKis lowered (operations-runbook.md:581-590).
Section sources
The Sigvara oracle is a production-grade reputation computation service that integrates blockchain data, optional external trust feeds, and optional job registry observations. Correct deployment requires careful environment configuration, persistent state management, secure network exposure, and robust monitoring. Checker mode adds independent verification and divergence visibility, while fee gating and payment verification strengthen economic security. Operators should treat private keys, RPC endpoints, and admin tokens as sensitive assets and validate their deployment against the health and metrics surfaces before trusting the service in production.
- Blockchain RPC endpoint for the target chain.
- Oracle wallet with
ORACLE_ROLE. - Persistent volume for oracle state.
- Reverse proxy with TLS for public endpoints.
- Prometheus or compatible metrics scraper.
- Optional: ERC-8004 RPC and registry addresses.
- Optional: ERC-8183 job registry address and RPC.
- Optional:
SigvaraEpochFeescontract for fee gating.
Section sources
- 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