FlareForge

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

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

Related on the site