Scenarios, commands, and copy‑paste snippets for common tasks.
# Start Anvil in separate terminal first
anvil
# Then deploy oracle + factory + feeds + adapters
export ADMIN=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
make anvil-bootstrap-allExpected Output:
✅ Oracle deployed: 0x5FbDB2315678afecb367f032d93F642f64180aa3
✅ Factory deployed: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
✅ Feed created: ar/bytes-testv1 (0x3f32666a...)
✅ Adapter deployed: 0xD9164F568A7d21189F61bd53502BdE277883A0A2
# Copy oracle address from output above
export ORACLE=0x5FbDB2315678afecb367f032d93F642f64180aa3
# For AR/byte feed (18 decimals, ~1.5e-9 AR/byte)
node scripts/bot/operators-bot.mjs \
--rpc http://127.0.0.1:8545 \
--oracle $ORACLE \
--feedDesc "ar/bytes-testv1" \
--interval 30000 \
--priceBase 1.5e-9
# Or for AR/USD feed (8 decimals, ~$6 per AR)
node scripts/bot/operators-bot.mjs \
--rpc http://127.0.0.1:8545 \
--oracle $ORACLE \
--feedDesc "ar/usd-testv1" \
--interval 30000 \
--priceBase 6Expected Output:
🚀 Operator bot starting
📊 Feed: AR/byte (decimals=18)
💰 Base price: 1.5e-9 → 1500000000 (scaled to 18 decimals)
✅ Initialized 6/6 valid operator wallets
📤 Starting new round 1 for ar/bytes-testv1
✍️ 0xf39F…2266 → 1485000000 ✅
✍️ 0x7099…79C8 → 1515000010 ✅
✍️ 0x3C44…93BC → 1492000020 ✅
✅ Quorum (3) reached—skipping remaining operators
🟢 latest round=1 answer=1492000020 age=1s changed=🔄
# Deploy test consumer (copy adapter address from step 1)
export ADAPTER=0xD9164F568A7d21189F61bd53502BdE277883A0A2
forge script script/DeployTestConsumer.s.sol:DeployTestConsumer \
--rpc-url http://127.0.0.1:8545 \
--broadcast \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# Run integration test (copy consumer address from output)
export CONSUMER=0x610178dA211FEF7D417bC0e6FeD39F05609AD788
node scripts/test-adapter-consumer.mjsExpected Output:
🧪 Testing Adapter & Consumer Integration
📊 Testing Oracle...
✅ Latest Round Data: Round ID: 2, Answer: 9992000030
🔌 Testing Adapter (Chainlink-compatible)...
✅ Adapter data matches Oracle
🛒 Testing Consumer Contract...
✅ Consumer data matches Oracle & Adapter
✅ ALL TESTS PASSED!
Feed IDs are bytes32 values computed as the keccak256 hash of the feed description string:
Solidity:
bytes32 feedId = keccak256(abi.encodePacked("AR/byte"));JavaScript/TypeScript (ethers v6):
import { ethers } from "ethers";
const feedId = ethers.id("AR/byte"); // id() === keccak256(toUtf8Bytes())Command line (cast):
export FEED_ID=$(cast keccak "AR/byte")Note: The feed description is case-sensitive. "AR/byte" and "ar/byte" produce different feed IDs.
For deterministic CREATE2 deployments, predict adapter addresses via factory:
cast call $FACTORY "computeAdapterAddress(bytes32)" $FEED_ID --rpc-url $RPC_URL# Set admin credentials
export ADMIN=0xYourAdminAddress
export ADMIN_PRIVATE_KEY=0xYourAdminPrivateKey
# Deploy everything (uses --slow and --gas-estimate-multiplier 300 for alphanet)
make alphanet-bootstrap-allNote: Alphanet deployment uses --slow flag to ensure transactions are processed sequentially and --gas-estimate-multiplier 300 to account for higher gas requirements on alphanet.
# Anvil uses default test account
make anvil-bootstrap-all FEEDS_FILE=feeds-anvil.jsonUse this step-by-step approach for production deployments to verify each stage.
# Deploy PriceLoomOracle with secure admin address (e.g., Multi-Sig)
export ADMIN=0xYourMultiSigAddress
forge create src/PriceLoomOracle.sol:PriceLoomOracle \
--rpc-url https://alphanet.load.network \
--constructor-args $ADMIN \
--private-key $DEPLOYER_PRIVATE_KEYExpected Output:
Deployer: 0x123...
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xabc...
# Copy oracle address from Step 1
export ORACLE=0x5FbDB2315678afecb367f032d93F642f64180aa3
make alphanet-deploy-factoryExpected Output:
✅ Factory deployed: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Bound to oracle: 0x5FbDB2315678afecb367f032d93F642f64180aa3
# Ensure feeds.json is configured with your feed parameters
export ORACLE=0x5FbDB2315678afecb367f032d93F642f64180aa3
make alphanet-create-feeds-json FEEDS_FILE=feeds.jsonExpected Output:
Creating feeds from feeds.json...
✅ Feed created: AR/byte (feedId: 0x3f32...)
✅ Feed created: ETH/USD (feedId: 0x7a21...)
✅ 2 feeds created successfully
# Copy factory address from Step 2
export FACTORY=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
make alphanet-deploy-adapters-json FEEDS_FILE=feeds.jsonExpected Output:
Deploying adapters from feeds.json...
✅ Adapter deployed: AR/byte → 0xD916...
✅ Adapter deployed: ETH/USD → 0x8A3C...
✅ 2 adapters deployed successfully
# Check oracle has feeds
cast call $ORACLE "getConfig(bytes32)" $(cast keccak "AR/byte") --rpc-url https://alphanet.load.network
# Check adapter points to correct oracle
export ADAPTER=0xD9164F568A7d21189F61bd53502BdE277883A0A2
cast call $ADAPTER "decimals()" --rpc-url https://alphanet.load.networkExpected Output:
# getConfig returns tuple with decimals, min/max submissions, etc.
# decimals() returns: 8 (or your configured decimals)
The decimals field determines price precision. Choose based on the value range:
| Feed Type | Price Range | Recommended Decimals | Example |
|---|---|---|---|
| AR/byte | ~1.5e-9 AR | 18 | 1.5e-9 AR → 1,500,000,000 (18 decimals) |
| AR/USD | ~$6 USD | 8 | $6 → 600,000,000 (8 decimals) |
| ETH/USD | ~$2000 USD | 8 | $2000 → 200,000,000,000 (8 decimals) |
| Stablecoins | ~$1 USD | 8 | $1 → 100,000,000 (8 decimals) |
Key Guidelines:
- Use 18 decimals for very small values (< 1e-6) to avoid rounding to zero
- Use 8 decimals for standard token/USD pairs (Chainlink convention)
- For rebalancing/liquidation protocols: Higher decimals = more precision for detecting small changes
- Decimals are immutable after feed creation (requires new feed to change)
{
"id": "ar/bytes-v1",
"decimals": 18,
"description": "AR per byte",
"minSubmissions": 3,
"maxSubmissions": 5,
"heartbeatSec": 3600,
"deviationBps": 50,
"timeoutSec": 7200,
"minPrice": "0",
"maxPrice": "10000000000000000000000"
}Calculation Example:
- Real price:
1.5e-9 AR/byte - With 18 decimals:
1.5e-9 * 1e18 = 1,500,000,000 - Consumer calculation:
totalCost = (bytes * price) / 1e18
# Set all feed parameters via environment variables
export ORACLE=0x5FbDB2315678afecb367f032d93F642f64180aa3
export FEED_DESC="AR/byte"
export DECIMALS=18 # Use 18 for AR/byte, 8 for AR/USD
export MIN_SUBMISSIONS=2
export MAX_SUBMISSIONS=3
export HEARTBEAT_SEC=3600
export DEVIATION_BPS=50
export TIMEOUT_SEC=900
export MIN_PRICE=0
export MAX_PRICE=10000000000000000000000
export DESCRIPTION="AR/byte price feed"
export OPERATORS_JSON='["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"]'
# Create feed
make alphanet-create-feed-envExpected Output:
Creating feed AR/byte...
feedId: 0x3f32666a3e43d4d82c6c5b5e89e2d0b8c8fb4c8a9c20b7b0d8c6e8f8a4b2c6d8
decimals: 18
operators: 3
✅ Feed created successfully
export ORACLE=0x5FbDB2315678afecb367f032d93F642f64180aa3
export RPC_URL=https://alphanet.load.network
export PRIVATE_KEY=0xYourPrivateKey
cast send $ORACLE "pause()" --rpc-url $RPC_URL --private-key $PRIVATE_KEYExpected Output:
status 1 (success)
✅ Oracle paused
# Poke single feed
export FEED_ID=$(cast keccak "AR/byte")
cast send $ORACLE "poke(bytes32)" $FEED_ID --rpc-url $RPC_URL --private-key $PRIVATE_KEY
# Or poke all feeds from JSON (works while paused)
make alphanet-poke-feeds-json ORACLE=$ORACLE FEEDS_FILE=feeds.jsonExpected Output:
✅ Round 25 finalized for AR/byte
✅ Round 18 rolled forward (stale) for ETH/USD
# Ensure no open round before updating config
export FEED_ID=$(cast keccak "AR/byte")
# Update config (example: change heartbeat to 7200 seconds)
cast send $ORACLE "setFeedConfig(bytes32,(uint8,uint8,uint8,uint8,uint32,uint32,uint32,int256,int256,string))" \
$FEED_ID "(8,2,3,0,7200,50,900,0,10000000000000000000000,'AR/byte updated')" \
--rpc-url $RPC_URL --private-key $PRIVATE_KEYExpected Output:
status 1 (success)
✅ Feed config updated
export FEED_ID=$(cast keccak "AR/byte")
export NEW_OPERATOR=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
cast send $ORACLE "addOperator(bytes32,address)" $FEED_ID $NEW_OPERATOR \
--rpc-url $RPC_URL --private-key $PRIVATE_KEYExpected Output:
status 1 (success)
✅ Operator 0x9965...A4dc added to feed AR/byte
export OLD_OPERATOR=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
cast send $ORACLE "removeOperator(bytes32,address)" $FEED_ID $OLD_OPERATOR \
--rpc-url $RPC_URL --private-key $PRIVATE_KEYExpected Output:
status 1 (success)
✅ Operator 0x9965...A4dc removed from feed AR/byte
# List all operators for a feed
cast call $ORACLE "getOperators(bytes32)" $FEED_ID --rpc-url $RPC_URLExpected Output:
[0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC]
(uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = oracle.latestRoundData(feedId);
require(roundId == answeredInRound, "stale-forwarded");
require(block.timestamp - updatedAt <= MAX_DELAY, "stale-age");After deploying oracle and feeds, run the operator bot to start submitting prices:
# Install dependencies
npm install
# Run bot with deployed addresses
node scripts/bot/operators-bot.mjs \
--rpc http://127.0.0.1:8545 \
--oracle $ORACLE \
--feedDesc "ar/bytes-testv1" \
--interval 30000The bot will:
- Automatically match on-chain operators with Anvil test keys
- Submit prices sequentially (minSubmissions reached, then stop)
- Recover automatically from stuck rounds via
poke() - Handle pause/unpause gracefully
See docs/operator-guide.md for production setup and docs/operator-bot-fix-report.md for architecture details.
Verify the full stack (oracle → adapter → consumer):
# Deploy test consumer
export ADAPTER=0xAdapterAddress
forge script script/DeployTestConsumer.s.sol:DeployTestConsumer \
--rpc-url $RPC_URL --broadcast --sender $ADMIN
# Test integration (specify which feed to test)
export CONSUMER=0xConsumerAddress
export FEED_DESC=ar/bytes-testv1 # Or ar/usd-testv1 for AR/USD feed
node scripts/test-adapter-consumer.mjsThis tests:
- Oracle latest round data
- Adapter Chainlink compatibility
- Consumer reads through adapter
- Historical data access
Testing different feeds:
# Test AR/bytes feed (18 decimals)
export RPC_URL=https://alphanet.load.network
export ORACLE=0x8A0ffF4C118767c818C9F8a30c39E8F9bB36CEd5
export ADAPTER=0xCbbbff18714b1276756980BA7691C67052C9C9ff
export CONSUMER=0x5a65F24AEAd3154aFe3cc9c46806e3D4D2a00118
export FEED_DESC=ar/bytes-testv1
node scripts/test-adapter-consumer.mjs
# Test AR/USD feed (8 decimals)
export RPC_URL=https://alphanet.load.network
export ORACLE=0x8A0ffF4C118767c818C9F8a30c39E8F9bB36CEd5
export ADAPTER=0x920380c14685b88Bb8f6D6A35def83D085152550
export CONSUMER=0xdb067EEFC660e1b38546e670aCAC08D970911fF2
export FEED_DESC=ar/usd-testv1
node scripts/test-adapter-consumer.mjsImportant: Make sure FEED_DESC matches the feed that the ADAPTER is bound to, otherwise the test will query the Oracle with the wrong feed ID and report mismatches.
- Use
anvil-<target>oralphanet-<target>prefixed make targets to auto-set RPC_URL and CHAIN_ID. make doctorprints the selected RPC/CHAIN_ID and the live chain-id from the RPC.- All script targets verify the remote chain-id before broadcasting and pass
--chain-idto sign with the correct domain.
Symptom: Transactions succeed in simulation but fail when broadcast to alphanet.
Cause: Alphanet has different gas economics than local simulation.
Solution: The bootstrap-all target now uses --slow and --gas-estimate-multiplier 300 by default. If you're using custom deployment scripts, add these flags:
forge script YourScript.s.sol:YourScript \
--rpc-url https://alphanet.load.network \
--chain-id 9496 \
--broadcast \
--slow \
--gas-estimate-multiplier 300Symptom: Later transactions fail because they depend on earlier transactions that haven't confirmed yet.
Cause: Multiple dependent transactions broadcast simultaneously.
Solution: Use the --slow flag to wait for each transaction to confirm before sending the next one.
Symptom: Bot shows "Could not find private key for registered operator" warnings.
Cause: The PRIVATE_KEYS_JSON environment variable is not set or contains keys that don't match on-chain operators.
Solution:
# Verify your keys.json matches on-chain operators
node -e "
const keys = require('./keys.json');
const { ethers } = require('ethers');
keys.forEach((key, i) => {
const wallet = new ethers.Wallet(key);
console.log(\`[\${i}] \${wallet.address}\`);
});
"
# Compare with on-chain operators
cast call $ORACLE "getOperators(bytes32)" $FEED_ID --rpc-url $RPC_URL
# Set PRIVATE_KEYS_JSON correctly
export PRIVATE_KEYS_JSON=$(cat keys.json | jq -c)After successful deployment, addresses are saved to out/e2e-addresses.txt:
# View all deployed addresses
make show-addresses
# Or manually extract
cat out/e2e-addresses.txt
# Export to environment variables
export ORACLE=$(awk -F= '/^oracle=/{print $2}' out/e2e-addresses.txt)
export FACTORY=$(awk -F= '/^factory=/{print $2}' out/e2e-addresses.txt)- Local Development Guide - Test deployments locally with Anvil
- Systemd Deployment Guide - Deploy operator bot as systemd service on Ubuntu
- Maintenance Guide - Post-deployment operations
- Operator Guide - Run operator nodes after deployment
- Scripts & Bots - Operator bot and integration testing
- Pre-Mainnet Checklist - Production readiness requirements