# Create a Custom Feed

> Bring any time-series data onchain with Custom Feeds.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/ftso/guides/create-custom-feed

Custom Feeds, introduced in [FIP.13](https://proposals.flare.network/FIP/FIP_13.html), extend the FTSO [block-latency feeds](/ftso/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.

Risk Profile

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)](/fdc/overview) to fetch data from an external API, verify it onchain, and make it available to other smart contracts.

## Prerequisites[​](#prerequisites "Direct link to 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 `VERIFIER_URL_TESTNET`, `VERIFIER_API_KEY_TESTNET`, and `COSTON2_DA_LAYER_URL` as used in the `PriceVerification.ts` script.

## Concepts[​](#concepts "Direct link to Concepts")

### `IICustomFeed` Interface[​](#iicustomfeed-interface "Direct link to 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`](https://www.npmjs.com/package/@flarenetwork/flare-periphery-contracts) package, acts as a standard entry point for consumers of the data.

#### Key functions[​](#key-functions "Direct link to 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`[​](#onchain-contract-priceverifiercustomfeedsol "Direct link to onchain-contract-priceverifiercustomfeedsol")

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-types/web2-json) FDC attestation type. It then exposes this price through the `IICustomFeed` interface.

#### Key components[​](#key-components "Direct link to 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 the `latestVerifiedPrice`.
    
    1.  **Parses the API URL** from the proof to ensure the data is from the expected source.
        
    2.  **Verifies the proof's authenticity** by calling the FDC's onchain `verifyWeb2Json` function.
        
    3.  **Decodes the price data** from the proof's response body.
        
    4.  **Stores the new price** in the `latestVerifiedPrice` state variable.
        
    5.  **Emits an event** to log the update.
        
    
    ```
    function verifyPrice(IWeb2Json.Proof calldata _proof) external {    require(ContractRegistry.getFdcVerification().verifyWeb2Json(_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()` and `getCurrentFeed()` simply return the latestVerifiedPrice and other stored data.
    

<details>
<summary>View full PriceVerifierCustomFeed.sol contract</summary>

View full `PriceVerifierCustomFeed.sol` contract

/examples/developer-hub-solidity/PriceVerifierCustomFeed.sol

```
// SPDX-License-Identifier: MITpragma 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/interfaces/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().verifyWeb2Json(_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;    }}
```

</details>

### Offchain script: `PriceVerification.ts`[​](#offchain-script-priceverificationts "Direct link to offchain-script-priceverificationts")

The `PriceVerification.ts` script automates fetching data from CoinGecko via the FDC and submitting it to your `PriceVerifierCustomFeed` contract.

It follows these sequential steps:

1.  **Deploy Contract:** The script first deploys the `PriceVerifierCustomFeed.sol` contract to the network. It constructs the unique `feedId` by combining the `0x21` prefix with a hash of the asset string (e.g., "BTC/USD-HIST").
    
2.  **Prepare Attestation Request:** It constructs a request for the FDC [Web2Json](/fdc/attestation-types/web2-json) 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.
    
3.  **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.
    
4.  **Retrieve Proof:** After the attestation round is final, the script queries the [Data Availability (DA)](/fdc/reference/data-availability-api) layer to retrieve the finalized data and its corresponding Merkle proof.
    
5.  **Submit Proof to Custom Feed:** Finally, the script calls the `verifyPrice()` function on the deployed `PriceVerifierCustomFeed` contract, passing the retrieved proof. The contract then executes its verification logic and, if successful, updates the onchain price.
    

<details>
<summary>View full PriceVerification.ts script.</summary>

View full `PriceVerification.ts` script.

/examples/developer-hub-javascript/PriceVerification.ts

```
import { artifacts, web3, run } from "hardhat";import type {  PriceVerifierCustomFeedInstance,  IRelayInstance,} from "../../typechain-types";import {  prepareAttestationRequestBase,  postRequestToDALayer,  sleep,} from "../utils/fdc";import { getFdcHub, getRelay } from "../utils/getters";import {  getFdcRequestFee,  calculateRoundId,  toUtf8HexString,} from "../utils/core";const PriceVerifierCustomFeed = artifacts.require("PriceVerifierCustomFeed");const { 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: 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-anyasync 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 (    !VERIFIER_URL_TESTNET ||    !VERIFIER_API_KEY_TESTNET ||    !COSTON2_DA_LAYER_URL  ) {    throw new Error(      "Missing one or more required environment variables: 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);});
```

</details>

## Deploy and use a Custom feed[​](#deploy-and-use-a-custom-feed "Direct link to Deploy and use a Custom feed")

This guide walks you through using the [Flare Hardhat Starter](https://github.com/flare-foundation/flare-hardhat-starter) to deploy and interact with a custom price feed.

### 1\. Clone the hardhat starter[​](#1-clone-the-hardhat-starter "Direct link to 1. Clone the hardhat starter")

First, clone the [`flare-hardhat-starter`](https://github.com/flare-foundation/flare-hardhat-starter) repository and navigate into the project directory:

```
git clone https://github.com/flare-foundation/flare-hardhat-starter.gitcd flare-hardhat-starter
```

### 2\. Install dependencies[​](#2-install-dependencies "Direct link to 2. Install dependencies")

Install the project dependencies using `npm` or `yarn`:

```
npm install # or yarn install
```

### 3\. Set up environment variables[​](#3-set-up-environment-variables "Direct link to 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](https://faucet.flare.network/coston2).
-   `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](https://portal.flare.network/).
-   `COSTON2_DA_LAYER_URL`: The URL for the Data Availability Layer on Coston2. You can leave the default value.

danger

Never commit your `.env` file or share your private keys publicly.

### 4\. Run the verification script[​](#4-run-the-verification-script "Direct link to 4. Run the verification script")

The `PriceVerification.ts` script, located in `scripts/customFeeds/`, automates the entire process. Execute it on the Coston2 Testnet:

```
yarn hardhat run scripts/customFeeds/PriceVerification.ts --network coston2
```

The script will:

1.  Deploy the `PriceVerifierCustomFeed.sol` contract.
2.  Prepare an attestation request for the CoinGecko API.
3.  Submit the request to the Flare Data Connector (FDC).
4.  Wait for the attestation to be finalized.
5.  Retrieve the proof from the Data Availability layer.
6.  Submit the proof to the deployed `PriceVerifierCustomFeed` contract.

### 5\. Understanding the output[​](#5-understanding-the-output "Direct link to 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: 12345Price verification process completed successfully.
```

### 6\. Verify on explorer[​](#6-verify-on-explorer "Direct link to 6. Verify on explorer")

You can view your deployed contract on the [Coston2 Explorer](https://coston2-explorer.flare.network/). 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[​](#propose-a-new-custom-feed "Direct link to 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](/ftso/feeds) page. To do so, submit a "New Feed Request" via an issue in the [Flare Developer Hub](https://github.com/flare-foundation/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.

[Propose Custom Feed](https://github.com/flare-foundation/developer-hub/issues/new?labels=enhancement&template=feed_request.yml&title=%5Breq%5D%3A)

## Conclusion[​](#conclusion "Direct link to 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.
