Operators sign EIP‑712 price submissions off‑chain and relay them on‑chain. This guide covers responsibilities, code snippets, and best practices.
Quick Start: For local testing, use the provided operator bot at scripts/bot/operators-bot.mjs. For production, follow the detailed setup below.
- Secure key management (HSM or well‑managed hot wallet).
- Data quality (multiple upstream sources, sanity checks, outlier filtering).
- Timeliness (submit per heartbeat, or when deviation threshold is crossed).
Type: PriceSubmission(bytes32 feedId,uint80 roundId,int256 answer,uint256 validUntil)
Domain: { name: "Price Loom", version: "1", chainId, verifyingContract }
import { ethers } from "ethers";
const oracle = new ethers.Contract(ORACLE_ADDRESS, [
"function nextRoundId(bytes32) view returns (uint80)",
"function dueToStart(bytes32,int256) view returns (bool)",
"function getConfig(bytes32) view returns (tuple(uint8,uint8,uint8,uint8,uint32,uint32,uint32,int256,int256,string))",
"function submitSigned(bytes32,(bytes32,uint80,int256,uint256),bytes)"
], provider);
const feedId = FEED_ID; // keccak256(abi.encodePacked("AR/byte"))
const proposed = 101n * 10n ** 8n; // example price at decimals=8
let round = await oracle.nextRoundId(feedId);
const allowOpen = await oracle.dueToStart(feedId, proposed);
// If no open round yet and not due to start, wait until heartbeat or sufficient deviation
const submission = {
feedId,
roundId: round,
answer: proposed,
validUntil: BigInt(Math.floor(Date.now()/1000) + 60),
};
const domain = {
name: "Price Loom",
version: "1",
chainId: (await provider.getNetwork()).chainId,
verifyingContract: ORACLE_ADDRESS,
};
const types = {
PriceSubmission: [
{ name: "feedId", type: "bytes32" },
{ name: "roundId", type: "uint80" },
{ name: "answer", type: "int256" },
{ name: "validUntil", type: "uint256" },
],
};
const sig = await signer.signTypedData(domain, types, submission);
const tx = await oracle.submitSigned(feedId, submission, sig);
await tx.wait();For local testing and as a reference implementation, see scripts/bot/operators-bot.mjs. This bot demonstrates:
- Sequential submission pattern (prevents race conditions)
- Automatic recovery via
poke()when rounds get stuck - Graceful error handling for all oracle error types
- Pause detection and automatic resume
- Round state tracking with
nextRoundId()queries
# Install dependencies
npm install
# Example 1: AR/Byte feed (18 decimals, ~1.5e-9 AR/byte)
node scripts/bot/operators-bot.mjs \
--rpc http://127.0.0.1:8545 \
--oracle 0x5FbDB2315678afecb367f032d93F642f64180aa3 \
--feedDesc "ar/bytes-testv1" \
--interval 30000 \
--priceBase 1.5e-9
# Example 2: AR/USD feed (8 decimals, ~$6 per AR)
node scripts/bot/operators-bot.mjs \
--rpc http://127.0.0.1:8545 \
--oracle 0x5FbDB2315678afecb367f032d93F642f64180aa3 \
--feedDesc "ar/usd-testv1" \
--interval 30000 \
--priceBase 6
# Or with environment variables
export RPC_URL=http://127.0.0.1:8545
export ORACLE=0x5FbDB...
export FEED_DESC="ar/bytes-testv1"
export INTERVAL_MS=30000
export PRICE_BASE=1.5e-9
node scripts/bot/operators-bot.mjsParameters:
--rpc: RPC endpoint URL--oracle: Oracle contract address--feedDesc: Feed description string (e.g., "ar/bytes-testv1", "ar/usd-testv1")--interval: Tick interval in milliseconds (default: 30000)--priceBase: Base price value - the bot reads decimals from the oracle config automatically- For AR/byte feeds: Use scientific notation (e.g.,
1.5e-9) - For AR/USD feeds: Use regular numbers (e.g.,
6) - The bot scales the price to the correct decimals automatically
- For AR/byte feeds: Use scientific notation (e.g.,
Note: The test bot uses default Anvil keys and generates random price variations around priceBase (±1%). For production, set PRIVATE_KEYS_JSON environment variable and implement real data source integration.
- Dynamic Operator Initialization: Automatically matches on-chain registered operators with available private keys
- Sequential Submissions: Avoids race conditions by submitting one operator at a time
- State Adaptation: Re-queries
nextRoundId()before each submission to detect round changes - Early Exit: Stops after quorum reached (minSubmissions)
- Recovery Logic: Calls
poke()after 2 consecutive failed ticks - Comprehensive Logging: Shows submission status, round progression, price age, and staleness
See operator-bot-fix-report.md for detailed architecture and troubleshooting.
- Always compute
roundIdvianextRoundId(feedId)and validate withdueToStartwhen opening a new round. - Use sequential submissions for deterministic behavior (see bot implementation).
- Keep
validUntilshort (e.g., 60–120s). - Keep your time source synced (NTP).
- Monitor
isStale(feedId, maxDelay)and alerts on missed heartbeats/deviations. - Rotate operators via admin flow as needed.
- MEV Protection: Consider using private mempools (e.g., Flashbots, MEV-Blocker) to prevent front-running of price submissions. While submissions are signed off-chain and can't be modified, they can be extracted and potentially manipulated by MEV bots in public mempools.
Expired(): Your signature'svalidUntiltimestamp has passed. Check that your system clock is synchronized.WrongRound(): TheroundIdyou signed for is not the current open round. This can happen if a new round started after you fetched thenextRoundIdbut before your transaction was mined.DuplicateSubmission(): Your operator address has already submitted a price for this round.NotOperator(): The signing key does not correspond to a registered operator for this feed.OutOfBounds(): The price you submitted is outside theminPrice/maxPriceconfigured for the feed.
While the script above is a functional example, a production-grade operator requires additional robustness and security.
Storing a plaintext private key in an environment variable is not secure for production. Use a dedicated key management solution:
- Hardware Security Module (HSM): For the highest level of security.
- Managed KMS: Cloud provider services like AWS KMS, Google Cloud KMS, or Azure Key Vault.
- Self-Hosted Vault: Tools like HashiCorp Vault.
Your application should request a signature from these systems without ever accessing the raw private key.
Do not rely on a single API for price data. Your service should:
- Fetch data from multiple, independent, and highly-reputable sources (e.g., Binance, Coinbase, Kraken, CoinGecko).
- Implement logic to validate and aggregate these prices, for example, by taking the median or a volume-weighted average.
- Include sanity checks to discard outlier sources that deviate significantly from the aggregate.
Your operator node is critical infrastructure. You must have monitoring in place to alert you to problems:
- Submission Success: Monitor your transactions to ensure they are being successfully mined. Alert on consecutive failures.
- Price Deviation: Alert if your sourced price deviates significantly from the current on-chain median. This could indicate an issue with your sources or a market event.
- Gas Tank: Monitor the balance of the address used for submitting transactions and alert when it runs low.
- System Health: Monitor the health (CPU, memory, etc.) of the server running your operator service.
- Scripts & Bots - Operator bot reference and commands
- Deployment Cookbook - Deploy oracle and configure feeds
- Maintenance Guide - Manage operators and feed configs
- Local Development Guide - Test operator workflows locally
- Oracle Design - Technical specification and architecture
- Operator Bot Fix Report - Detailed bot architecture and troubleshooting