Guide · Oracle
Read FTSO Scaling feeds with Merkle proofs in Solidity
Block-latency FTSOv2 gives you around 30 feeds pushed every 1.8 seconds. FTSO Scaling is the larger sibling: 1000+ feeds published as a single Merkle root on-chain every 90 seconds, with the individual values served off-chain. You pull the feed value plus a Merkle branch from any Flare Systems Client, submit it to your contract, and your contract verifies the branch against the on-chain root before trusting the number. No new infrastructure, just an extra off-chain hop.
When to use Scaling instead of block-latency
- Your pair is not in the anchor set. Block-latency publishes the top crypto and forex pairs only. Anything niche (long-tail tokens, exotic forex, commodities) ships via Scaling.
- You need finer-grained accuracy. Scaling values are a median of the full voter committee; block-latency is a trimmed mid-value from the anchor subset.
- 90-second latency is fine. Scaling is a single on-chain write per voting round. If you settle in blocks, block-latency is cheaper and simpler. If you settle in rounds, Scaling is free data.
Solidity: verify a feed on-chain
The caller passes you a FeedDataWithProof struct. You verify it, check the voting round is recent enough, and use the value.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {FtsoV2Interface} from
"@flarenetwork/flare-periphery-contracts/flare/FtsoV2Interface.sol";
import {ContractRegistry} from
"@flarenetwork/flare-periphery-contracts/flare/ContractRegistry.sol";
contract ScalingConsumer {
uint32 public constant MAX_ROUND_AGE = 4; // 4 rounds = ~6 minutes
function lastPrice(FtsoV2Interface.FeedDataWithProof calldata proof)
external
view
returns (int256 value, int8 decimals)
{
FtsoV2Interface ftso = ContractRegistry.getFtsoV2();
require(ftso.verifyFeedData(proof), "FTSO Scaling: bad proof");
uint32 latest = ftso.getCurrentVotingEpochId();
require(
proof.body.votingRoundId + MAX_ROUND_AGE >= latest,
"FTSO Scaling: stale proof"
);
return (int256(proof.body.value), proof.body.decimals);
}
}verifyFeedData hashes proof.body and checks the supplied Merkle branch against the root committed for proof.body.votingRoundId. The staleness check is yours to define; 4 rounds is about 6 minutes which is lenient. If you write derivatives, tighten it.
Off-chain: pull the feed value + proof
Any Flare Systems Client exposes a standard endpoint that returns the latest value for a given feed ID plus the Merkle branch for the current voting round. Feed ID encoding is the same bytes21 format as block-latency (category byte + 20-byte UTF-8 symbol), documented in the FTSOv2 prices guide.
import json
from urllib.request import Request, urlopen
FEED_ID = "0x01534f4c2f55534400000000000000000000000000" # SOL/USD, crypto
CLIENT = "https://your-flare-systems-client.example"
def fetch_feed(feed_id: str) -> dict:
url = f"{CLIENT}/api/v0/ftso/scaling/feeds/{feed_id}/latest"
with urlopen(url, timeout=10) as resp:
return json.loads(resp.read())
data = fetch_feed(FEED_ID)
# {
# "body": {
# "votingRoundId": 123456,
# "id": "0x01534f4c2f55534400000000000000000000000000",
# "value": 14200000000,
# "turnoutBIPS": 9950,
# "decimals": 8
# },
# "proof": ["0xa1...", "0xb2...", "0xc3..."]
# }
value = data["body"]["value"] / (10 ** data["body"]["decimals"])
print(f"SOL/USD = {value:,.2f} (round {data['body']['votingRoundId']})")Don't run a Flare Systems Client yourself unless you're running a voter. Several validators publish public endpoints; pick one with an uptime SLO you trust. Treat the response as untrusted data. The on-chain verifyFeedData is the authority.
TypeScript: the write path
Typical frontend flow: fetch the feed off-chain, pass the proof into your contract write. viem handles the struct encoding as long as the ABI matches FtsoV2Interface.FeedDataWithProof.
import { createWalletClient, http, parseAbi } from "viem";
import { flare } from "viem/chains";
type FeedBody = {
votingRoundId: number;
id: `0x${string}`;
value: bigint;
turnoutBIPS: number;
decimals: number;
};
type FeedWithProof = { body: FeedBody; proof: `0x${string}`[] };
const abi = parseAbi([
"function lastPrice(((uint32,bytes21,int32,uint16,int8),bytes32[])) view returns (int256,int8)",
]);
async function getSolUsd(signer: ReturnType<typeof createWalletClient>) {
const res = await fetch(
"https://your-flare-systems-client.example/api/v0/ftso/scaling/feeds/0x01534f4c2f55534400000000000000000000000000/latest",
);
const feed: FeedWithProof = await res.json();
const [value, decimals] = await signer.readContract({
address: "0xYourScalingConsumer",
abi,
functionName: "lastPrice",
args: [feed],
});
return Number(value) / 10 ** decimals;
}Things that bite on the first integration
- The proof expires when the next round commits. Your caller can't hold a proof for an hour and then submit it; the root for that voting round is still on-chain but any new read you do wants a fresh one. Fetch, submit, done, within the same round if possible.
- Wrong feed ID category byte. Scaling feed IDs use the same
bytes21as block-latency. If you copy-paste a feed ID from block-latency docs but the pair isn't in the Scaling set, the proof verifies fine and returns a value from a pair that no longer exists. Always sanity-checkproof.body.idmatches your expected feed ID in Solidity before using the value. - Trusting the client response without staleness check. A compromised Flare Systems Client could feed you a stale but structurally valid proof for last week. The staleness assertion in Solidity (
votingRoundId + MAX_ROUND_AGE >= latest) is the line of defence.
Related on the site
- FTSOv2 in 10 lines, companion for the simpler block-latency flow.
- Oracle Lab, live catalog of every block-latency feed; Scaling catalog is orders of magnitude larger and not yet UI-rendered.
- Backtest a data-provider strategy, the other side of the oracle coin, for providers instead of consumers.