Simplified O2 TWAP oracle integration
Standalone guide for agents and applications integrating the active Fuel Credit V8 testnet oracle.
Testnet deployment
| Component | Value |
|---|---|
| Network / chain ID | Fuel testnet / 0 |
| GraphQL RPC | https://testnet.fuel.network/v1/graphql |
| Simplified TWAP oracle | 0x2c32b316b19394261f271bdf2ece2756389c2d9351ab50f13d5b3605b5a176f2 |
| O2 PriceFeed source | 0x7ab771b983bec88a0f7296f55d67e033cdfd40d114fe0f26f3bf1534cfbabf35 |
| O2 fFUEL source asset | 0x43eebd58e837c2e8fa94f1b2db1268175c551b5b1bf41a59fafc4f2c69ab7dc2 |
| Credit V8 consumer | 0x1d5c4730c1d6fada3f0b7c6ce5899cd171ff6b1da38b0f269f1cba1052ba0f9f |
| Oracle deployment transaction | 0x595105553ab8933a054ec3a701f13b47bc7795b1e375ee5c4ad4e67017ff10bc |
| Initial update transaction | 0x2bea3c3e90e709833561fce3f77e87694aba232d954da348d1e2cd44449a224e |
| Health endpoint | https://fuel-credit-testnet-simplified-oracle.nick-j-alexander.workers.dev/v1/health |
Canonical ABI artifact: contracts/simplified-twap-oracle/out/release/simplified-twap-oracle-abi.json. Source: contracts/simplified-twap-oracle/src/main.sw.
These addresses are testnet-only. Independently verify the source contract, asset, records, ABI, and deployment configuration before any mainnet use.
Model and trust boundary
The wrapper has immutable O2 source and asset configurables. It has no owner, privileged updater, price setter, or method for supplying a price. Anyone may call update(); every accepted sample is read directly from O2.
It stores up to 120 samples, separated by at least six hours. A rolling sum, count, and ring index make both updates and reads O(1). The full ring represents approximately 30 days.
The updater Worker runs every 15 minutes. The contract accepts only the first eligible call after each six-hour interval; earlier attempts are harmless skips/reverts. The contract—not the Worker—enforces timing and price validity.
Price policy and units
The oracle's price() and rolling_price() return only the arithmetic mean of retained O2 bid samples:
rolling_average = rolling_sum / sample_countAll PriceRecord.bid and ask values are U128 at O2's 1e18 scale. For example, 802000000000000 means 0.000802 USD/FUEL. Never decode or calculate these values with JavaScript number; use bigint or the Fuels SDK BN.
Credit V8 uses a separate conservative policy:
credit_price_1e15 = min(live_O2_bid_1e18, rolling_average_1e18) / 1_000The integer division floors. The wrapper itself does not take this minimum and does not convert scales. A consumer requiring V8-equivalent pricing must independently read the same O2 source, validate it, take the minimum, then divide by 1_000 if its math expects 1e15.
Source and wrapper validation
Every successful update requires an O2 record with:
- nonzero bid;
ask >= bid;asset_decimals == 9;- source age no greater than 300 seconds;
- source timestamp no more than 60 seconds in the future;
- source timestamp not older than the previously accepted source timestamp; and
- a bid small enough that a 120-sample sum cannot overflow
U128.
price() and rolling_price() return None before the first sample, if chain time moves backwards, if the average is zero, or when the wrapper's last update is older than 90,000 seconds (25 hours).
ABI
pub struct PriceRecord {
bid: U128,
ask: U128,
asset_decimals: u8,
timestamp: u64,
}
abi SimplifiedTWAPOracle {
#[storage(read, write)] fn update();
#[storage(read)] fn price() -> Option<PriceRecord>;
#[storage(read)] fn rolling_price() -> Option<PriceRecord>;
fn source_oracle() -> ContractId;
fn source_asset_id() -> AssetId;
#[storage(read)] fn last_update_timestamp() -> u64;
#[storage(read)] fn sample_count() -> u64;
}Unlike older O2-compatible wrappers, price() takes no asset argument. The source asset is immutable and available through source_asset_id().
The O2 dependency is:
abi O2PriceFeed {
#[storage(read)]
fn price(asset: AssetId) -> Option<PriceRecord>;
}Timestamps are Unix seconds. There are no wrapper log events. Read contract state or monitor known updater transaction receipts; do not build an indexer that expects oracle event IDs.
Sway consumer example
use std::u128::U128;
pub struct PriceRecord {
bid: U128,
ask: U128,
asset_decimals: u8,
timestamp: u64,
}
abi SimplifiedTWAPOracle {
#[storage(read)] fn price() -> Option<PriceRecord>;
}
fn rolling_bid(oracle_id: ContractId) -> U128 {
let oracle = abi(SimplifiedTWAPOracle, oracle_id.into());
oracle.price().unwrap().bid
}The caller transaction must include the oracle contract as an input. To reproduce Credit V8 pricing, define the O2 ABI too, call its immutable asset, validate the returned record, and use min(live.bid, rolling.bid).
Fuels TypeScript examples
import { Contract, Provider } from "fuels";
import oracleAbi from "./simplified-twap-oracle-abi.json";
import o2Abi from "./price-feed-abi.json";
const provider = new Provider("https://testnet.fuel.network/v1/graphql");
const oracle = new Contract(
"0x2c32b316b19394261f271bdf2ece2756389c2d9351ab50f13d5b3605b5a176f2",
oracleAbi,
provider,
);
const o2 = new Contract(
"0x7ab771b983bec88a0f7296f55d67e033cdfd40d114fe0f26f3bf1534cfbabf35",
o2Abi,
provider,
);
const asset = { bits: "0x43eebd58e837c2e8fa94f1b2db1268175c551b5b1bf41a59fafc4f2c69ab7dc2" };
const [{ value: rolling }, { value: live }] = await Promise.all([
oracle.functions.price().get(),
o2.functions.price(asset).get(),
]);
if (!rolling || !live) throw new Error("price unavailable");
const accepted1e18 = BigInt(live.bid.toString()) < BigInt(rolling.bid.toString())
? BigInt(live.bid.toString())
: BigInt(rolling.bid.toString());
const accepted1e15 = accepted1e18 / 1_000n;A permissionless update requires a signer with testnet ETH for gas and the O2 source as a contract input:
const oracleWithWallet = new Contract(oracle.id, oracleAbi, wallet);
const o2WithWallet = new Contract(o2.id, o2Abi, wallet);
const result = await oracleWithWallet.functions
.update()
.addContracts([o2WithWallet])
.call();
await result.waitForResult();An update sooner than six hours after the prior accepted sample reverts with too early; callers may treat that specific result as a healthy no-op.
Operational endpoint
GET https://fuel-credit-testnet-simplified-oracle.nick-j-alexander.workers.dev/v1/health
The endpoint exposes the rolling average, live O2 bid, conservative accepted value, Credit-scale value, timestamps, updater gas status, and last updater result. It is useful for monitoring and UI availability, but it is not authoritative pricing. Contracts and high-integrity clients must read Fuel contract state.
Integration checklist
- Verify chain ID and every contract/asset ID.
- Read
source_oracle()andsource_asset_id()and compare them with expected constants. - Use the committed ABI artifact; remember
price()has no arguments. - Reject
None, zero, crossed, stale, future-dated, or wrong-decimal records. - Keep values as
U128/BN/bigint; never floating-point numbers. - Decide whether rolling-only pricing is sufficient or whether V8's live-bid minimum is required.
- Include all called contracts as transaction inputs.
- Monitor sample count, last update, O2 freshness, updater gas, and transaction receipts.
- Test stale and unavailable behavior before enabling value-bearing operations.
- Re-verify everything independently before mainnet deployment.