Create a Custom Feed
Custom Feeds, introduced in FIP.13, extend the FTSO block-latency feeds by enabling developers to create onchain feeds for arbitrary time-series data. Unlike standard FTSO feeds, which are secured by a decentralized network of data providers, Custom Feeds derive their values from logic defined in a developer-controlled smart contract. This expands the FTSO's capabilities beyond traditional price pairs, allowing for a wider variety of data to be brought onto the Flare network, such as prices for Liquid Staked Tokens (LSTs), data from specific offchain APIs, or other bespoke datasets.
Each Custom Feed has a unique risk profile determined by its smart contract and data source, which users and developers must assess individually.
This guide demonstrates how to build a Custom Feed feed that uses the Flare Data Connector (FDC) to fetch data from an external API, verify it onchain, and make it available to other smart contracts.
Prerequisites
Before you begin, ensure you have:
- A conceptual understanding of smart contracts and Solidity.
- Familiarity with TypeScript/JavaScript and Node.js.
- Hardhat development environment set up.
- Knowledge of the FTSO and an overview of the Flare Data Connector (FDC).
- Environment variables set up for
WEB2JSON_VERIFIER_URL_TESTNET
,VERIFIER_API_KEY_TESTNET
, andCOSTON2_DA_LAYER_URL
as used in thePriceVerification.ts
script.
Concepts
IICustomFeed
Interface
To ensure compatibility with the FTSO system, any custom feed contract must implement the IICustomFeed
interface.
This interface, found in the @flarenetwork/flare-periphery-contracts
package, acts as a standard entry point for consumers of the data.
Key functions
feedId() external view returns (bytes21 _feedId)
:Returns the feed's unique identifier. The first byte must be 0x21 to signify a custom feed.read() public view returns (uint256 value)
: Returns the latest value of the feed.decimals() external view returns (int8)
: Returns the number of decimals for the feed's value.calculateFee() external pure returns (uint256 _fee)
: Calculates the fee for reading the feed. This can be zero.getCurrentFeed() external payable returns (uint256 _value, int8 _decimals, uint64 _timestamp)
: The primary method for retrieving the current feed data, including value, decimals, and a timestamp.
Onchain contract: PriceVerifierCustomFeed.sol
The PriceVerifierCustomFeed.sol
contract is designed to store a historical price for a crypto asset and allow this price to be updated by verifying a proof from the Web2Json FDC attestation type.
It then exposes this price through the IICustomFeed
interface.
Key components
-
State Variables: The contract stores its configuration and the latest verified price.
// --- State Variables ---
bytes21 public immutable feedIdentifier; // Unique ID for this custom feed. ID, e.g., 0x21 + keccak256("BTC/USD-HIST")
string public expectedSymbol; // Asset symbol this feed tracks (e.g., "BTC").
int8 public decimals_; // Precision for the price.
uint256 public latestVerifiedPrice; // Stores the most recent verified price.
address public owner; // Address that deployed the contract, with privileges to update mappings. -
Constructor: Initializes the feed's immutable properties, such as its
feedIdentifier
, symbol, and decimals. It also sets up an initial mapping of asset symbols (e.g., "BTC") to API-specific identifiers (e.g., CoinGecko's "bitcoin").constructor(
bytes21 _feedId,
string memory _expectedSymbol,
int8 _decimals
) {
// ... validation logic ...
owner = msg.sender;
feedIdentifier = _feedId;
expectedSymbol = _expectedSymbol;
decimals_ = _decimals;
// ...
} -
verifyPrice(IWeb2Json.Proof calldata _proof)
: This is the heart of the contract. It accepts a proof from the FDC and performs a series of checks before updating thelatestVerifiedPrice
.-
Parses the API URL from the proof to ensure the data is from the expected source.
-
Verifies the proof's authenticity by calling the FDC's onchain
verifyJsonApi
function. -
Decodes the price data from the proof's response body.
-
Stores the new price in the
latestVerifiedPrice
state variable. -
Emits an event to log the update.
function verifyPrice(IWeb2Json.Proof calldata _proof) external {
require(ContractRegistry.getFdcVerification().verifyJsonApi(_proof), "FDC: Invalid Web2Json proof");
PriceData memory newPriceData = abi.decode(_proof.data.responseBody.abiEncodedData, (PriceData));
latestVerifiedPrice = newPriceData.price;
emit PriceVerified(expectedSymbol, newPriceData.price, _proof.data.requestBody.url);
} -
-
IICustomFeed
Implementation: The contract provides concrete implementations for the interface methods. For example,read()
andgetCurrentFeed()
simply return the latestVerifiedPrice and other stored data.
View full PriceVerifierCustomFeed.sol
contract
PriceVerifierCustomFeed.sol
contract// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
import {IWeb2Json} from "@flarenetwork/flare-periphery-contracts/coston2/IWeb2Json.sol";
import {IICustomFeed} from "@flarenetwork/flare-periphery-contracts/coston2/customFeeds/interface/IICustomFeed.sol";
struct PriceData {
uint256 price;
}
/**
* @title PriceVerifierCustomFeed
* @notice An FTSO Custom Feed contract that sources its value from FDC-verified data using Web2Json.
* @dev Implements the IICustomFeed interface and includes verification logic specific to Web2Json API structure.
*/
contract PriceVerifierCustomFeed is IICustomFeed {
// --- State Variables ---
bytes21 public immutable feedIdentifier;
string public expectedSymbol;
int8 public decimals_;
uint256 public latestVerifiedPrice;
address public owner;
mapping(bytes32 => string) public symbolToCoinGeckoId;
// --- Events ---
event PriceVerified(string indexed symbol, uint256 price, string apiUrl);
event UrlParsingCheck(string apiUrl, string coinGeckoId, string dateString);
event CoinGeckoIdMappingSet(bytes32 indexed symbolHash, string coinGeckoId);
// --- Errors ---
error InvalidFeedId();
error InvalidSymbol();
error UrlCoinGeckoIdMismatchExpected();
error CoinGeckoIdParsingFailed();
error UnknownSymbolForCoinGeckoId(); // Kept for direct call if needed, but mapping is primary
error CoinGeckoIdNotMapped(string symbol);
error DateStringParsingFailed();
error InvalidCoinGeckoIdInUrl(
string url,
string extractedId,
string expectedId
);
error InvalidProof();
// --- Constructor ---
constructor(
bytes21 _feedId,
string memory _expectedSymbol,
int8 _decimals
) {
if (_feedId == bytes21(0)) revert InvalidFeedId();
if (bytes(_expectedSymbol).length == 0) revert InvalidSymbol();
if (_decimals < 0) revert InvalidSymbol();
owner = msg.sender;
feedIdentifier = _feedId;
expectedSymbol = _expectedSymbol;
decimals_ = _decimals;
// Initialize default CoinGecko IDs
_setCoinGeckoIdInternal("BTC", "bitcoin");
_setCoinGeckoIdInternal("ETH", "ethereum");
// Ensure the expected symbol has a mapping at deployment time
require(
bytes(
symbolToCoinGeckoId[
keccak256(abi.encodePacked(_expectedSymbol))
]
).length > 0,
"Initial symbol not mapped"
);
}
// --- Owner Functions ---
/**
* @notice Allows the owner to add or update a CoinGecko ID mapping for a symbol.
* @param _symbol The trading symbol (e.g., "LTC").
* @param _coinGeckoId The corresponding CoinGecko ID (e.g., "litecoin").
*/
function setCoinGeckoIdMapping(
string calldata _symbol,
string calldata _coinGeckoId
) external {
_setCoinGeckoIdInternal(_symbol, _coinGeckoId);
}
function _setCoinGeckoIdInternal(
string memory _symbol,
string memory _coinGeckoId
) internal {
require(bytes(_symbol).length > 0, "Symbol cannot be empty");
require(bytes(_coinGeckoId).length > 0, "CoinGecko ID cannot be empty");
bytes32 symbolHash = keccak256(abi.encodePacked(_symbol));
symbolToCoinGeckoId[symbolHash] = _coinGeckoId;
emit CoinGeckoIdMappingSet(symbolHash, _coinGeckoId);
}
// --- FDC Verification & Price Logic ---
/**
* @notice Verifies the price data proof and stores the price.
* @dev Uses Web2Json FDC verification. Checks if the symbol in the URL matches expectedSymbol.
* @param _proof The IWeb2Json.Proof data structure.
*/
function verifyPrice(IWeb2Json.Proof calldata _proof) external {
// 1. CoinGecko ID Verification (from URL)
string memory extractedCoinGeckoId = _extractCoinGeckoIdFromUrl(
_proof.data.requestBody.url
);
string memory expectedCoinGeckoId = symbolToCoinGeckoId[
keccak256(abi.encodePacked(expectedSymbol))
];
if (bytes(expectedCoinGeckoId).length == 0) {
revert CoinGeckoIdNotMapped(expectedSymbol);
}
if (
keccak256(abi.encodePacked(extractedCoinGeckoId)) !=
keccak256(abi.encodePacked(expectedCoinGeckoId))
) {
revert InvalidCoinGeckoIdInUrl(
_proof.data.requestBody.url,
extractedCoinGeckoId,
expectedCoinGeckoId
);
}
// 2. FDC Verification (Web2Json)
// Aligned with the Web2Json.sol example's pattern
require(
ContractRegistry.getFdcVerification().verifyJsonApi(_proof),
"FDC: Invalid Web2Json proof"
);
// 3. Decode Price Data
// Path changed to _proof.data.responseBody.abiEncodedData
PriceData memory newPriceData = abi.decode(
_proof.data.responseBody.abiEncodedData,
(PriceData)
);
// 4. Store verified data
latestVerifiedPrice = newPriceData.price;
// 5. Emit main event
emit PriceVerified(
expectedSymbol,
newPriceData.price,
_proof.data.requestBody.url // URL from the Web2Json proof
);
}
// --- Custom Feed Logic ---
function read() public view returns (uint256 value) {
value = latestVerifiedPrice;
}
function feedId() external view override returns (bytes21 _feedId) {
_feedId = feedIdentifier;
}
function calculateFee() external pure override returns (uint256 _fee) {
return 0;
}
function getFeedDataView()
external
view
returns (uint256 _value, int8 _decimals)
{
_value = latestVerifiedPrice;
_decimals = decimals_;
}
function getCurrentFeed()
external
payable
override
returns (uint256 _value, int8 _decimals, uint64 _timestamp)
{
_value = latestVerifiedPrice;
_decimals = decimals_;
_timestamp = 0;
}
function decimals() external view returns (int8) {
return decimals_;
}
// --- Internal Helper Functions To Parse URL---
/**
* @notice Helper function to extract a slice of bytes.
* @param data The original bytes array.
* @param start The starting index (inclusive).
* @param end The ending index (exclusive).
* @return The sliced bytes.
*/
function slice(
bytes memory data,
uint256 start,
uint256 end
) internal pure returns (bytes memory) {
require(end >= start, "Slice: end before start");
require(data.length >= end, "Slice: end out of bounds");
bytes memory result = new bytes(end - start);
for (uint256 i = start; i < end; i++) {
result[i - start] = data[i];
}
return result;
}
/**
* @notice Extracts the CoinGecko ID from the API URL.
* @dev It assumes the URL format is like ".../coins/{id}/history..." or ".../coins/{id}"
* @param _url The full URL string from the proof.
* @return The extracted CoinGecko ID.
*/
function _extractCoinGeckoIdFromUrl(
string memory _url
) internal pure returns (string memory) {
bytes memory urlBytes = bytes(_url);
bytes memory prefix = bytes("/coins/");
bytes memory suffix = bytes("/history");
uint256 startIndex = _indexOf(urlBytes, prefix);
if (startIndex == type(uint256).max) {
return ""; // Prefix not found
}
startIndex += prefix.length;
uint256 endIndex = _indexOfFrom(urlBytes, suffix, startIndex);
if (endIndex == type(uint256).max) {
// Suffix not found, assume it's the end of the string
endIndex = urlBytes.length;
}
return string(slice(urlBytes, startIndex, endIndex));
}
/**
* @notice Helper to find the first occurrence of a marker in bytes.
* @param data The bytes data to search in.
* @param marker The bytes marker to find.
* @return The starting index of the marker, or type(uint256).max if not found.
*/
function _indexOf(
bytes memory data,
bytes memory marker
) internal pure returns (uint256) {
uint256 dataLen = data.length;
uint256 markerLen = marker.length;
if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;
for (uint256 i = 0; i <= dataLen - markerLen; i++) {
bool found = true;
for (uint256 j = 0; j < markerLen; j++) {
if (data[i + j] != marker[j]) {
found = false;
break;
}
}
if (found) return i;
}
return type(uint256).max;
}
/**
* @notice Helper to find the first occurrence of a marker in bytes, starting from an index.
* @param data The bytes data to search in.
* @param marker The bytes marker to find.
* @param from The index to start searching from.
* @return The starting index of the marker, or type(uint256).max if not found.
*/
function _indexOfFrom(
bytes memory data,
bytes memory marker,
uint256 from
) internal pure returns (uint256) {
uint256 dataLen = data.length;
uint256 markerLen = marker.length;
if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;
for (uint256 i = from; i <= dataLen - markerLen; i++) {
bool found = true;
for (uint256 j = 0; j < markerLen; j++) {
if (data[i + j] != marker[j]) {
found = false;
break;
}
}
if (found) return i;
}
return type(uint256).max;
}
}
Offchain script: PriceVerification.ts
The PriceVerification.ts
script automates fetching data from CoinGecko via the FDC and submitting it to your PriceVerifierCustomFeed
contract.
It follows these sequential steps:
-
Deploy Contract: The script first deploys the
PriceVerifierCustomFeed.sol
contract to the network. It constructs the uniquefeedId
by combining the0x21
prefix with a hash of the asset string (e.g., "BTC/USD-HIST"). -
Prepare Attestation Request: It constructs a request for the FDC Web2Json attestation type, specifying the target API endpoint (CoinGecko), the parameters (which coin and date), and the JQ filter to extract the exact data point (the USD price) from the JSON response.
-
Submit Request to FDC: The script sends this request to the FDC, which fetches the data, secures it through an attestation process, and makes a proof available.
-
Retrieve Proof: After the attestation round is final, the script queries the Data Availability (DA) layer to retrieve the finalized data and its corresponding Merkle proof.
-
Submit Proof to Custom Feed: Finally, the script calls the
verifyPrice()
function on the deployedPriceVerifierCustomFeed
contract, passing the retrieved proof. The contract then executes its verification logic and, if successful, updates the onchain price.
View full PriceVerification.ts
script.
PriceVerification.ts
script.import { artifacts, web3, run } from "hardhat";
import {
PriceVerifierCustomFeedInstance,
IRelayInstance,
} from "../../typechain-types";
import {
prepareAttestationRequestBase,
getFdcHub,
getFdcRequestFee,
calculateRoundId,
toUtf8HexString,
getRelay,
postRequestToDALayer,
sleep,
} from "../fdcExample/Base";
const PriceVerifierCustomFeed = artifacts.require("PriceVerifierCustomFeed");
const {
WEB2JSON_VERIFIER_URL_TESTNET,
VERIFIER_API_KEY_TESTNET,
COSTON2_DA_LAYER_URL,
} = process.env;
type AttestationRequest = {
source: string;
sourceIdBase: string;
verifierUrlBase: string;
verifierApiKey: string;
urlTypeBase: string;
data: any; // eslint-disable-line @typescript-eslint/no-explicit-any
};
const priceSymbol = "BTC";
const priceDecimals = 2;
const coinGeckoIds: { [key: string]: string } = {
BTC: "bitcoin",
ETH: "ethereum",
};
// --- CoinGecko BTC Price Request Data ---
const symbolForRequest = priceSymbol;
const decimalsForRequest = priceDecimals;
const coinGeckoId = coinGeckoIds[symbolForRequest];
if (!coinGeckoId)
throw new Error(`CoinGecko ID not found for symbol: ${symbolForRequest}`);
const dateToFetch = new Date();
dateToFetch.setDate(dateToFetch.getDate() - 2);
const day = String(dateToFetch.getDate()).padStart(2, "0");
const month = String(dateToFetch.getMonth() + 1).padStart(2, "0");
const year = dateToFetch.getFullYear();
const dateString = `${day}-${month}-${year}`;
const fullApiUrl = `https://api.coingecko.com/api/v3/coins/${coinGeckoId}/history`;
const postprocessJq = `{price: (.market_data.current_price.usd * ${10 ** decimalsForRequest} | floor)}`;
const abiSig = `{"components": [{"internalType": "uint256","name": "price","type": "uint256"}],"internalType": "struct PriceData","name": "priceData","type": "tuple"}`;
const stringifiedQueryParams = JSON.stringify({
date: dateString,
localization: "false",
});
const requests: AttestationRequest[] = [
{
source: "web2json",
sourceIdBase: "PublicWeb2",
verifierUrlBase: WEB2JSON_VERIFIER_URL_TESTNET!,
verifierApiKey: VERIFIER_API_KEY_TESTNET!,
urlTypeBase: "",
data: {
apiUrl: fullApiUrl,
httpMethod: "GET",
headers: "{}",
queryParams: stringifiedQueryParams,
body: "",
postProcessJq: postprocessJq,
abiSignature: abiSig,
logDisplayUrl: fullApiUrl,
},
},
];
async function prepareWeb2JsonAttestationRequest(
transaction: AttestationRequest,
) {
const attestationTypeBase = "Web2Json";
const requestBody = {
url: transaction.data.apiUrl,
httpMethod: transaction.data.httpMethod,
headers: transaction.data.headers,
queryParams: transaction.data.queryParams,
body: transaction.data.body,
postProcessJq: transaction.data.postProcessJq,
abiSignature: transaction.data.abiSignature,
};
const url = `${transaction.verifierUrlBase}Web2Json/prepareRequest`;
const apiKey = transaction.verifierApiKey;
return await prepareAttestationRequestBase(
url,
apiKey,
attestationTypeBase,
transaction.sourceIdBase,
requestBody,
);
}
async function prepareAttestationRequests(transactions: AttestationRequest[]) {
console.log("\nPreparing data...\n");
const data: Map<string, string> = new Map();
for (const transaction of transactions) {
console.log(`(${transaction.source})\n`);
const responseData = await prepareWeb2JsonAttestationRequest(transaction);
console.log("Data:", responseData, "\n");
data.set(transaction.source, responseData.abiEncodedRequest);
}
return data;
}
async function submitAttestationRequests(data: Map<string, string>) {
console.log("\nSubmitting attestation requests...\n");
const fdcHub = await getFdcHub();
const roundIds: Map<string, number> = new Map();
for (const [source, abiEncodedRequest] of data.entries()) {
console.log(`(${source})\n`);
const requestFee = await getFdcRequestFee(abiEncodedRequest);
const transaction = await fdcHub.requestAttestation(abiEncodedRequest, {
value: requestFee,
});
console.log("Submitted request:", transaction.tx, "\n");
const roundId = await calculateRoundId(transaction);
console.log(
`Check round progress at: https://${hre.network.name}-systems-explorer.flare.rocks/voting-round/${roundId}?tab=fdc\n`,
);
roundIds.set(source, roundId);
}
return roundIds;
}
async function retrieveDataAndProofs(
data: Map<string, string>,
roundIds: Map<string, number>,
) {
console.log("\nRetrieving data and proofs...\n");
const proofs: Map<string, any> = new Map(); // eslint-disable-line @typescript-eslint/no-explicit-any
const url = `${COSTON2_DA_LAYER_URL}api/v1/fdc/proof-by-request-round-raw`;
console.log("Url:", url, "\n");
for (const [source, roundId] of roundIds.entries()) {
console.log(`(${source})\n`);
console.log("Waiting for the round to finalize...");
const relay: IRelayInstance = await getRelay();
const protocolId = 200;
console.log("Protocol ID:", protocolId);
while (!(await relay.isFinalized(protocolId, roundId))) {
await sleep(10000);
}
console.log("Round finalized!\n");
const request = { votingRoundId: roundId, requestBytes: data.get(source) };
console.log("Prepared request:\n", request, "\n");
let proof = await postRequestToDALayer(url, request, true);
console.log("Waiting for the DA Layer to generate the proof...");
while (proof.response_hex == undefined) {
await sleep(10000);
proof = await postRequestToDALayer(url, request, false);
}
console.log("Proof generated!\n");
console.log("Proof:", proof, "\n");
proofs.set(source, proof);
}
return proofs;
}
async function retrieveDataAndProofsWithRetry(
data: Map<string, string>,
roundIds: Map<string, number>,
attempts: number = 10,
) {
for (let i = 0; i < attempts; i++) {
try {
return await retrieveDataAndProofs(data, roundIds);
} catch (error) {
console.log(
"Error:",
error,
"\n",
"Remaining attempts:",
attempts - i,
"\n",
);
await sleep(20000);
}
}
throw new Error(
`Failed to retrieve data and proofs after ${attempts} attempts`,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function prepareDataAndProofs(data: Map<string, any>) {
const IWeb2JsonVerification = await artifacts.require(
"IWeb2JsonVerification",
);
const proof = data.get("web2json");
console.log(IWeb2JsonVerification._json.abi[0].inputs[0].components);
return {
merkleProof: proof.merkleProof,
data: web3.eth.abi.decodeParameter(
IWeb2JsonVerification._json.abi[0].inputs[0].components[1],
proof.data || proof.response_hex,
),
};
}
async function deployAndVerifyContract(): Promise<PriceVerifierCustomFeedInstance> {
const feedIdString = `${priceSymbol}/USD-HIST`;
const feedIdHex = toUtf8HexString(feedIdString).substring(2);
const truncatedFeedIdHex = feedIdHex.substring(0, 40);
const finalFeedIdHex = `0x21${truncatedFeedIdHex}`;
if (finalFeedIdHex.length !== 44) {
throw new Error(
`Generated feed ID has incorrect length: ${finalFeedIdHex.length}. Expected 44 characters (0x + 42 hex). Feed string: ${feedIdString}`,
);
}
const customFeedArgs: any[] = [finalFeedIdHex, priceSymbol, priceDecimals]; // eslint-disable-line @typescript-eslint/no-explicit-any
const customFeed: PriceVerifierCustomFeedInstance =
await PriceVerifierCustomFeed.new(...customFeedArgs);
console.log(`PriceVerifierCustomFeed deployed to: ${customFeed.address}\n`);
console.log(
"Waiting 10 seconds before attempting verification on explorer...",
);
await sleep(10000);
try {
await run("verify:verify", {
address: customFeed.address,
constructorArguments: customFeedArgs,
contract:
"contracts/customFeeds/PriceVerifierCustomFeed.sol:PriceVerifierCustomFeed",
});
console.log("Contract verification successful.\n");
} catch (error) {
console.log("Contract verification failed:", error);
}
return customFeed;
}
async function submitDataAndProofsToCustomFeed(
customFeed: PriceVerifierCustomFeedInstance,
proof: any, // eslint-disable-line @typescript-eslint/no-explicit-any
) {
console.log("Proof from submitDataAndProofsToCustomFeed:", proof);
const tx = await customFeed.verifyPrice(proof);
console.log(
`Proof for ${priceSymbol}Price submitted successfully. Transaction hash:`,
tx.transactionHash,
);
}
async function getLatestVerifiedPrice(
customFeed: PriceVerifierCustomFeedInstance,
) {
console.log("\nRetrieving latest verified price from the contract...");
const { _value, _decimals } = await customFeed.getFeedDataView();
const formattedPrice = Number(_value) / 10 ** Number(_decimals);
console.log(
`Latest verified price for ${priceSymbol}/USD: ${formattedPrice} (raw value: ${_value.toString()}, decimals: ${_decimals})`,
);
return formattedPrice;
}
async function main() {
if (
!WEB2JSON_VERIFIER_URL_TESTNET ||
!VERIFIER_API_KEY_TESTNET ||
!COSTON2_DA_LAYER_URL
) {
throw new Error(
"Missing one or more required environment variables: WEB2JSON_VERIFIER_URL_TESTNET, VERIFIER_API_KEY_TESTNET, COSTON2_DA_LAYER_URL",
);
}
const customFeed = await deployAndVerifyContract();
const data = await prepareAttestationRequests(requests);
const roundIds = await submitAttestationRequests(data);
const proofs = await retrieveDataAndProofsWithRetry(data, roundIds);
const decodedData = await prepareDataAndProofs(proofs);
const proof = {
merkleProof: proofs.get("web2json").proof,
data: decodedData.data,
};
await submitDataAndProofsToCustomFeed(customFeed, proof);
await getLatestVerifiedPrice(customFeed);
console.log("Price verification process completed successfully.");
}
void main().then(() => {
process.exit(0);
});
Deploy and use a Custom feed
This guide walks you through using the Flare Hardhat Starter to deploy and interact with a custom price feed.
1. Clone the hardhat starter
First, clone the flare-hardhat-starter
repository and navigate into the project directory:
git clone https://github.com/flare-foundation/flare-hardhat-starter.git
cd flare-hardhat-starter
2. Install dependencies
Install the project dependencies using npm
or yarn
:
npm install # or yarn install
3. Set up environment variables
Copy the example environment file and update it with your own credentials.
cp .env.example .env
You will need to provide the following:
PRIVATE_KEY
: The private key of the account you want to use for deployment on the Coston2 testnet. This account must be funded with C2FLR tokens from the Coston2 Faucet.WEB2JSON_VERIFIER_URL_TESTNET
: The URL for the Web2Json Verifier service. You can leave the default value.VERIFIER_API_KEY_TESTNET
: An API key for the verifier service. You can get one from the Flare Developer Portal.COSTON2_DA_LAYER_URL
: The URL for the Data Availability Layer on Coston2. You can leave the default value.
Never commit your .env
file or share your private keys publicly.
4. Run the verification script
The PriceVerification.ts
script, located in scripts/customFeeds/
, automates the entire process. Execute it on the Coston2 Testnet:
npx hardhat run scripts/customFeeds/PriceVerification.ts --network coston2
The script will:
- Deploy the
PriceVerifierCustomFeed.sol
contract. - Prepare an attestation request for the CoinGecko API.
- Submit the request to the Flare Data Connector (FDC).
- Wait for the attestation to be finalized.
- Retrieve the proof from the Data Availability layer.
- Submit the proof to the deployed
PriceVerifierCustomFeed
contract.
5. Understanding the output
The script will log its progress. A successful run will display the deployed contract address and the final verified price:
Deploying PriceVerifierCustomFeed...
PriceVerifierCustomFeed deployed to: 0x... (contract address)
Preparing data...
// ... (attestation request details)
Submitting attestation requests...
// ... (transaction details)
Waiting for round 12345 to be finalized...
Round finalized.
Retrieving proofs...
// ... (proof details)
Submitting proof to custom feed...
Proof for BTCPrice submitted successfully...
Latest verified price: 12345
Price verification process completed successfully.
6. Verify on explorer
You can view your deployed contract on the Coston2 Explorer.
Use the contract address from the script's output to look it up. On the Read Contract tab, call the latestVerifiedPrice()
function to confirm the price was stored onchain.
Propose a new Custom Feed
If you have developed a Custom Feed that could benefit the wider ecosystem, you can propose it for official listing on the Block-Latency Feeds page. To do so, submit a "New Feed Request" via an issue in the Flare Developer Hub GitHub repository. The request should include a business justification and a link to the verified contract on a block explorer. The Flare Foundation will review the submission for eligibility.
Conclusion
You now have the tools to create FTSO Custom Feeds, bringing diverse, verifiable data from any API onto the Flare network. This capability greatly expands the possibilities for DeFi and other onchain applications. Remember, the security of your Custom Feed depends entirely on its smart contract logic and data verification process. Prioritize careful design and rigorous testing in your implementations.