Chainlink Oracles
Price a tool's foreign-currency cost against your grant's budget with Chainlink and Pyth readings the provider cannot choose.
On this page
A grant budgeted in dollars can pay a tool that bills in USDC or ETH, at a rate neither side picks. When a tool reports its cost in another currency, the kernel reads a Chainlink price, corroborated by Pyth, converts the cost into the grant's currency, and records the reading on the signed receipt.
A seller-controlled exchange rate could defeat a capability budget: a grant for ten dollars of API time means little if the provider sets the rate. Oracles publish a price and its time, so any third party can recheck the reading a receipt carries.
StartThe kernel prices a foreign cost before it reconciles the hold
The hold is taken in the grant's currency before dispatch. After the call, the tool reports its realized cost, and the kernel converts it through the oracle only when the two currencies differ.
A fresh, corroborated reading reconciles the hold
The Chainlink reading is 312 s old and Pyth agrees within 500 bps. The kernel converts 0.50 USDC at 0.9998, rounds up, adds the 200 bps margin, and charges USD 0.51 against the USD 0.80 hold. The reading rides on the receipt as oracle_evidence. Oracle evidence in receipts.
A failed read never lowers the charge. The call has already run, so the kernel closes the hold at the authorized amount and records the failure, with its reason, in the receipt's metadata. A successful read charges the converted amount, plus the operator's margin, and releases the rest of the hold.
1. Choose the pairs the kernel may price
Chainlink Data Feeds are the primary source, updated on each feed's heartbeat plus a deviation trigger. Pyth Network, read through Hermes, is a pull-based corroborating fallback with a sub-second publish cadence. The shipped inventory is four Base pairs under one policy: a max_age_seconds of 600 for staleness and a divergence_threshold_bps of 500 (5%) for agreement between the sources.
| Pair | Sources | TWAP |
|---|---|---|
ETH/USD | Chainlink and Pyth | Enabled, 600 s window |
BTC/USD | Chainlink and Pyth | Enabled, 600 s window |
USDC/USD | Chainlink and Pyth | Disabled, spot only |
LINK/USD | Chainlink only | Enabled, 600 s window |
USDC/USD is spot only because peg deviation is itself the signal, and smoothing would hide it. LINK/USD has no Pyth source, so its breaker never trips on divergence. Per-pair overrides in the operator configuration enable or disable a pair, force a backend, set a custom divergence threshold, or allow degraded mode, and apply to every later read.
2. Write the oracle configuration
Each pair names its Chainlink feed, its Pyth price id, and its policy. The exchange_rate_margin_bps of 200 (2%) is added after conversion and covers the window between the price read and settlement; tune it per pair. The host installs the configured oracle in the kernel that reconciles charges.
{
"primary": "chainlink",
"fallback": "pyth",
"pairs": [
{
"base": "ETH", "quote": "USD", "chain_id": 8453,
"chainlink": { "address": "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70", "decimals": 8, "heartbeat_seconds": 300 },
"pyth": { "id": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" },
"policy": {
"max_age_seconds": 600,
"divergence_threshold_bps": 500,
"exchange_rate_margin_bps": 200,
"twap_enabled": true
}
},
{
"base": "USDC", "quote": "USD", "chain_id": 8453,
"chainlink": { "address": "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B", "decimals": 8, "heartbeat_seconds": 68400 },
"pyth": { "id": "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a" },
"policy": {
"max_age_seconds": 600,
"divergence_threshold_bps": 500,
"exchange_rate_margin_bps": 200,
"twap_enabled": false,
"stable_pair": true
}
}
],
"operator": {
"global_pause": false,
"chains": [
{
"chain_id": 8453,
"label": "base-mainnet",
"caip2": "eip155:8453",
"enabled": true,
"sequencer_uptime_feed": "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433",
"sequencer_grace_period_seconds": 300
}
]
}
}Check addresses, decimals, and update intervals against the Chainlink feed catalog and the Pyth feed catalog before you deploy. A feed's heartbeat is independent of your staleness limit: the USDC/USD feed's 68,400-second heartbeat means a 600-second limit can reject a reading between updates. Choose the limit for your application's risk and availability needs.
3. Enable the chain and watch its sequencer
Base Mainnet is enabled by default and monitored through the official Chainlink sequencer-uptime feed. Arbitrum One ships as standby inventory, disabled by default. Enable it only after you configure its feeds, connectivity, and sequencer-health policy.
While a chain's sequencer is offline, reads for its pairs fail with SequencerDown. After it recovers they fail with SequencerRecovering until sequencer_grace_period_seconds (300 s by default) elapses, so a freshly restarted sequencer cannot price against stale state. Chainlink's sequencer-health guidance describes the feed's recovery behavior. Setting global_pause stops every read.
4. Know how a read fails
Every unsuccessful read returns a typed PriceOracleError. The oracle library signs nothing itself; the kernel records the failure on the charge's receipt.
- Source divergence. When a pair has two sources and their prices differ by more than
divergence_threshold_bps, the read fails withPriceOracleError::CircuitBreakerTripped, carrying the pair, the observed divergence, and the threshold. This is the only check that compares readings with each other. - Stale reading. A reading whose age exceeds
max_age_secondsfails that read withStale, carrying the observed age and the limit. Each read is judged on the readings it gets: there is no consecutive-failure counter and no persisted unhealthy flag. - Sequencer outage.
SequencerDownandSequencerRecovering, as in step 3.
Operator reports show a PairHealthStatus of healthy, fallback_active, degraded_grace, paused, tripped, or unavailable. The status is derived per observation for reporting and gates nothing. When a read fails during reconciliation, the charge stays at the held amount and the receipt's oracle_conversion note carries status: failed and the reason.
5. Read the conversion on the receipt
A successful conversion attaches an OracleConversionEvidence object under oracle_evidence, in the kernel receipt's financial metadata and in the settlement execution receipt. The type is flat and deny_unknown_fields: a key it does not declare makes the whole receipt fail to parse.
pub struct OracleConversionEvidence {
pub schema: String,
pub base: String,
pub quote: String,
pub authority: String,
pub rate_numerator: u64,
pub rate_denominator: u64,
pub source: String,
pub feed_address: String,
pub updated_at: u64,
pub max_age_seconds: u64,
pub cache_age_seconds: u64,
pub converted_cost_units: u64,
pub original_cost_units: u64,
pub original_currency: String,
pub grant_currency: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oracle_public_key: Option<PublicKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<Signature>,
}The rate is a rate_numerator over rate_denominator fraction, so a verifier recomputes the conversion in exact integer arithmetic. Both costs are in the minor unit of their own currency. The settlement verifier requires converted_cost_units to equal original_cost_units times rate_numerator times the quote currency's minor units per unit, divided by the base currency's minor units per unit times rate_denominator, rounded up.
USD, EUR, and GBP have 100 minor units to the unit, JPY 1, USDC and USDT a million, BTC 100 million, and ETH and LINK 10 to the eighteenth; any other currency is refused as unsupported. The verifier also requires base and quote to equal original_currency and grant_currency, and cache_age_seconds to be at most max_age_seconds. Generate a settlement proof fixture and read its oracle field:
chio proof fixture generate public-settlement-finality-below-threshold --out ./below-threshold$ jq '.settlement_receipt.oracle_evidence' \
./below-threshold/settlement-proof-bundle.json{
"authority": "chio_link_runtime_v1",
"base": "ETH",
"cache_age_seconds": 45,
"converted_cost_units": 300,
"feed_address": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612",
"grant_currency": "USD",
"max_age_seconds": 3600,
"oracle_public_key": "d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c",
"original_cost_units": 1000000000000000,
"original_currency": "ETH",
"quote": "USD",
"rate_denominator": 100,
"rate_numerator": 300000,
"schema": "chio.oracle-conversion-evidence.v1",
"signature": "d1b5f86427cd80be8fced5663435ba313b1daf8ab8b469e2864ec21a7bb07613463f9863dbff2711ac0392fc1b8737f2520f0e9c001122414fbc1305dc820c09",
"source": "chainlink",
"updated_at": 1743292740
}You should see 0.001 ETH (1000000000000000 wei) priced at 3,000 USD per ETH (300000 over 100), which converts to 300 cents: 1015 times 300,000 times 100, divided by 1018 times 100. The reading is 45 seconds old against this fixture's 3,600-second limit. The fixture's evidence also carries oracle_public_key and signature, which a settlement verifier checks against its trusted oracle keys.