# Price Feeds Tutorial

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

This tutorial walks you through integrating Switchboard oracle price feeds into your Sui Move contracts using the Quote Verifier pattern. You'll learn how to securely fetch, verify, and use real-time price data.

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

## What You'll Build

A Move contract that:

* Fetches real-time price data from multiple Switchboard oracles
* Verifies oracle signatures cryptographically
* Validates data freshness and price deviation
* Stores verified prices for your DeFi logic

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

## Prerequisites

* Sui CLI installed ([Installation Guide](https://docs.sui.io/guides/developer/getting-started/sui-install))
* Node.js 21+ and npm/pnpm
* A Sui keypair with SUI tokens (testnet or mainnet)
* Basic understanding of Move and TypeScript

## Key Concepts

### The Quote Verifier Pattern

Switchboard uses a **Quote Verifier** pattern to ensure oracle data is legitimate. The verifier:

* Checks that data comes from authorized oracles on the correct queue
* Tracks timestamps and slots to prevent replay attacks
* Enables custom validation logic (freshness, deviation limits)

### Why Use Quote Verifier?

Without verification:

* Anyone could submit fake prices if you don't check the queue ID
* You'd have to track last update timestamps yourself
* Stale data could be replayed to manipulate prices

With verification:

* Only data from the authorized oracle queue is accepted
* Automatic freshness checks prevent stale data
* Replay attacks are prevented
* You don't need to manually track update timestamps

### Feed Hashes

Each price feed has a unique 32-byte hex identifier. You can find feed hashes in the [Switchboard Explorer](https://ondemand.switchboard.xyz/).

Example: BTC/USD feed hash:

```
0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
```

## The Move Contract

Here's the complete Move contract that consumes oracle data with built-in verification:

```move
module example::example;

use sui::clock::Clock;
use sui::event;
use switchboard::quote::{QuoteVerifier, Quotes};
use switchboard::decimal::Decimal;

// ========== Error Codes ==========

#[error]
const EInvalidQuote: vector<u8> = b"Invalid quote data";

#[error]
const EQuoteExpired: vector<u8> = b"Quote data is expired";

#[error]
const EPriceDeviationTooHigh: vector<u8> = b"Price deviation exceeds threshold";

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

/// QuoteConsumer - Your Oracle Data Consumer
///
/// This struct manages oracle price data with built-in security features:
/// - `quote_verifier`: Verifies oracle signatures and manages quote storage
/// - `last_price`: The most recent verified price
/// - `last_update_time`: Timestamp of the last update
/// - `max_age_ms`: Maximum age for valid quotes
/// - `max_deviation_bps`: Maximum price deviation allowed (basis points)
public struct QuoteConsumer has key {
    id: UID,
    quote_verifier: QuoteVerifier,
    last_price: Option<Decimal>,
    last_update_time: u64,
    max_age_ms: u64,
    max_deviation_bps: u64,
}

/// Event emitted when price is updated
public struct PriceUpdated has copy, drop {
    feed_hash: vector<u8>,
    old_price: Option<u128>,
    new_price: u128,
    timestamp: u64,
    num_oracles: u64,
}

// ========== Public Functions ==========

/// Initialize a QuoteConsumer with a Quote Verifier
public fun init_quote_consumer(
    queue: ID,
    max_age_ms: u64,
    max_deviation_bps: u64,
    ctx: &mut TxContext
): QuoteConsumer {
    let verifier = switchboard::quote::new_verifier(ctx, queue);

    QuoteConsumer {
        id: object::new(ctx),
        quote_verifier: verifier,
        last_price: option::none(),
        last_update_time: 0,
        max_age_ms,
        max_deviation_bps,
    }
}

/// Create and share a QuoteConsumer
public fun create_quote_consumer(
    queue: ID,
    max_age_ms: u64,
    max_deviation_bps: u64,
    ctx: &mut TxContext
) {
    let consumer = init_quote_consumer(queue, max_age_ms, max_deviation_bps, ctx);
    transfer::share_object(consumer);
}

/// Update price using Switchboard oracle quotes
public fun update_price(
    consumer: &mut QuoteConsumer,
    quotes: Quotes,
    feed_hash: vector<u8>,
    clock: &Clock,
) {
    // STEP 1: Verify oracle signatures and queue membership
    consumer.quote_verifier.verify_quotes(&quotes, clock);

    // STEP 2: Check if the feed exists in the verified quotes
    assert!(consumer.quote_verifier.quote_exists(*& feed_hash), EInvalidQuote);

    // STEP 3: Get the verified quote
    let quote = consumer.quote_verifier.get_quote(*& feed_hash);

    // STEP 4: Ensure the quote is fresh (within 10 seconds)
    assert!(quote.timestamp_ms() + 10000 > clock.timestamp_ms(), EQuoteExpired);

    // STEP 5: Extract the price value
    let new_price = quote.result();

    // STEP 6: Validate price deviation (if we have a previous price)
    if (consumer.last_price.is_some()) {
        let last_price = *consumer.last_price.borrow();
        validate_price_deviation(&last_price, &new_price, consumer.max_deviation_bps);
    };

    // Store the old price for the event
    let old_price_value = if (consumer.last_price.is_some()) {
        option::some(consumer.last_price.borrow().value())
    } else {
        option::none()
    };

    // STEP 7: Update the stored price and timestamp
    consumer.last_price = option::some(new_price);
    consumer.last_update_time = quote.timestamp_ms();

    // STEP 8: Emit event for transparency
    event::emit(PriceUpdated {
        feed_hash,
        old_price: old_price_value,
        new_price: new_price.value(),
        timestamp: quote.timestamp_ms(),
        num_oracles: quotes.oracles().length(),
    });
}

/// Get the current price (if available)
public fun get_current_price(consumer: &QuoteConsumer): Option<Decimal> {
    consumer.last_price
}

/// Check if the current price is fresh (within max age)
public fun is_price_fresh(consumer: &QuoteConsumer, clock: &Clock): bool {
    if (consumer.last_update_time == 0) {
        return false
    };

    let current_time = clock.timestamp_ms();
    current_time - consumer.last_update_time <= consumer.max_age_ms
}

// ========== Private Helper Functions ==========

/// Validate that price deviation is within acceptable bounds
fun validate_price_deviation(
    old_price: &Decimal,
    new_price: &Decimal,
    max_deviation_bps: u64
) {
    let old_value = old_price.value();
    let new_value = new_price.value();

    let change = if (new_value > old_value) {
        ((new_value - old_value) * 10000) / old_value
    } else {
        ((old_value - new_value) * 10000) / old_value
    };

    assert!(change <= (max_deviation_bps as u128), EPriceDeviationTooHigh);
}
```

### Contract Walkthrough

#### Imports

```move
use switchboard::quote::{QuoteVerifier, Quotes};
use switchboard::decimal::Decimal;
```

* `QuoteVerifier` - Manages quote verification and storage
* `Quotes` - The signed oracle data structure
* `Decimal` - Switchboard's decimal type for price values

#### QuoteConsumer Struct

The `QuoteConsumer` is a shared object that stores:

* `quote_verifier`: Handles signature verification and prevents replay attacks
* `last_price`: Most recent verified price
* `last_update_time`: Timestamp for freshness checks
* `max_age_ms`: Maximum acceptable quote age (e.g., 300000 = 5 minutes)
* `max_deviation_bps`: Maximum price change in basis points (e.g., 1000 = 10%)

#### The update\_price Function

This is the core function that processes oracle data:

1. **`verify_quotes()`** - Verifies all oracle signatures and ensures they're from the correct queue
2. **`quote_exists()`** - Confirms the requested feed is in the quotes
3. **`get_quote()`** - Retrieves the verified quote data
4. **Freshness check** - Rejects data older than 10 seconds
5. **Deviation check** - Prevents sudden price jumps that might indicate manipulation
6. **Store and emit** - Updates state and emits a `PriceUpdated` event

#### Business Logic Examples

The contract includes example functions for common DeFi use cases:

```move
/// Calculate collateral ratio using fresh price
public fun calculate_collateral_ratio(
    consumer: &QuoteConsumer,
    collateral_amount: u64,
    debt_amount: u64,
    clock: &Clock
): u64 {
    assert!(is_price_fresh(consumer, clock), EQuoteExpired);

    let price = consumer.last_price.borrow();
    let collateral_value = (collateral_amount as u128) * price.value();
    let debt_value = (debt_amount as u128) * 1_000_000_000;

    ((collateral_value * 100) / debt_value as u64)
}

/// Check if liquidation is needed
public fun should_liquidate(
    consumer: &QuoteConsumer,
    collateral_amount: u64,
    debt_amount: u64,
    liquidation_threshold: u64,
    clock: &Clock
): bool {
    if (!is_price_fresh(consumer, clock)) {
        return false // Don't liquidate with stale data
    };

    let ratio = calculate_collateral_ratio(consumer, collateral_amount, debt_amount, clock);
    ratio < liquidation_threshold
}
```

## The TypeScript Client

Here's the complete TypeScript client that creates a QuoteConsumer and updates prices:

```typescript
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
import { fromBase64 as fromB64 } from "@mysten/sui/utils";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { SwitchboardClient, Quote } from "@switchboard-xyz/sui-sdk";

// Configuration
const config = {
  network: (process.env.SUI_NETWORK || "mainnet") as "mainnet" | "testnet",
  rpcUrl: process.env.SUI_RPC_URL || undefined,
  keystoreIndex: parseInt(process.env.KEYSTORE_INDEX || "0"),
  examplePackageId: process.env.EXAMPLE_PACKAGE_ID || "",
  feedHash: process.env.FEED_HASH || "0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812",
  numOracles: parseInt(process.env.NUM_ORACLES || "3"),
  maxAgeMs: parseInt(process.env.MAX_AGE_MS || "300000"),
  maxDeviationBps: parseInt(process.env.MAX_DEVIATION_BPS || "1000"),
};

// Load keypair from Sui keystore
function loadKeypair(): Ed25519Keypair {
  const keystorePath = path.join(os.homedir(), ".sui", "sui_config", "sui.keystore");
  const keystore = JSON.parse(fs.readFileSync(keystorePath, "utf-8"));
  const secretKey = fromB64(keystore[config.keystoreIndex]);
  return Ed25519Keypair.fromSecretKey(secretKey.slice(1));
}

async function main() {
  console.log("Switchboard Oracle Quote Verifier Example\n");

  const rpcUrl = config.rpcUrl || getFullnodeUrl(config.network);
  console.log("Configuration:");
  console.log(`  Network: ${config.network}`);
  console.log(`  Package: ${config.examplePackageId}`);
  console.log(`  Feed: ${config.feedHash}`);
  console.log(`  Oracles: ${config.numOracles}\n`);

  // Initialize clients
  const client = new SuiClient({ url: rpcUrl });
  const sb = new SwitchboardClient(client);
  const state = await sb.fetchState();

  console.log("Switchboard Connected:");
  console.log(`  Oracle Queue: ${state.oracleQueueId}`);
  console.log(`  Network: ${state.mainnet ? 'Mainnet' : 'Testnet'}\n`);

  const keypair = loadKeypair();
  const userAddress = keypair.getPublicKey().toSuiAddress();
  console.log(`User Address: ${userAddress}\n`);

  // Step 1: Create QuoteConsumer
  console.log("Step 1: Creating QuoteConsumer...");

  const createTx = new Transaction();
  createTx.moveCall({
    target: `${config.examplePackageId}::example::create_quote_consumer`,
    arguments: [
      createTx.pure.id(state.oracleQueueId),
      createTx.pure.u64(config.maxAgeMs),
      createTx.pure.u64(config.maxDeviationBps),
    ],
  });

  const createRes = await client.signAndExecuteTransaction({
    signer: keypair,
    transaction: createTx,
    options: { showEffects: true, showObjectChanges: true, showEvents: true },
  });

  // Extract QuoteConsumer ID
  let quoteConsumerId: string | null = null;
  for (const change of createRes.objectChanges ?? []) {
    if (change.type === "created" && change.objectType?.includes("::example::QuoteConsumer")) {
      quoteConsumerId = change.objectId;
      console.log(`QuoteConsumer Created: ${quoteConsumerId}\n`);
      break;
    }
  }

  if (!quoteConsumerId) {
    throw new Error("Failed to create QuoteConsumer");
  }

  // Wait for object availability
  await new Promise(resolve => setTimeout(resolve, 2000));

  // Step 2: Fetch Oracle Data
  console.log("Step 2: Fetching Oracle Data...");

  const updateTx = new Transaction();
  const quotes = await Quote.fetchUpdateQuote(sb, updateTx, {
    feedHashes: [config.feedHash],
    numOracles: config.numOracles,
  });

  console.log("Oracle data fetched successfully\n");

  // Step 3: Verify and Update Price
  console.log("Step 3: Verifying and Updating Price...");

  updateTx.moveCall({
    target: `${config.examplePackageId}::example::update_price`,
    arguments: [
      updateTx.object(quoteConsumerId),
      quotes,
      updateTx.pure.vector("u8", Array.from(Buffer.from(config.feedHash.replace("0x", ""), "hex"))),
      updateTx.object("0x6"), // Sui Clock
    ],
  });

  const updateRes = await client.signAndExecuteTransaction({
    signer: keypair,
    transaction: updateTx,
    options: { showEffects: true, showEvents: true },
  });

  // Display results
  if (updateRes.effects?.status.status === "success") {
    console.log("Price Update Successful!\n");
  }

  // Parse events
  for (const event of updateRes.events ?? []) {
    if (event.type.includes("PriceUpdated")) {
      const data = event.parsedJson as any;
      console.log("PriceUpdated Event:");
      console.log(`  Feed Hash: ${Buffer.from(data.feed_hash).toString('hex')}`);
      console.log(`  New Price: ${data.new_price}`);
      console.log(`  Timestamp: ${new Date(parseInt(data.timestamp)).toISOString()}`);
      console.log(`  Oracles: ${data.num_oracles}`);
    }
  }
}

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

### Client Walkthrough

#### Step 1: Create QuoteConsumer

```typescript
const createTx = new Transaction();
createTx.moveCall({
  target: `${config.examplePackageId}::example::create_quote_consumer`,
  arguments: [
    createTx.pure.id(state.oracleQueueId),  // Oracle queue from Switchboard state
    createTx.pure.u64(config.maxAgeMs),      // Max quote age (5 minutes)
    createTx.pure.u64(config.maxDeviationBps), // Max deviation (10%)
  ],
});
```

This creates a shared QuoteConsumer object tied to Switchboard's oracle queue.

#### Step 2: Fetch Oracle Quotes

```typescript
const quotes = await Quote.fetchUpdateQuote(sb, updateTx, {
  feedHashes: [config.feedHash],
  numOracles: config.numOracles,
});
```

`Quote.fetchUpdateQuote()` contacts Crossbar to get signed price data from multiple oracles. The `quotes` object is added to the transaction automatically.

#### Step 3: Update Price

```typescript
updateTx.moveCall({
  target: `${config.examplePackageId}::example::update_price`,
  arguments: [
    updateTx.object(quoteConsumerId),
    quotes,
    updateTx.pure.vector("u8", feedHashBytes),
    updateTx.object("0x6"), // Sui Clock
  ],
});
```

This calls your contract's `update_price` function with the fetched quotes. The Move contract will verify signatures and update the stored price.

## Project Structure

```
sui/feeds/basic/
├── Move.toml              # Checked-in default Move config (testnet)
├── Move.testnet.toml      # Explicit testnet configuration
├── Move.mainnet.toml      # Explicit mainnet configuration
├── sources/
│   └── example.move       # Quote Consumer contract with verifier
├── scripts/
│   ├── run.ts             # Complete TypeScript example
│   └── quotes.ts          # Simple quote fetching example
└── package.json
```

## Available Scripts

```bash
# Build explicitly for testnet
npm run build:testnet

# Build explicitly for mainnet
npm run build:mainnet

# Run Move tests
npm run test

# Deploy to testnet
npm run deploy:testnet

# Deploy to mainnet
npm run deploy:mainnet

# Run the complete example with Move integration
npm run example

# Run simple quote fetching example (defaults to mainnet)
npm run quotes

# Run simple quote fetching example on testnet
npm run quotes -- --network testnet
```

## 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/sui/feeds/basic
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Smoke-Test Quote Fetching

If you want to validate the Switchboard quote path before deploying your Move package, run the quote-only example first:

```bash
# Defaults to mainnet and dry-runs without a private key
npm run quotes

# Optional: target testnet explicitly
npm run quotes -- --network testnet
```

### 4. Build the Move Contract

```bash
# For testnet
npm run build:testnet

# For mainnet
npm run build:mainnet
```

### 5. Deploy the Contract

```bash
# For testnet
npm run deploy:testnet

# For mainnet
npm run deploy:mainnet
```

Save the package ID from the deployment output.

### 6. Run the Example

```bash
# Match the network to the Move package you deployed
export SUI_NETWORK=testnet
export EXAMPLE_PACKAGE_ID=0xYOUR_PACKAGE_ID

# Run the example
npm run example

# Or with custom parameters
export FEED_HASH=0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
export NUM_ORACLES=5
npm run example
```

### Expected Output

```
Switchboard Oracle Quote Verifier Example

Configuration:
  Network: testnet
  Package: 0xYOUR_PACKAGE_ID
  Feed: 0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
  Oracles: 3

Switchboard Connected:
  Oracle Queue: 0xe9324b82374f18d17de601ae5a19cd72e8c9f57f54661bf9e41a76f8948e80b5
  Network: Testnet

User Address: 0x...

Step 1: Creating QuoteConsumer...
QuoteConsumer Created: 0x...

Step 2: Fetching Oracle Data...
Oracle data fetched successfully

Step 3: Verifying and Updating Price...
Price Update Successful!

PriceUpdated Event:
  Feed Hash: 4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
  New Price: 98765432100
  Timestamp: 2025-12-18T10:30:00.000Z
  Oracles: 3
```

## Adding to Your Project

### 1. Add Switchboard to Move.toml

```toml
[dependencies.Switchboard]
git = "https://github.com/switchboard-xyz/sui.git"
subdir = "on_demand/"
rev = "mainnet"  # or "testnet"

[dependencies.Sui]
git = "https://github.com/MystenLabs/sui.git"
subdir = "crates/sui-framework/packages/sui-framework"
rev = "framework/mainnet"  # or "framework/testnet"
```

### 2. Import in Your Move Module

```move
use switchboard::quote::{QuoteVerifier, Quotes};
use switchboard::decimal::Decimal;
```

### 3. Add Quote Verifier to Your Struct

```move
public struct MyProtocol has key {
    id: UID,
    quote_verifier: QuoteVerifier,
    // ... your other fields
}
```

### 4. TypeScript Dependencies

```bash
npm install @switchboard-xyz/sui-sdk@0.1.14 @mysten/sui@1.38.0
```

## Available Feeds

| Asset   | Feed Hash                                                            |
| ------- | -------------------------------------------------------------------- |
| BTC/USD | `0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812` |
| ETH/USD | `0xa0950ee5ee117b2e2c30f154a69e17bfb489a7610c508dc5f67eb2a14616d8ea` |
| SOL/USD | `0x822512ee9add93518eca1c105a38422841a76c590db079eebb283deb2c14caa9` |
| SUI/USD | `0x7ceef94f404e660925ea4b33353ff303effaf901f224bdee50df3a714c1299e9` |

Find more feeds at the [Switchboard Explorer](https://ondemand.switchboard.xyz/).

## Deployments

| Network | Package ID                                                           |
| ------- | -------------------------------------------------------------------- |
| Mainnet | `0xa81086572822d67a1559942f23481de9a60c7709c08defafbb1ca8dffc44e210` |
| Testnet | `0x28005599a66e977bff26aeb1905a02cda5272fd45bb16a5a9eb38e8659658cff` |

## Troubleshooting

### "EInvalidQuote"

* The requested feed hash is not in the quotes
* Verify the feed hash is correct and included in `fetchUpdateQuote()`

### "EQuoteExpired"

* Quote data is older than 10 seconds
* Fetch fresh data before calling `update_price()`
* Check network latency

### "EPriceDeviationTooHigh"

* Price changed more than the configured `max_deviation_bps`
* This can happen during high volatility
* Adjust the threshold if needed for your use case

### "EInvalidQueue"

* The quotes are from a different oracle queue
* Verify you're using the correct queue ID for your network (mainnet vs testnet)

### Build Errors

```bash
# Clean and rebuild
rm -rf build/

# For testnet
npm run build:testnet

# For mainnet
npm run build:mainnet
```

## Next Steps

* **Multiple Feeds**: Pass multiple feed hashes to `fetchUpdateQuote()` to update several prices in one transaction
* **Real-time Streaming**: See the [Surge Price Stream](https://github.com/switchboard-xyz/gitbook-on-demand/blob/main/docs-by-chain/sui/price-feeds/surge-price-stream.md) tutorial for WebSocket-based streaming
* **Custom Feeds**: Learn how to create custom data feeds in the [Custom Feeds](/custom-feeds/build-and-deploy-feed.md) section


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.switchboard.xyz/docs-by-chain/sui/price-feeds/price-feeds-tutorial.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
