# Verify report data onchain (EVM)
Source: https://docs.chain.link/data-streams/tutorials/evm-onchain-report-verification

> For the complete documentation index, see [llms.txt](/llms.txt).

<DataStreams section="dsNotes" />

In this tutorial, you will deploy a verifier contract to *Arbitrum Sepolia*, fund it with LINK, and use it to verify a Data Streams report onchain. You will also learn how to verify multiple reports in a single transaction using `verifyBulkReports()`.

## Before you begin

Make sure you understand how to fetch a signed report payload from the Streams API before working through this tutorial. The payload you receive from fetching a report is exactly what you pass to the verifier contract — no transformation needed.

Refer to the following tutorials:

| Environment    | Fetch                                                              | Stream                                                               |
| -------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Go SDK         | [Fetch and decode reports](/data-streams/tutorials/go-sdk-fetch)   | [Stream and decode reports](/data-streams/tutorials/go-sdk-stream)   |
| TypeScript SDK | [Fetch and decode reports](/data-streams/tutorials/ts-sdk-fetch)   | [Stream and decode reports](/data-streams/tutorials/ts-sdk-stream)   |
| Rust SDK       | [Fetch and decode reports](/data-streams/tutorials/rust-sdk-fetch) | [Stream and decode reports](/data-streams/tutorials/rust-sdk-stream) |

## Requirements

- Testnet ETH and LINK on *Arbitrum Sepolia*. Both are available at [faucets.chain.link](https://faucets.chain.link/arbitrum-sepolia).
- A wallet private key for signing transactions. If you use MetaMask, follow the [MetaMask guide](https://support.metamask.io/managing-my-wallet/secret-recovery-phrase-and-private-keys/how-to-export-an-accounts-private-key/) to export your private key. Never share your private key or commit it to version control.
- [Foundry](https://book.getfoundry.sh/) installed. If you don't have it yet, run:

  ```bash
  curl -L https://foundry.paradigm.xyz | bash
  ```

  Then restart your terminal and run:

  ```bash
  foundryup
  ```

  Verify the installation:

  ```bash
  forge --version
  ```

## Set up your project

1. Create a new directory and initialize a Foundry project:

   ```bash
   mkdir data-streams-verification
   cd data-streams-verification
   forge init --no-git
   ```

2. Install the required npm packages. Foundry resolves `@chainlink/contracts` and `@openzeppelin/contracts` imports through `node_modules`:

   ```bash
   npm install @chainlink/contracts @openzeppelin/contracts
   ```

3. Configure `foundry.toml` to include `node_modules` in the library search path and add the remappings:

   ```toml
   [profile.default]
   src = "src"
   out = "out"
   libs = ["lib", "node_modules"]
   remappings = [
     "@chainlink/contracts/=node_modules/@chainlink/contracts/",
     "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
     "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts/",
   ]
   ```

4. Copy the `ClientReportsVerifier.sol` contract into your `src/` directory. You can view the full contract below:

   ```sol
   // SPDX-License-Identifier: MIT
   pragma solidity ^0.8.20;

   import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol";
   import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol";
   import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
   import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

   using SafeERC20 for IERC20;

   /**
    * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
    * DO NOT USE THIS CODE IN PRODUCTION.
    *
    *  This contract verifies Chainlink Data Streams reports onchain and pays
    *  the verification fee in LINK (when required). It exposes two verification
    *  functions:
    *
    *  - `verifyReport()`      – verifies a single report payload.
    *  - `verifyBulkReports()` – verifies multiple payloads (across any feed IDs)
    *                             in a single transaction using `verifyBulk()`.
    *
    * - If `VerifierProxy.s_feeManager()` returns a non-zero address, the network
    *   expects you to interact with that FeeManager for every verification call:
    *   quote fees, approve the RewardManager, then call `verify()` / `verifyBulk()`.
    *
    * - If `s_feeManager()` returns the zero address, no FeeManager contract has
    *   been deployed on that chain. In that case there is nothing to quote or pay
    *   onchain, so the contract skips the fee logic entirely.
    *
    *  The `if (address(feeManager) != address(0))` check below chooses the
    *  correct path automatically, making the same bytecode usable on any chain.
    */

   // ────────────────────────────────────────────────────────────────────────────
   //  Interfaces
   // ────────────────────────────────────────────────────────────────────────────

   interface IVerifierProxy {
     /**
      * @notice Route a report to the correct verifier and (optionally) bill fees.
      * @param payload           Full report payload (header + signed report).
      * @param parameterPayload  ABI-encoded fee metadata.
      */
     function verify(
       bytes calldata payload,
       bytes calldata parameterPayload
     ) external payable returns (bytes memory verifierResponse);

     /**
      * @notice Route multiple reports to the correct verifier in a single call.
      * @param payloads          Array of full report payloads. Each entry may reference
      *                          a different feed ID. Order is preserved in the output.
      * @param parameterPayload  ABI-encoded fee token address shared across all reports
      *                          (or empty bytes if no FeeManager is deployed).
      * @return verifiedReports  Verified report bytes in the same order as the input.
      */
     function verifyBulk(
       bytes[] calldata payloads,
       bytes calldata parameterPayload
     ) external payable returns (bytes[] memory verifiedReports);

     function s_feeManager() external view returns (IVerifierFeeManager);
   }

   interface IFeeManager {
     /**
      * @return fee, reward, totalDiscount
      */
     function getFeeAndReward(
       address subscriber,
       bytes memory unverifiedReport,
       address quoteAddress
     ) external returns (Common.Asset memory, Common.Asset memory, uint256);

     function i_linkAddress() external view returns (address);

     function i_nativeAddress() external view returns (address);

     function i_rewardManager() external view returns (address);
   }

   // ────────────────────────────────────────────────────────────────────────────
   //  Contract
   // ────────────────────────────────────────────────────────────────────────────

   /**
    * @dev This contract implements functionality to verify Data Streams reports from
    * the Data Streams API, with payment in LINK tokens.
    */
   contract ClientReportsVerifier {
     // ----------------- Errors -----------------
     error NothingToWithdraw();
     error NotOwner(address caller);
     error InvalidReportVersion(uint16 version);
     error EmptyReportsArray();

     // ----------------- Report schemas -----------------
     // More info: https://docs.chain.link/data-streams/reference/report-schema-v3
     /**
      * @dev Data Streams report schema v3 (crypto streams).
      *      Prices, bids and asks use 8 or 18 decimals depending on the stream.
      */
     struct ReportV3 {
       bytes32 feedId;
       uint32 validFromTimestamp;
       uint32 observationsTimestamp;
       uint192 nativeFee;
       uint192 linkFee;
       uint32 expiresAt;
       int192 price;
       int192 bid;
       int192 ask;
     }

     /**
      * @dev Data Streams report schema v8 (RWA streams).
      */
     struct ReportV8 {
       bytes32 feedId;
       uint32 validFromTimestamp;
       uint32 observationsTimestamp;
       uint192 nativeFee;
       uint192 linkFee;
       uint32 expiresAt;
       uint64 lastUpdateTimestamp;
       int192 midPrice;
       uint32 marketStatus;
     }

     // ----------------- Storage -----------------
     IVerifierProxy public immutable i_verifierProxy;
     address private immutable i_owner;

     int192 public lastDecodedPrice;
     int192[] public lastDecodedPrices;

     // ----------------- Events -----------------
     event DecodedPrice(int192 price);

     // ----------------- Constructor / modifier -----------------
     /**
      * @param _verifierProxy Address of the VerifierProxy on the target network.
      *        Addresses: https://docs.chain.link/data-streams/crypto-streams
      */
     constructor(
       address _verifierProxy
     ) {
       i_owner = msg.sender;
       i_verifierProxy = IVerifierProxy(_verifierProxy);
     }

     modifier onlyOwner() {
       if (msg.sender != i_owner) revert NotOwner(msg.sender);
       _;
     }

     // ----------------- Public API -----------------

     /**
      * @notice Verify a Data Streams report (schema v3 or v8).
      *
      * @dev Steps:
      *  1. Decode the unverified report to get `reportData`.
      *  2. Read the first two bytes → schema version (`0x0003` or `0x0008`).
      *     - Revert if the version is unsupported.
      *  3. Fee handling:
      *     - Query `s_feeManager()` on the proxy.
      *       – Non-zero → quote the fee, approve the RewardManager,
      *         ABI-encode the fee token address for `verify()`.
      *       – Zero     → no FeeManager; skip quoting/approval and pass `""`.
      *  4. Call `VerifierProxy.verify()`.
      *  5. Decode the verified report into the correct struct and emit the price.
      *
      *  @param unverifiedReport Full payload returned by Streams Direct.
      *  @custom:reverts InvalidReportVersion when schema ≠ v3/v8.
      */
     function verifyReport(
       bytes memory unverifiedReport
     ) external {
       // ─── 1. & 2. Extract reportData and schema version ──
       (, bytes memory reportData) = abi.decode(unverifiedReport, (bytes32[3], bytes));

       uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | uint16(uint8(reportData[1]));
       if (reportVersion != 3 && reportVersion != 8) {
         revert InvalidReportVersion(reportVersion);
       }

       // ─── 3. Fee handling ──
       IFeeManager feeManager = IFeeManager(address(i_verifierProxy.s_feeManager()));

       bytes memory parameterPayload;
       if (address(feeManager) != address(0)) {
         // FeeManager exists — always quote & approve
         address feeToken = feeManager.i_linkAddress();

         (Common.Asset memory fee,,) = feeManager.getFeeAndReward(address(this), reportData, feeToken);

         IERC20(feeToken).approve(feeManager.i_rewardManager(), fee.amount);
         parameterPayload = abi.encode(feeToken);
       } else {
         // No FeeManager deployed on this chain
         parameterPayload = bytes("");
       }

       // ─── 4. Verify through the proxy ──
       bytes memory verified = i_verifierProxy.verify(unverifiedReport, parameterPayload);

       // ─── 5. Decode & store price ──
       if (reportVersion == 3) {
         int192 price = abi.decode(verified, (ReportV3)).price;
         lastDecodedPrice = price;
         emit DecodedPrice(price);
       } else {
         int192 price = abi.decode(verified, (ReportV8)).midPrice;
         lastDecodedPrice = price;
         emit DecodedPrice(price);
       }
     }

     /**
      * @notice Verify multiple Data Streams reports (schema v3 or v8) in one transaction.
      *
      * @dev Steps:
      *  1. Decode each payload to extract `reportData` and the schema version.
      *     - Revert if any version is unsupported.
      *  2. Fee handling (when FeeManager is deployed):
      *     - Quote the fee for each report individually via `getFeeAndReward`.
      *     - Accumulate the total fee across all reports.
      *     - Approve the RewardManager once for the combined total.
      *     - ABI-encode the fee token address for `verifyBulk()`.
      *  3. Call `VerifierProxy.verifyBulk()` once with all payloads.
      *  4. Decode each verified report, store prices in `lastDecodedPrices`,
      *     and emit a `DecodedPrice` event per report.
      *
      *  @param unverifiedReports Array of full payloads from Streams Direct.
      *         Each payload may reference a different feed ID.
      *  @custom:reverts EmptyReportsArray    when called with an empty array.
      *  @custom:reverts InvalidReportVersion when any schema version ≠ v3/v8.
      */
     function verifyBulkReports(
       bytes[] memory unverifiedReports
     ) external {
       if (unverifiedReports.length == 0) revert EmptyReportsArray();

       // ─── 1. Decode all payloads upfront ──
       bytes[] memory reportDataArray = new bytes[](unverifiedReports.length);
       uint16[] memory reportVersions = new uint16[](unverifiedReports.length);

       for (uint256 i = 0; i < unverifiedReports.length; i++) {
         (, bytes memory reportData) = abi.decode(unverifiedReports[i], (bytes32[3], bytes));

         uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | uint16(uint8(reportData[1]));
         if (reportVersion != 3 && reportVersion != 8) {
           revert InvalidReportVersion(reportVersion);
         }

         reportDataArray[i] = reportData;
         reportVersions[i] = reportVersion;
       }

       // ─── 2. Fee handling ──
       IFeeManager feeManager = IFeeManager(address(i_verifierProxy.s_feeManager()));

       bytes memory parameterPayload;
       if (address(feeManager) != address(0)) {
         address feeToken = feeManager.i_linkAddress();
         uint256 totalFee = 0;

         // Quote per-report fees and accumulate
         for (uint256 i = 0; i < reportDataArray.length; i++) {
           (Common.Asset memory fee,,) = feeManager.getFeeAndReward(address(this), reportDataArray[i], feeToken);
           totalFee += fee.amount;
         }

         // Single approval covers the combined cost of all reports
         IERC20(feeToken).approve(feeManager.i_rewardManager(), totalFee);
         parameterPayload = abi.encode(feeToken);
       } else {
         // No FeeManager deployed on this chain
         parameterPayload = bytes("");
       }

       // ─── 3. Verify all reports in one proxy call ──
       bytes[] memory verifiedReports = i_verifierProxy.verifyBulk(unverifiedReports, parameterPayload);

       // ─── 4. Decode verified reports, store prices, emit events ──
       int192[] memory prices = new int192[](verifiedReports.length);

       for (uint256 i = 0; i < verifiedReports.length; i++) {
         int192 price;
         if (reportVersions[i] == 3) {
           price = abi.decode(verifiedReports[i], (ReportV3)).price;
         } else {
           price = abi.decode(verifiedReports[i], (ReportV8)).midPrice;
         }
         prices[i] = price;
         emit DecodedPrice(price);
       }

       lastDecodedPrices = prices;
     }

     /**
      * @notice Withdraw all balance of an ERC-20 token held by this contract.
      * @param _beneficiary Address that receives the tokens.
      * @param _token       ERC-20 token address.
      */
     function withdrawToken(
       address _beneficiary,
       address _token
     ) external onlyOwner {
       uint256 amount = IERC20(_token).balanceOf(address(this));
       if (amount == 0) revert NothingToWithdraw();
       IERC20(_token).safeTransfer(_beneficiary, amount);
     }
   }
   ```

5. Set up credentials for signing transactions. This tutorial uses Foundry's encrypted keystore, which stores your private key in an encrypted file rather than plaintext:

   ```bash
   cast wallet import dataStreamsWallet --interactive
   ```

   Foundry will prompt you for your private key and a password to encrypt it with. Verify it was saved:

   ```bash
   cast wallet list
   ```

   You should see `dataStreamsWallet` in the output. You will be prompted for the keystore password whenever you send a transaction.

> **CAUTION: Securing your private key**
>
> There are several ways to manage private keys for signing — encrypted keystores, hardware wallets, environment
> variables loaded from a secrets manager, and others. This tutorial demonstrates one approach. You are solely
> responsible for securing your credentials and private keys. Never commit plaintext private keys to version control
> or share them with anyone.

1. Set your RPC URL as an environment variable for the session:

   ```bash
   export RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
   ```

2. Compile the contract to verify the setup is correct:

   ```bash
   forge build
   ```

   You should see output ending with:

   ```bash
   Compiler run successful!
   ```

## Deploy the verifier contract

Deploy `ClientReportsVerifier` to *Arbitrum Sepolia*, passing the Chainlink-deployed `VerifierProxy` address as the constructor argument. The `VerifierProxy` is a Chainlink-deployed contract that routes your verification calls to the correct Verifier contract and handles fee collection. You are not deploying the proxy yourself — you only point your contract at it. The VerifierProxy address for Arbitrum Sepolia is 0x2ff010DEbC1297f19579B4246cad07bd24F2488A. You can find addresses for other networks on the [Stream Addresses](/data-streams/crypto-streams) page.

```bash
forge create src/ClientReportsVerifier.sol:ClientReportsVerifier \
  --rpc-url $RPC_URL \
  --account dataStreamsWallet \
  --broadcast \
  --constructor-args 0x2ff010DEbC1297f19579B4246cad07bd24F2488A
```

Expect output similar to:

```bash
[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xYourWalletAddress
Deployed to: 0xYourContractAddress
Transaction hash: 0xYourTransactionHash
```

You can view the deployed contract on the [Arbitrum Sepolia explorer](https://sepolia.arbiscan.io/address/0xYourContractAddress). For a real example, see this [deployed contract](https://sepolia.arbiscan.io/address/0x546BD7DEFa79060eEf1fB5F99F5790B335376A7D) from a test run.

Save the `Deployed to` address — you'll need it for the remaining steps. Export it as an environment variable:

```bash
export CONTRACT_ADDRESS=0xYourContractAddress
```

## Fund the verifier contract

This contract pays verification fees in LINK tokens on networks where a `FeeManager` is deployed. Arbitrum Sepolia requires fees, so the contract needs LINK before it can verify reports.

The LINK token contract address on Arbitrum Sepolia is 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E. Transfer 1 testnet LINK to your deployed contract:

```bash
cast send 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E \
  "transfer(address,uint256)" \
  $CONTRACT_ADDRESS 1000000000000000000 \
  --rpc-url $RPC_URL \
  --account dataStreamsWallet
```

`1000000000000000000` is 1 LINK expressed in 18 decimal places. Confirm the transfer was successful by checking the contract's LINK balance:

```bash
cast call 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E \
  "balanceOf(address)(uint256)" \
  $CONTRACT_ADDRESS \
  --rpc-url $RPC_URL
```

You should see a non-zero value returned.

## Verify a single report

You need a signed report payload to verify. This is the `fullReport` value from a report you fetched using the Streams API — see the [Fetch and decode reports](/data-streams/tutorials/go-sdk-fetch) tutorial for an example payload.

When you call `verifyReport()`, the contract does the following:

1. **Extract the report version**: The payload is ABI-encoded as `(bytes32[3], bytes)`. The contract decodes it and reads the first two bytes of `reportData` to determine the schema version:

   | Schema version | Report type                                                        |
   | -------------- | ------------------------------------------------------------------ |
   | `0x0003`       | [Crypto Advanced (v3)](/data-streams/reference/report-schema-v3)   |
   | `0x0008`       | [RWA Standard (v8)](/data-streams/reference/report-schema-v8)      |
   | `0x0007`       | [Redemption Rates (v7)](/data-streams/reference/report-schema-v7)  |
   | `0x0009`       | [SmartData (v9)](/data-streams/reference/report-schema-v9)         |
   | `0x000A`       | [Tokenized Asset (v10)](/data-streams/reference/report-schema-v10) |
   | `0x000B`       | [RWA Advanced (v11)](/data-streams/reference/report-schema-v11)    |

   Unsupported versions revert with `InvalidReportVersion`.

2. **Handle fees**: The contract calls `s_feeManager()` on the proxy to check whether a `FeeManager` is deployed on the current network. On networks with a FeeManager (Arbitrum, Avalanche, Base, Blast, Bob, Ink, Linea, OP, Scroll, Soneium, ZKSync), it quotes the fee via `getFeeAndReward`, approves the `RewardManager` to spend that LINK amount, and encodes the fee token address into `parameterPayload`. On networks without a FeeManager (zero address returned), it passes empty bytes and skips fee logic entirely. This check runs automatically, so the same contract works on any supported network without changes.

3. **Verify through the proxy**: The contract calls `VerifierProxy.verify()`, which checks the DON's signatures and returns the verified report body.

4. **Decode and store the price**: The verified bytes are decoded into the appropriate struct (`ReportV3` or `ReportV8`). The price is stored in `lastDecodedPrice` and emitted as a `DecodedPrice` event.

Store the payload in your shell:

```bash
PAYLOAD=0x0006f9b553e393ced311551efd30d1decedb63d76ad41737462e2cdbbdff1578000000000000000000000000000000000000000000000000000000004f8e8a11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba78200000000000000000000000000000000000000000000000000000000675e0a5b00000000000000000000000000000000000000000000000000000000675e0a5b00000000000000000000000000000000000000000000000000001787ff5c6fb8000000000000000000000000000000000000000000000000000c01807477ecd000000000000000000000000000000000000000000000000000000000675f5bdb0000000000000000000000000000000000000000000000d1865f8f627c4113300000000000000000000000000000000000000000000000d18572c6cdc3b915000000000000000000000000000000000000000000000000d1879ab8e98f743ad00000000000000000000000000000000000000000000000000000000000000002f3316e5c964d118f6683eecda454985fcc696e4ba34d65edb4a71a8d0cfe970676f465618c7d01196e433cc35b6994e7ad7b8189b0462b51458e663d601fdfaa0000000000000000000000000000000000000000000000000000000000000002219a4493fdf311421d664e0c8d69efa74b776461f8e252d191eda7edb980ab9a5cce69ec0ad35ba210cf60a201ceff6771b35b44860fda859f4aaba242c476bf
```

> **NOTE: Use a fresh payload in production**
>
> The example payload above is from the [Fetch and decode reports](/data-streams/tutorials/go-sdk-fetch) tutorial and is
> provided for reference. Reports have an expiry timestamp — a production contract should verify reports before they
> expire. For testing, use a payload you fetched yourself.

Call `verifyReport()` on your deployed contract:

```bash
cast send $CONTRACT_ADDRESS \
  "verifyReport(bytes)" \
  $PAYLOAD \
  --rpc-url $RPC_URL \
  --account dataStreamsWallet
```

Expect output similar to:

```bash
blockHash            0xecdaf4164a7b440557a7f3ac1dd6a95b74d6ff22a8f1dc8d02778c403e449d96
blockNumber          287851808
contractAddress
from                 0xYourWalletAddress
gasUsed              118541
status               1 (success)
to                   0xYourContractAddress
transactionHash      0xac09ea7b489fe0d41dad9dd82e7ed3ebabe9124f89ffe350ec7df746b9f8b6da
```

A `status` of `1` means the transaction succeeded and the report was verified. You can view the transaction on the [Arbitrum Sepolia explorer](https://sepolia.arbiscan.io/tx/0xac09ea7b489fe0d41dad9dd82e7ed3ebabe9124f89ffe350ec7df746b9f8b6da).

### Read the decoded price

After verification, the contract stores the decoded price in `lastDecodedPrice`. Read it:

```bash
cast call $CONTRACT_ADDRESS \
  "lastDecodedPrice()(int192)" \
  --rpc-url $RPC_URL
```

Example output:

```bash
1926616346301577350000
```

This is the ETH/USD price with 18 decimal places: **\~1,926.62 USD**. Each stream uses a different number of decimal places — see the [Stream Addresses](/data-streams/crypto-streams) page for details.

## Verify multiple reports

`verifyBulkReports()` lets you verify reports for multiple feed IDs in a single transaction. This is useful when your protocol needs prices from more than one stream atomically.

The function follows the same four stages as `verifyReport()`, extended to handle an array of payloads:

1. All payloads are decoded and their versions validated in a loop before any fee or verification logic runs.
2. `getFeeAndReward` is called once per report, and the individual fees are summed into a single `totalFee`. A single `approve()` covers the combined cost of the entire batch.
3. All payloads are passed to `VerifierProxy.verifyBulk()` in one call.
4. The verified reports are decoded and stored in `lastDecodedPrices[]`, with a `DecodedPrice` event emitted per report.

Payloads in the array may reference different feed IDs — there is no requirement that they all be for the same stream.

Store two payloads (one per feed) as shell variables, then call the function with them as an array:

```bash
PAYLOAD_1=0xYOUR_FIRST_PAYLOAD
PAYLOAD_2=0xYOUR_SECOND_PAYLOAD
```

```bash
cast send $CONTRACT_ADDRESS \
  "verifyBulkReports(bytes[])" \
  "[$PAYLOAD_1,$PAYLOAD_2]" \
  --rpc-url $RPC_URL \
  --account dataStreamsWallet
```

Expect output similar to:

```bash
blockHash            0x5d89e4caf23eac7339e3a64348364821a622b0c30617954bdf250512e7ac78f1
blockNumber          287851901
contractAddress
from                 0xYourWalletAddress
gasUsed              207014
status               1 (success)
to                   0xYourContractAddress
transactionHash      0x67ba5902005ed54cb38826381ca3cede6938442996839bb5cb9341e732e6ec77
```

A `status` of `1` means the transaction succeeded and both reports were verified. You can view the transaction on the [Arbitrum Sepolia explorer](https://sepolia.arbiscan.io/tx/0x67ba5902005ed54cb38826381ca3cede6938442996839bb5cb9341e732e6ec77).

After the transaction succeeds, read the decoded prices. `lastDecodedPrices` is a dynamic array — you can read individual entries by index:

```bash
# First price (index 0) — ETH/USD
cast call $CONTRACT_ADDRESS \
  "lastDecodedPrices(uint256)(int192)" \
  0 \
  --rpc-url $RPC_URL

# Second price (index 1) — LINK/USD
cast call $CONTRACT_ADDRESS \
  "lastDecodedPrices(uint256)(int192)" \
  1 \
  --rpc-url $RPC_URL
```

Example output:

```bash
1926607236156530850000
8531864121895529000
```

These are the ETH/USD and LINK/USD prices with 18 decimal places: **\~1,926.61 USD** and **\~8.53 USD** respectively.

## Recover tokens

The contract includes a `withdrawToken()` function that allows the owner to recover any ERC-20 tokens (including LINK) held by the contract. It reverts with `NothingToWithdraw` if the balance is zero.