Skip to content

Simplified O2 TWAP oracle integration ​

Standalone guide for agents and applications integrating the active Fuel Credit V8 testnet oracle.

Testnet deployment ​

ComponentValue
Network / chain IDFuel testnet / 0
GraphQL RPChttps://testnet.fuel.network/v1/graphql
Simplified TWAP oracle0x2c32b316b19394261f271bdf2ece2756389c2d9351ab50f13d5b3605b5a176f2
O2 PriceFeed source0x7ab771b983bec88a0f7296f55d67e033cdfd40d114fe0f26f3bf1534cfbabf35
O2 fFUEL source asset0x43eebd58e837c2e8fa94f1b2db1268175c551b5b1bf41a59fafc4f2c69ab7dc2
Credit V8 consumer0x1d5c4730c1d6fada3f0b7c6ce5899cd171ff6b1da38b0f269f1cba1052ba0f9f
Oracle deployment transaction0x595105553ab8933a054ec3a701f13b47bc7795b1e375ee5c4ad4e67017ff10bc
Initial update transaction0x2bea3c3e90e709833561fce3f77e87694aba232d954da348d1e2cd44449a224e
Health endpointhttps://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:

text
rolling_average = rolling_sum / sample_count

All 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:

text
credit_price_1e15 = min(live_O2_bid_1e18, rolling_average_1e18) / 1_000

The 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 ​

sway
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:

sway
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 ​

sway
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 ​

ts
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:

ts
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 ​

  1. Verify chain ID and every contract/asset ID.
  2. Read source_oracle() and source_asset_id() and compare them with expected constants.
  3. Use the committed ABI artifact; remember price() has no arguments.
  4. Reject None, zero, crossed, stale, future-dated, or wrong-decimal records.
  5. Keep values as U128/BN/bigint; never floating-point numbers.
  6. Decide whether rolling-only pricing is sufficient or whether V8's live-bid minimum is required.
  7. Include all called contracts as transaction inputs.
  8. Monitor sample count, last update, O2 freshness, updater gas, and transaction receipts.
  9. Test stale and unavailable behavior before enabling value-bearing operations.
  10. Re-verify everything independently before mainnet deployment.

Open-source infrastructure on Fuel.