# Price Feeds Tutorial

> **Example Code**: The complete working example for this tutorial is available at [sb-on-demand-examples/evm/price-feeds](https://github.com/switchboard-xyz/sb-on-demand-examples/tree/main/evm/price-feeds)

This tutorial walks you through integrating Switchboard oracle price feeds into your EVM smart contracts. You'll learn how to fetch oracle data, submit updates to your contract, and read verified prices.

> **Version source of truth:** [SDK Version Matrix](https://docs.switchboard.xyz/tooling/sdk-version-matrix)

## What You'll Build

A Solidity smart contract that:

* Receives and stores verified oracle price updates
* Validates price freshness and deviation
* Provides helper functions for DeFi use cases (collateral ratios, liquidations)

Plus a TypeScript client that fetches oracle data and submits it to your contract.

## Prerequisites

* **Foundry** for Solidity development (`forge`, `cast`)
* **Bun** or Node.js 20+
* Native tokens for gas (MON, ETH, etc.)
* Basic understanding of Solidity and ethers.js

## Key Concepts

### How Switchboard On-Demand Works on EVM

Switchboard uses an **on-demand** model where:

1. **Your client** fetches signed price data from Crossbar (Switchboard's gateway)
2. **Your contract** submits the signed data to the Switchboard contract for verification
3. **Switchboard verifies** the oracle signatures and stores the data
4. **Your contract** reads the verified data via `latestUpdate()`

This pattern ensures prices are cryptographically verified on-chain while keeping gas costs low.

### The CrossbarClient

The `CrossbarClient` from `@switchboard-xyz/common` is your interface to fetch oracle data for the v2 feed-hash flow used by current Monad integrations and Feed Builder feeds:

```typescript
import { CrossbarClient } from "@switchboard-xyz/common";

const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
const response = await crossbar.fetchV2Update([feedId], {
  chain: "evm",
  network: "mainnet",
  use_timestamp: true,
});

if (!response.encoded) {
  throw new Error("Crossbar returned no encoded update payload");
}

const updates = [response.encoded];
```

If you are using a `bytes32` feed ID from Explorer or Feed Builder, this is the path you want. Legacy `fetchEVMResults()` and `/updates/evm/...` routes remain available for older aggregator-based integrations, but they are not the primary flow for custom feeds.

### Fee Handling

Some networks require a fee for oracle updates. Always check before submitting:

```typescript
const fee = await switchboard.getFee(updates);
await contract.updatePrices(updates, [feedId], { value: fee });
```

## The Smart Contract

Here's a complete example contract that integrates Switchboard price feeds:

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

import { ISwitchboard } from "./switchboard/interfaces/ISwitchboard.sol";
import { SwitchboardTypes } from "./switchboard/libraries/SwitchboardTypes.sol";

/**
 * @title SwitchboardPriceConsumer
 * @notice Example contract demonstrating Switchboard On-Demand oracle integration
 *
 * Key Features:
 * - Secure price updates with signature verification
 * - Staleness checks to prevent old data usage
 * - Price deviation validation
 * - Multi-feed support
 */
contract SwitchboardPriceConsumer {
    // ========== State Variables ==========

    /// @notice The Switchboard contract interface
    ISwitchboard public immutable switchboard;

    /// @notice Stored price data for each feed
    mapping(bytes32 => PriceData) public prices;

    /// @notice Maximum age for price data (default: 5 minutes)
    uint256 public maxPriceAge = 300;

    /// @notice Maximum price deviation in basis points (default: 10% = 1000 bps)
    uint256 public maxDeviationBps = 1000;

    /// @notice Contract owner
    address public owner;

    // ========== Structs ==========

    /**
     * @notice Stored price information for a feed
     * @param value The price value (18 decimals)
     * @param timestamp When the price was last updated
     * @param slotNumber Solana slot number of the update
     */
    struct PriceData {
        int128 value;
        uint256 timestamp;
        uint64 slotNumber;
    }

    // ========== Events ==========

    event PriceUpdated(
        bytes32 indexed feedId,
        int128 oldPrice,
        int128 newPrice,
        uint256 timestamp,
        uint64 slotNumber
    );

    event PriceValidationFailed(bytes32 indexed feedId, string reason);

    // ========== Errors ==========

    error InsufficientFee(uint256 expected, uint256 received);
    error PriceTooOld(uint256 age, uint256 maxAge);
    error PriceDeviationTooHigh(uint256 deviation, uint256 maxDeviation);
    error InvalidFeedId();
    error Unauthorized();

    // ========== Constructor ==========

    constructor(address _switchboard) {
        switchboard = ISwitchboard(_switchboard);
        owner = msg.sender;
    }

    // ========== External Functions ==========

    /**
     * @notice Update price feeds with oracle data
     * @param updates Encoded Switchboard updates with signatures
     * @param feedIds Array of feed IDs to process from the update
     */
    function updatePrices(
        bytes[] calldata updates,
        bytes32[] calldata feedIds
    ) external payable {
        // Get the required fee
        uint256 fee = switchboard.getFee(updates);
        if (msg.value < fee) {
            revert InsufficientFee(fee, msg.value);
        }

        // Submit updates to Switchboard (verifies signatures)
        switchboard.updateFeeds{ value: fee }(updates);

        // Process each feed ID
        for (uint256 i = 0; i < feedIds.length; i++) {
            bytes32 feedId = feedIds[i];

            // Get the latest verified update from Switchboard
            SwitchboardTypes.LegacyUpdate memory update = switchboard.latestUpdate(feedId);

            // Store the price with validation
            _processFeedUpdate(
                feedId,
                update.result,
                uint64(update.timestamp),
                update.slotNumber
            );
        }

        // Refund excess payment
        if (msg.value > fee) {
            (bool success, ) = msg.sender.call{ value: msg.value - fee }("");
            require(success, "Refund failed");
        }
    }

    /**
     * @notice Get the current price for a feed
     * @param feedId The feed identifier
     * @return value The price value
     * @return timestamp The update timestamp
     * @return slotNumber The Solana slot number
     */
    function getPrice(
        bytes32 feedId
    ) external view returns (int128 value, uint256 timestamp, uint64 slotNumber) {
        PriceData memory priceData = prices[feedId];
        if (priceData.timestamp == 0) revert InvalidFeedId();
        return (priceData.value, priceData.timestamp, priceData.slotNumber);
    }

    /**
     * @notice Check if a price is fresh (within maxPriceAge)
     */
    function isPriceFresh(bytes32 feedId) public view returns (bool) {
        PriceData memory priceData = prices[feedId];
        if (priceData.timestamp == 0) return false;
        return block.timestamp - priceData.timestamp <= maxPriceAge;
    }

    // ========== Internal Functions ==========

    function _processFeedUpdate(
        bytes32 feedId,
        int128 newValue,
        uint64 timestamp,
        uint64 slotNumber
    ) internal {
        PriceData memory oldPrice = prices[feedId];

        // Validate price deviation if we have a previous price
        if (oldPrice.timestamp != 0) {
            uint256 deviation = _calculateDeviation(oldPrice.value, newValue);
            if (deviation > maxDeviationBps) {
                emit PriceValidationFailed(feedId, "Deviation too high");
                revert PriceDeviationTooHigh(deviation, maxDeviationBps);
            }
        }

        // Store the new price
        prices[feedId] = PriceData({
            value: newValue,
            timestamp: timestamp,
            slotNumber: slotNumber
        });

        emit PriceUpdated(feedId, oldPrice.value, newValue, timestamp, slotNumber);
    }

    function _calculateDeviation(
        int128 oldValue,
        int128 newValue
    ) internal pure returns (uint256) {
        if (oldValue == 0) return 0;

        uint128 absOld = oldValue < 0 ? uint128(-oldValue) : uint128(oldValue);
        uint128 absNew = newValue < 0 ? uint128(-newValue) : uint128(newValue);

        uint128 diff = absNew > absOld ? absNew - absOld : absOld - absNew;
        return (uint256(diff) * 10000) / uint256(absOld);
    }
}
```

### Contract Walkthrough

#### State Variables

```solidity
ISwitchboard public immutable switchboard;
mapping(bytes32 => PriceData) public prices;
uint256 public maxPriceAge = 300;
uint256 public maxDeviationBps = 1000;
```

* `switchboard` - Reference to the deployed Switchboard contract
* `prices` - Maps feed IDs to their latest price data
* `maxPriceAge` - Maximum acceptable age for price data (5 minutes default)
* `maxDeviationBps` - Maximum price change allowed (10% default, prevents manipulation)

#### The updatePrices Function

This is the main entry point for updating prices:

1. **Check fee** - Ensure caller sent enough to cover the oracle update fee
2. **Submit to Switchboard** - Call `updateFeeds()` which verifies oracle signatures
3. **Read verified data** - Call `latestUpdate()` to get the verified price
4. **Store locally** - Save the price in your contract's storage
5. **Refund excess** - Return any overpayment to the caller

#### Reading Prices

```solidity
(int128 value, uint256 timestamp, uint64 slotNumber) = consumer.getPrice(feedId);
```

Always check freshness before using a price:

```solidity
require(consumer.isPriceFresh(feedId), "Price is stale");
```

## The TypeScript Client

Here's a complete client that fetches oracle data and submits it to your contract:

```typescript
import * as ethers from "ethers";
import { CrossbarClient } from "@switchboard-xyz/common";

async function main() {
  // Setup
  const privateKey = process.env.PRIVATE_KEY!;
  const contractAddress = process.env.CONTRACT_ADDRESS!;
  const switchboardAddress = process.env.SWITCHBOARD_ADDRESS!;

  const provider = new ethers.JsonRpcProvider("https://rpc.hyperliquid.xyz/evm");
  const signer = new ethers.Wallet(privateKey, provider);

  const consumerAbi = [
    "function updatePrices(bytes[] calldata updates, bytes32[] calldata feedIds) external payable",
    "function getPrice(bytes32 feedId) external view returns (int128 value, uint256 timestamp, uint64 slotNumber)",
    "event PriceUpdated(bytes32 indexed feedId, int128 oldPrice, int128 newPrice, uint256 timestamp, uint64 slotNumber)"
  ];
  const switchboardAbi = [
    "function getFee(bytes[] calldata updates) external view returns (uint256)"
  ];

  const contract = new ethers.Contract(contractAddress, consumerAbi, signer);
  const switchboard = new ethers.Contract(switchboardAddress, switchboardAbi, signer);
  const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

  // The feed ID you want to update (e.g., BTC/USD)
  const feedId = "0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812";

  // Step 1: Fetch signed oracle data from Crossbar
  const response = await crossbar.fetchV2Update([feedId], {
    chain: "evm",
    network: "mainnet",
    use_timestamp: true,
  });

  if (!response.encoded) {
    throw new Error("Crossbar returned no encoded update payload");
  }

  const updates = [response.encoded];
  const median = response.medianResponses[0];

  console.log("Fetched", updates.length, "encoded updates");
  console.log("Median value:", median?.value);
  console.log("Oracle timestamp:", new Date(response.timestamp * 1000).toISOString());

  // Step 2: Submit to your contract
  const fee = await switchboard.getFee(updates);
  const tx = await contract.updatePrices(updates, [feedId], { value: fee });
  console.log("Transaction hash:", tx.hash);

  // Step 3: Wait for confirmation
  const receipt = await tx.wait();
  console.log("Confirmed in block:", receipt.blockNumber);

  // Step 4: Parse events
  const iface = new ethers.Interface(consumerAbi);
  for (const log of receipt.logs) {
    try {
      const parsed = iface.parseLog({ topics: log.topics, data: log.data });
      if (parsed?.name === "PriceUpdated") {
        console.log("\n=== Price Updated ===");
        console.log("Feed ID:", parsed.args.feedId);
        console.log("New Price:", ethers.formatUnits(parsed.args.newPrice, 18));
        console.log("Timestamp:", new Date(Number(parsed.args.timestamp) * 1000).toISOString());
      }
    } catch (e) {
      // Skip non-matching logs
    }
  }

  // Step 5: Read the stored price
  const [value, timestamp, slotNumber] = await contract.getPrice(feedId);
  console.log("\n=== Current Price ===");
  console.log("Value:", ethers.formatUnits(value, 18));
  console.log("Timestamp:", new Date(Number(timestamp) * 1000).toISOString());
  console.log("Slot:", slotNumber.toString());
}

main().catch(console.error);
```

### Client Walkthrough

#### Step 1: Fetch Oracle Data

```typescript
const response = await crossbar.fetchV2Update([feedId], {
  chain: "evm",
  network: "mainnet",
  use_timestamp: true,
});

if (!response.encoded) {
  throw new Error("Crossbar returned no encoded update payload");
}

const updates = [response.encoded];
```

The `fetchV2Update` call returns:

* `medianResponses` with one consensus value per feed
* `timestamp` for the signed oracle consensus
* `oracleResponses` for per-oracle detail
* `encoded`, the EVM payload you wrap into `bytes[]` for `getFee` and `updateFeeds`

#### Step 2: Submit to Contract

```typescript
const fee = await switchboard.getFee(updates);
const tx = await contract.updatePrices(updates, [feedId], { value: fee });
```

Your contract receives the encoded data, submits it to Switchboard for verification, then stores the result.

#### Step 3-5: Confirm and Read

After confirmation, you can:

* Parse `PriceUpdated` events from the receipt
* Read the stored price directly from your contract

## Deployment

The packaged example now uses one network switch for both deploys and runtime:

* `NETWORK=monad-testnet` or `NETWORK=monad-mainnet`
* `RPC_URL` is optional and overrides the default RPC for the selected network
* `PRIVATE_KEY` is required
* `SWITCHBOARD_ADDRESS` is an advanced override only

Defaults:

* `NETWORK=monad-testnet`
* Testnet RPC: `https://testnet-rpc.monad.xyz`
* Mainnet RPC: `https://rpc.monad.xyz`

The packaged deploy flow validates the selected network before broadcast:

* the RPC chain ID must match `NETWORK`
* the resolved Switchboard address must have deployed bytecode
* Monad `SWITCHBOARD_ADDRESS` overrides must match the canonical address for the selected network

Use the packaged wrapper from the example repo:

```bash
# Default: Monad testnet
bun run deploy

# Explicit aliases
bun run deploy:monad-testnet
bun run deploy:monad-mainnet

# Flip to mainnet with one env var
NETWORK=monad-mainnet bun run deploy
```

If you want raw Foundry instead of the wrapper, keep the same env contract:

```bash
NETWORK=monad-testnet \
RPC_URL=https://testnet-rpc.monad.xyz \
forge script deploy/DeploySwitchboardPriceConsumer.s.sol:DeploySwitchboardPriceConsumer \
  --rpc-url $RPC_URL \
  --broadcast \
  -vvvv
```

## Running the Example

### 1. Clone the Examples Repository

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/evm/price-feeds
```

### 2. Install Dependencies

```bash
bun install
(
  cd ../randomness/coin-flip
  [ -d lib/forge-std ] || forge install foundry-rs/forge-std --no-git --shallow
)
forge build
```

### 3. Configure Environment

> **Security:** Never use `export PRIVATE_KEY=...`—it appears in shell history. Use a `.env` file instead.

Create a `.env` file (add it to `.gitignore`):

```bash
PRIVATE_KEY=0x...
NETWORK=monad-testnet
RPC_URL=
SWITCHBOARD_ADDRESS=
# Optional: if omitted, the script deploys a new consumer contract
CONTRACT_ADDRESS=0x...
```

### 4. Run the Example

If `CONTRACT_ADDRESS` is unset, the script deploys a fresh consumer contract before it fetches the v2 update and submits it on-chain:

```bash
bun run example
```

Switch to Monad mainnet without changing the script:

```bash
NETWORK=monad-mainnet bun run example
```

For Feed Builder or custom feeds, it is useful to preflight before sending a transaction:

```typescript
await crossbar.simulateFeed(feedId, false, undefined, "testnet");
```

### Expected Output

```
Feed ID: 0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
Encoded updates length: 1
Transaction hash: 0x...
Transaction confirmed in block: 12345678

=== Feed Update Event ===
Price: 97234500000000000000000
Timestamp: 2024-12-18T10:30:00.000Z

=== Latest Update Details ===
Result: 97234500000000000000000
Timestamp: 2024-12-18T10:30:00.000Z
```

## Adding to Your Project

### 1. Install the Switchboard Interfaces

Copy the interface files from the examples repo:

```bash
cp -r sb-on-demand-examples/evm/price-feeds/src/switchboard your-project/src/
```

Or install via npm:

```bash
npm install @switchboard-xyz/on-demand-solidity
```

### 2. Import and Use

```solidity
import { ISwitchboard } from "./switchboard/interfaces/ISwitchboard.sol";
import { SwitchboardTypes } from "./switchboard/libraries/SwitchboardTypes.sol";

contract YourContract {
    ISwitchboard public switchboard;

    constructor(address _switchboard) {
        switchboard = ISwitchboard(_switchboard);
    }

    function yourFunction(bytes[] calldata updates, bytes32 feedId) external payable {
        // Submit to Switchboard
        uint256 fee = switchboard.getFee(updates);
        switchboard.updateFeeds{ value: fee }(updates);

        // Read verified data
        SwitchboardTypes.LegacyUpdate memory update = switchboard.latestUpdate(feedId);

        // Use update.result (the price)
        int128 price = update.result;
        // ... your logic here
    }
}
```

## Example: DeFi Business Logic

The example contract includes helper functions for common DeFi patterns:

### Calculate Collateral Ratio

```solidity
function calculateCollateralRatio(
    bytes32 feedId,
    uint256 collateralAmount,
    uint256 debtAmount
) external view returns (uint256 ratio) {
    require(isPriceFresh(feedId), "Price is stale");

    PriceData memory priceData = prices[feedId];

    // Calculate collateral value in USD
    uint256 collateralValue = (collateralAmount * uint128(priceData.value)) / 1e18;

    // Return ratio in basis points (15000 = 150%)
    ratio = (collateralValue * 10000) / debtAmount;
}
```

### Check Liquidation

```solidity
function shouldLiquidate(
    bytes32 feedId,
    uint256 collateralAmount,
    uint256 debtAmount,
    uint256 liquidationThreshold  // e.g., 11000 = 110%
) external view returns (bool) {
    if (!isPriceFresh(feedId)) return false;

    PriceData memory priceData = prices[feedId];
    uint256 collateralValue = (collateralAmount * uint128(priceData.value)) / 1e18;
    uint256 ratio = (collateralValue * 10000) / debtAmount;

    return ratio < liquidationThreshold;
}
```

## Switchboard Contract Addresses

| Network          | Chain ID | Switchboard Contract                         |
| ---------------- | -------- | -------------------------------------------- |
| Monad Testnet    | 10143    | `0x6724818814927e057a693f4e3A172b6cC1eA690C` |
| Monad Mainnet    | 143      | `0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67` |
| HyperEVM Mainnet | 999      | `0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347` |
| Arbitrum One     | 42161    | `0xAd9b8604b6B97187CDe9E826cDeB7033C8C37198` |
| Arbitrum Sepolia | 421614   | `0xA2a0425fA3C5669d384f4e6c8068dfCf64485b3b` |
| Core Mainnet     | 1116     | `0x33A5066f65f66161bEb3f827A3e40fce7d7A2e6C` |

## Available Feeds

Find available price feeds at the [Switchboard Explorer](https://explorer.switchboardlabs.xyz).

Popular feeds include:

* **BTC/USD**: `0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812`
* **ETH/USD**: `0xa0950ee5ee117b2e2c30f154a69e17bfb489a7610c508dc5f67eb2a14616d8ea`
* **SOL/USD**: `0x822512ee9add93518eca1c105a38422841a76c590db079eebb283deb2c14caa9`

## Troubleshooting

| Error                   | Solution                                                                                                                            |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `InsufficientFee`       | Query `switchboard.getFee(updates)` and send that amount as `msg.value`                                                             |
| `PriceDeviationTooHigh` | Normal during high volatility; adjust `maxDeviationBps` if needed                                                                   |
| `PriceTooOld`           | Fetch fresh data from Crossbar; adjust `maxPriceAge` if needed                                                                      |
| `InvalidFeedId`         | Ensure the feed ID exists and has been updated at least once                                                                        |
| `ORACLE_UNAVAILABLE`    | If `simulateFeed` works but `fetchV2Update` fails, treat it as oracle or gateway availability rather than a missing deployment step |
| Build errors            | Bootstrap `forge-std` in `../randomness/coin-flip`, then rerun `forge build`                                                        |

## Next Steps

* Explore [randomness integration](https://github.com/switchboard-xyz/gitbook-on-demand/blob/main/docs-by-chain/evm/randomness.md) for gaming and NFTs
* Learn about [custom feeds](https://docs.switchboard.xyz/custom-feeds/build-and-deploy-feed) for specialized data
* Join the [Switchboard Discord](https://discord.gg/TJAv6ZYvPC) for support
