Documentation

Solidity consumer example

This guide walks through a complete contract that accepts a signed DataLink report, verifies it on-chain, decodes it, checks it, and stores the price.

How verification flows

text

Your backend ──GET /api/v1/reports/latest──▶ signed payload
      │
      ▼ (sends a transaction with the payload)
Your contract.updatePrice(payload)
      │
      ▼ verify(payload, 0x)
VerifierProxy  ──reads the configuration digest, forwards──▶  Verifier
                                                               │ checks the signatures
      ◀──────────────── report data (nine fields) ─────────────┘
      │
      ▼
Your contract decodes, checks feed + freshness, stores the price
  1. Your backend fetches the signed report with its API key (the key never goes on-chain).
  2. A transaction calls your contract with the 0x-prefixed payload.
  3. Your contract calls the VerifierProxy, which forwards the report to the Verifier. The Verifier checks the signatures, emits ReportVerified and returns the report data — or reverts.
  4. Your contract decodes the report data and applies its own checks.

The contract

Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// The one VerifierProxy function a consumer needs.
interface IVerifierProxy {
    /// Verifies a signed DataLink report and returns its report data.
    /// Reverts if the signatures or the report are not valid.
    function verify(bytes calldata payload, bytes calldata parameterPayload)
        external
        payable
        returns (bytes memory verifierResponse);
}

/// Example DataLink consumer: verifies a signed report on-chain, decodes it,
/// checks it is for the expected feed and recent enough, then stores the price.
/// EXAMPLE CODE — not audited. Review and test it before any real use.
contract DataLinkPriceConsumer {
    /// The nine fields encoded in a DataLink report (schema v1), in order.
    struct Report {
        bytes32 feedId;
        uint32 observationsTimestamp;
        int192 benchmarkPrice;
        int192 bid;
        int192 ask;
        uint64 currentBlockNum;
        bytes32 currentBlockHash;
        uint64 validFromBlockNum;
        uint64 currentBlockTimestamp;
    }

    IVerifierProxy public immutable verifierProxy;
    bytes32 public immutable feedId;
    /// Price decimals of this feed (from its report schema). Stored for reference:
    /// a price of 8157000000 with 8 decimals means 81.57.
    uint8 public immutable decimals;
    /// Oldest report accepted, in seconds.
    uint256 public immutable maxReportAge;

    int192 public latestPrice;
    int192 public latestBid;
    int192 public latestAsk;
    uint32 public latestObservationsTimestamp;

    event PriceUpdated(bytes32 indexed feedId, int192 price, uint32 observationsTimestamp);

    error WrongFeed(bytes32 expected, bytes32 received);
    error StaleReport(uint32 observationsTimestamp, uint256 currentTimestamp);
    error NotNewerThanStored(uint32 observationsTimestamp, uint32 storedTimestamp);

    constructor(address verifierProxy_, bytes32 feedId_, uint8 decimals_, uint256 maxReportAge_) {
        verifierProxy = IVerifierProxy(verifierProxy_);
        feedId = feedId_;
        decimals = decimals_;
        maxReportAge = maxReportAge_;
    }

    /// @param signedPayload The `payload` from GET /api/v1/reports/latest, with a 0x prefix.
    function updatePrice(bytes calldata signedPayload) external {
        // 1. Verify. Reverts unless the DataLink signers signed this exact report.
        //    parameterPayload is empty: no FeeManager is configured on the
        //    XDC Mainnet VerifierProxy, so no fee is paid.
        bytes memory reportData = verifierProxy.verify(signedPayload, "");

        // 2. Decode the verified report data into its nine fields.
        Report memory report = abi.decode(reportData, (Report));

        // 3. Only accept the feed this contract is for.
        if (report.feedId != feedId) revert WrongFeed(feedId, report.feedId);

        // 4. Only accept recent reports.
        if (uint256(report.observationsTimestamp) + maxReportAge < block.timestamp) {
            revert StaleReport(report.observationsTimestamp, block.timestamp);
        }

        // 5. Never replace a newer stored price with an older (or the same) report.
        if (report.observationsTimestamp <= latestObservationsTimestamp) {
            revert NotNewerThanStored(report.observationsTimestamp, latestObservationsTimestamp);
        }

        // 6. Store.
        latestPrice = report.benchmarkPrice;
        latestBid = report.bid;
        latestAsk = report.ask;
        latestObservationsTimestamp = report.observationsTimestamp;
        emit PriceUpdated(report.feedId, report.benchmarkPrice, report.observationsTimestamp);
    }
}

What each check does

StepWhy
verifyOnly reports signed by the DataLink oracle network pass. Any changed byte reverts.
abi.decodeTurns the verified report data into typed fields — the struct must match the report schema exactly.
Feed IDA valid report for a different feed is still validly signed. Without this check, anyone could feed your contract another asset's price.
FreshnessA valid report stays valid forever. maxReportAge stops an old (but genuine) report from being used. Compare observationsTimestamp — not the API's createdAt, which isn't signed.
Newer than storedStops someone from replacing your latest price with an older report that is still within maxReportAge.

Using the price

latestPrice is an integer with the feed's decimals. With 8 decimals, divide by 10^8: 8157000000 → 81.57. In Solidity, keep working in the integer form (scale other values up rather than dividing), or convert to your own fixed-point format.

Deploying and using it

  1. Constructor arguments:

    ArgumentValue
    verifierProxy_0xCf49c434f2206Df1D507dE9f5DA70cA8c3dA61d2 (XDC Mainnet)
    feedId_Your feed's ID, e.g. 0x00018cd7a1e01e764604c0e299f74a0ff4179d81e92111389e6585ba6318cbd1
    decimals_The feed's price decimals (Feed Access page, or schema in the decoded API)
    maxReportAge_Seconds, e.g. 3600
  2. Deploy with Remix (Environment: Injected Provider on XDC Mainnet), Hardhat or Foundry.

  3. Update the price: your backend fetches a fresh report from /api/v1/reports/latest, adds 0x to payload, and sends updatePrice(payload).

  4. Read latestPrice, latestBid, latestAsk and latestObservationsTimestamp from your contract.

In the receipt you'll see two events: ReportVerified from the Verifier (with your contract as requester) and PriceUpdated from your contract.

Gas

Verifying through the VerifierProxy costs roughly 90,000 gas on its own (88,258 in the example transaction); this contract's full updatePrice used about 145,000 gas in testing. Actual gas varies — let your wallet estimate it.