# Introduction

Switchboard is the **fastest**, most **customizable**, and only **permissionless** way to bring data on-chain.

We're the go-to data provider for prominent DeFi projects such as [Kamino](https://kamino.com/), [Jito](https://www.jito.network/), [MarginFi](https://app.marginfi.com/), and [Drift](https://www.drift.trade/). We secure billions of dollars of on-chain volume, process hundreds of millions of requests weekly, and deliver real-time information for hundreds of projects and assets across 10+ blockchains.

Why use Switchboard? Our protocol is designed around 4 principles:

* **Fastest Oracle Updates**
  * With latencies of 2-5ms with Surge, or 400ms with our standard oracles, no one beats Switchboard's speeds. In the fast-paced world of DeFi, faster price updates directly translate to increased security and higher returns.
* **Low Costs**
  * With on-demand feeds, feeds are created and used only when needed. This eliminates constant data streaming and *significantly* reduces latency and costs.
* **Permissionless and Flexible**
  * Deploy your new data feed, stream from any source on or off-chain, and set the exact parameters that you need. No need to wait for contracts or red tape.
* **Secure and Private**
  * Switchboard prevents anyone, including node operators, from altering, exposing or frontrunning your data. Inside Trusted Execution Environments (TEEs), your code and data stay safe and secure.

The Switchboard Protocol is [open-source](https://github.com/switchboard-xyz) and contributions are welcome. Need help using Switchboard? Our support team is available around the clock on [Discord](https://discord.gg/TJAv6ZYvPC).


# Quick Start

**Learn to use Switchboard Data Feeds:**

* Learn how to use a Switchboard price feed in your on-chain application on [Solana](/docs-by-chain/solana-svm/price-feeds/basic-price-feed), [EVM](/docs-by-chain/evm/price-feeds/price-feeds-tutorial), or many other chains.
* Explore **Surge**, the industry's fastest oracle data stream, on [Solana](/docs-by-chain/solana-svm/surge), [EVM](/docs-by-chain/evm/surge), or [Sui](/docs-by-chain/sui/surge).
* Generate verifiably fair randomness for games, lotteries, and more on [Solana](/docs-by-chain/solana-svm/randomness/randomness-tutorial) or [EVM](/docs-by-chain/evm/randomness/randomness-tutorial).

**Explore existing feeds or create your own:**

* Browse through thousands of existing data feeds on [our explorer site](https://explorer.switchboardlabs.xyz).
* Can't find an existing feed that does what you need? Learn how to make and use a [custom data feed](/custom-feeds/build-and-deploy-feed) with infinite customization possibilities.

**Go Deeper:**

* Are you a power user that needs the fastest price updates and lots of queries? Learn how to set up a [Crossbar server](/tooling/crossbar).
* Dive into the [protocol](/how-it-works/switchboard-protocol) and [technology](/how-it-works/technical-architecture) that power Switchboard.


# Solana / SVM

Learn how to build and use programs that call existing Switchboard data feeds on Solana and other SVM platforms.

If you need to create a custom data feed, check out the [custom feeds section](/custom-feeds/build-and-deploy-feed).

## Accounts

| Account Type          | Address                                        |
| --------------------- | ---------------------------------------------- |
| Program ID            | `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv`  |
| Devnet Program ID     | `Aio4gaXjXzJNVLtzwtNVmSqGKpANtXhybbkhtAC94ji2` |
| Surge/Oracle Quotes   | `orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz`  |
| Default Mainnet Queue | `A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w` |
| Default Devnet Queue  | `EYiAmGSdsQTuCw413V5BzaruWuCCSDgTPtBGvLkXHbe7` |


# Price Feeds

Access to reliable, real-world data is essential for decentralised applications (dApps), particularly in Decentralised Finance (DeFi). Real-time asset prices, forming the backbone of any DeFi protocol, are among the most critical data points.

This is where Data Feeds come in. Think of them as secure bridges connecting the off-chain world of financial markets to your on-chain smart contracts. They provide a continuous stream of verified, aggregated price data for a wide range of assets, enabling your dApp to react to market fluctuations and operate correctly.

## In This Section

* [Basic Price Feed Tutorial](/docs-by-chain/solana-svm/price-feeds/basic-price-feed): Integrate managed, oracle-verified price feeds into a Solana program.
* [Advanced Price Feed Tutorial](/docs-by-chain/solana-svm/price-feeds/advanced-price-feed): Reduce compute costs with an authorized cranker pattern for oracle-backed quotes.
* [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts): Derive and read canonical quote-program accounts without relying on fixed offsets.
* [Authority-Updated Feeds](/docs-by-chain/solana-svm/price-feeds/authority-updated-feeds): Publish quote accounts directly from a trusted wallet or PDA when your application is the source of truth.

For new Solana/SVM feed-hash integrations, use the quote program: `queue.fetchManagedUpdateIxs(...)` writes canonical `OracleQuote` accounts derived from the queue and feed ID. The classic `PullFeed.fetchUpdateIx(...)` path is legacy compatibility only and requires queue/gateway support for classic PullFeed accounts.


# Basic Price Feed Tutorial

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

This tutorial walks you through the simplest way to integrate Switchboard oracle price feeds into your Solana program. You'll learn how to read verified price data using Switchboard's managed update system.

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

## What You'll Build

A minimal Anchor program that reads price feed data from a Switchboard oracle account, plus a TypeScript client that fetches fresh oracle data and calls your program.

## Prerequisites

* Rust and Cargo installed
* Solana CLI installed and configured
* Node.js 18+ and npm/pnpm
* Basic understanding of Anchor framework
* A Solana keypair with SOL (devnet or mainnet)

## Key Concepts

Before diving into the code, let's understand how Switchboard's managed update system works.

### Managed Updates

Switchboard uses a **managed update system** where oracle data is stored in canonical accounts derived deterministically from feed IDs. This means:

* No manual account management needed
* Same feed IDs always produce the same oracle account address
* Accounts are created automatically if they don't exist

### The Two-Instruction Pattern

Every Switchboard oracle update requires two instructions in sequence:

1. **Ed25519 Signature Verification** - Verifies the oracle operator's signature
2. **Quote Program Storage** - Stores the verified data in the canonical oracle account

Your program then reads from this oracle account as a third instruction in the same transaction.

This is the current path for new Solana/SVM feed-hash integrations. The older `PullFeed.fetchUpdateIx(...)` path writes classic PullFeed accounts and depends on queue/gateway support for that legacy flow. Use managed quote-program updates unless you are maintaining an existing classic PullFeed integration.

### Feed IDs

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

Example: BTC/USD feed ID:

```
4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
```

## The On-Chain Program

Here's the complete Anchor program that reads oracle data:

```rust
use anchor_lang::prelude::*;
use switchboard_on_demand::{
    SlotHashes, Instructions, default_queue, SwitchboardQuoteExt, SwitchboardQuote
};

declare_id!("9kVBXoCrvZgKYWTJ74w3S8wAp7daEB7zpG7kwiXxkCVN");

#[program]
pub mod basic_oracle_example {
    use super::*;

    /// Read and verify oracle data from the managed oracle account
    pub fn read_oracle_data(ctx: Context<ReadOracleData>) -> Result<()> {
        // Access the oracle data directly
        let feeds = &ctx.accounts.quote_account.feeds;

        // Calculate staleness (how old is the data?)
        let current_slot = ctx.accounts.sysvars.clock.slot;
        let quote_slot = ctx.accounts.quote_account.slot;
        let staleness = current_slot.saturating_sub(quote_slot);

        msg!("Number of feeds: {}", feeds.len());
        msg!("Quote slot: {}, Current slot: {}", quote_slot, current_slot);
        msg!("Staleness: {} slots", staleness);

        // Process each feed
        for (i, feed) in feeds.iter().enumerate() {
            msg!("Feed {}: ID = {}", i, feed.hex_id());
            msg!("Feed {}: Value = {}", i, feed.value());

            // Your business logic here!
            // - Store the price in your program state
            // - Trigger events based on price changes
            // - Use the price for calculations
        }

        msg!("Successfully read {} oracle feeds!", feeds.len());
        Ok(())
    }
}

/// Account context for reading oracle data
#[derive(Accounts)]
pub struct ReadOracleData<'info> {
    /// The canonical oracle account containing verified quote data
    /// The address constraint ensures this is the correct canonical account
    #[account(address = quote_account.canonical_key(&default_queue()))]
    pub quote_account: Box<Account<'info, SwitchboardQuote>>,

    /// System variables required for quote verification
    pub sysvars: Sysvars<'info>,
}

/// System variables required for oracle verification
#[derive(Accounts)]
pub struct Sysvars<'info> {
    pub clock: Sysvar<'info, Clock>,
    pub slothashes: Sysvar<'info, SlotHashes>,
    pub instructions: Sysvar<'info, Instructions>,
}
```

### Code Walkthrough

#### Imports

```rust
use switchboard_on_demand::{
    SlotHashes, Instructions, default_queue, SwitchboardQuoteExt, SwitchboardQuote
};
```

* `SwitchboardQuote` - The account type that holds oracle data
* `SwitchboardQuoteExt` - Extension trait for accessing feed values
* `default_queue()` - Returns the default Switchboard queue for the current network
* `SlotHashes`, `Instructions` - Sysvar types needed for verification

#### The Instruction

The `read_oracle_data` instruction:

1. **Accesses feed data** from `quote_account.feeds`
2. **Calculates staleness** by comparing the quote slot to the current slot
3. **Iterates through feeds** to extract values using `feed.hex_id()` and `feed.value()`

#### Account Validation

```rust
#[account(address = quote_account.canonical_key(&default_queue()))]
pub quote_account: Box<Account<'info, SwitchboardQuote>>,
```

This constraint ensures the passed account is the legitimate canonical oracle account for the contained feeds. It prevents malicious actors from passing fake oracle data.

#### Sysvars

The program requires three sysvars:

* `Clock` - For checking the current slot
* `SlotHashes` - For quote verification
* `Instructions` - For verifying the Ed25519 instruction was included

## The TypeScript Client

Here's the complete client code that fetches oracle data and calls your program:

```typescript
import * as sb from "@switchboard-xyz/on-demand";
import { OracleQuote } from "@switchboard-xyz/on-demand";

const FEED_ID = "4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812";

async function main() {
  // Step 1: Load environment (auto-detects network)
  const { program, keypair, connection, crossbar, queue } =
    await sb.AnchorUtils.loadEnv();

  console.log("Queue:", queue.pubkey.toBase58());
  console.log("Network:", crossbar.getNetwork());

  // Step 2: Derive the canonical oracle account from feed ID
  const [quoteAccount] = OracleQuote.getCanonicalPubkey(
    queue.pubkey,
    [FEED_ID]
  );
  console.log("Quote Account:", quoteAccount.toBase58());

  // Step 3: Simulate the feed to see current value
  const simResult = await crossbar.simulateFeed(FEED_ID);
  console.log("Simulated feed result:", simResult);

  // Step 4: Create managed update instructions
  const updateInstructions = await queue.fetchManagedUpdateIxs(
    crossbar,
    [FEED_ID],
    {
      variableOverrides: {},
      payer: keypair.publicKey,
    }
  );

  // Step 5: Create your program's instruction
  const readOracleIx = await program.methods
    .readOracleData()
    .accounts({
      quoteAccount: quoteAccount,
      // Sysvars are added automatically by Anchor
    })
    .instruction();

  // Step 6: Build and send the transaction
  const tx = await sb.asV0Tx({
    connection,
    ixs: [...updateInstructions, readOracleIx],
    signers: [keypair],
    computeUnitPrice: 20_000,
    computeUnitLimitMultiple: 1.1,
  });

  // Step 7: Simulate and send
  const sim = await connection.simulateTransaction(tx);
  console.log(sim.value.logs?.join("\n"));

  if (!sim.value.err) {
    const sig = await connection.sendTransaction(tx);
    console.log("Transaction:", sig);
  }
}

main();
```

### Client Code Walkthrough

#### Step 1: Load Environment

```typescript
const { program, keypair, connection, crossbar, queue } =
  await sb.AnchorUtils.loadEnv();
```

This auto-detects whether you're on mainnet or devnet based on your RPC endpoint and loads the appropriate Switchboard queue.

#### Step 2: Derive Canonical Account

```typescript
const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [FEED_ID]);
```

The canonical account is a PDA derived from the queue public key and feed IDs. This ensures the same inputs always produce the same account address.

#### Step 3: Simulate Feed (Optional)

```typescript
const simResult = await crossbar.simulateFeed(FEED_ID);
```

You can simulate a feed to see what value the oracle would return before submitting a transaction.

#### Step 4: Create Update Instructions

```typescript
const updateInstructions = await queue.fetchManagedUpdateIxs(
  crossbar,
  [FEED_ID],
  {
    variableOverrides: {},
    payer: keypair.publicKey,
  }
);
```

This returns an array of instructions:

1. Ed25519 signature verification instruction
2. Quote program `verified_update` instruction

#### Step 5-7: Build and Send Transaction

The transaction includes:

1. Ed25519 verification instruction
2. Quote program update instruction
3. Your program's instruction

All three must be in the same transaction for the verification to work.

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

### 2. Install Dependencies

```bash
npm install
```

### 3. Configure Your Environment

Create or update your Solana CLI config to point to devnet:

```bash
solana config set --url devnet
```

Ensure your keypair has SOL:

```bash
solana airdrop 2
```

### 4. Build and Deploy the Program

This step is optional only if you want to smoke-test the managed update flow by itself.

If you want the example to invoke `read_oracle_data` after the quote update, you must deploy the sample Anchor program first:

```bash
anchor build
anchor deploy
```

### 5. Run the Example

```bash
# Using default BTC/USD feed
npm run update

# Using a custom feed ID
npm run update -- --feedId YOUR_FEED_ID_HERE
```

`npm run update` always fetches a fresh managed update and submits the Switchboard transaction.

If the example program is not deployed, the script logs that it skipped the consumer-program step and only updates the quote account. If the program is deployed, the same command also appends the `read_oracle_data` instruction.

### Expected Output

If the example program is not deployed yet, you should see output like:

```
ℹ️  Skipping crank: basic_oracle_example program not deployed
✅ Transaction sent: 5c...
✅ Managed update confirmed
ℹ️  The quote account was updated without the example consumer instruction
```

With the example program deployed, you should also see logs like:

```
Queue: FdRnYujMnYbAJp5P2rkEYZCbF2TKs2D2yXZ7MYq89Hms
Network: devnet
Quote Account: 8Js7NsQ7sF3WLJN3JC4LJQGz8kHiEJwZ7sdGTtJC5J7d
Simulated feed result: { value: 97234.5, ... }
Number of feeds: 1
Quote slot: 123456789, Current slot: 123456790
Staleness: 1 slots
Feed 0: ID = 4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
Feed 0: Value = 97234.50
Successfully read 1 oracle feeds!
```

## Adding to Your Program

To integrate Switchboard into your own program:

### 1. Add Dependencies

In your `Cargo.toml`:

```toml
[dependencies]
switchboard-on-demand = { version = "0.13.0", features = ["anchor", "devnet"] }
```

The example program enables the `devnet` feature. If you are targeting a different Solana cluster, swap the cluster feature to match your deployment.

### 2. Add the Account Struct

```rust
use switchboard_on_demand::{
    SlotHashes, Instructions, default_queue, SwitchboardQuoteExt, SwitchboardQuote
};

#[derive(Accounts)]
pub struct YourInstruction<'info> {
    #[account(address = quote_account.canonical_key(&default_queue()))]
    pub quote_account: Box<Account<'info, SwitchboardQuote>>,

    pub clock: Sysvar<'info, Clock>,
    pub slothashes: Sysvar<'info, SlotHashes>,
    pub instructions: Sysvar<'info, Instructions>,

    // ... your other accounts
}
```

### 3. Read the Price

```rust
pub fn your_instruction(ctx: Context<YourInstruction>) -> Result<()> {
    let feeds = &ctx.accounts.quote_account.feeds;

    // Get the first feed's value
    let price = feeds[0].value();

    // Use the price in your logic
    // ...

    Ok(())
}
```

Quote accounts are variable-length. Do not read values by fixed byte offsets from one simulation result; parse them with `SwitchboardQuote` and `PackedFeedInfo`. See [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts).

## Troubleshooting

| Symptom                                                                                                                                                                              | What it means                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pullFeedSubmitResponseConsensus` or `PullFeed.fetchUpdateIx(...)` returns `ORACLE_UNAVAILABLE`, but `queue.fetchManagedUpdateIxs(...)` returns Ed25519 + quote-program instructions | The integration is using the legacy PullFeed path against quote-program infrastructure. Use managed quote-program updates and read the canonical quote account.                                                                     |
| Simulation succeeds, but signed update fetching fails with oracle validation errors such as `RangeExceeded`                                                                          | This is a feed validation issue, not a PullFeed-vs-quote-program issue. Check feed parameter units, especially raw v2 `maxJobRangePct`; see [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units). |

## Next Steps

* **Multiple Feeds**: Pass multiple feed IDs to `fetchManagedUpdateIxs` to update several prices in one transaction
* **Staleness Checks**: Add maximum staleness requirements for your use case
* **Quote Program Accounts**: Read canonical quote accounts safely in the [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts) guide
* **Authority-Updated Feeds**: Publish quotes from your own trusted wallet or PDA in the [Authority-Updated Feeds](/docs-by-chain/solana-svm/price-feeds/authority-updated-feeds) guide
* **Custom Feeds**: Learn how to create custom data feeds in the [Custom Feeds](/custom-feeds/build-and-deploy-feed) section
* **Advanced Patterns**: See the Advanced Price Feed tutorial for more complex integration patterns


# Advanced Price Feed Tutorial

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

This tutorial demonstrates how to build an ultra-optimized Switchboard oracle integration using the Pinocchio framework, achieving **\~90% reduction in compute units** compared to the basic Anchor implementation.

## Why Optimize Compute Units?

On Solana, **compute units directly impact transaction costs**. Every CU saved means lower fees for your users and operators. This matters especially for:

* **High-frequency oracle updates**: If you're cranking prices every few seconds, those CUs add up fast
* **Production DeFi protocols**: Lower costs improve margins and user experience
* **MEV-sensitive operations**: Smaller, faster transactions have competitive advantages

| Metric             | Basic (Anchor) | Advanced (Pinocchio) | Savings   |
| ------------------ | -------------- | -------------------- | --------- |
| Compute Units      | \~2,000 CU     | \~190 CU             | **\~90%** |
| Framework Overhead | High           | Minimal              | -         |
| Transaction Size   | Standard       | Optimized with LUTs  | \~90%     |

## What You'll Build

An ultra-optimized oracle program with:

* **Admin authorization** for trusted crankers
* **Four modular instructions**: init\_state, init\_oracle, crank, read
* **Direct oracle writes** bypassing expensive verification (for authorized users)
* **QuoteVerifier** for secure reads with staleness checks

## Prerequisites

* Completed the [Basic Price Feed](/docs-by-chain/solana-svm/price-feeds/basic-price-feed) tutorial
* Rust and Cargo installed
* Familiarity with Pinocchio framework concepts
* Solana CLI installed and configured
* A Solana keypair with SOL

## Key Concepts

### Pinocchio Framework

[Pinocchio](https://github.com/febo/pinocchio) is a zero-abstraction Solana program framework that provides:

* Direct syscall access without runtime overhead
* Zero-allocation account parsing
* `#[inline(always)]` instruction dispatch
* \~100 CU framework overhead vs \~2,000 CU for Anchor

### Admin Authorization Pattern

Instead of verifying every oracle update cryptographically (expensive), this pattern:

1. Stores an authorized signer in a **state account**
2. Only allows that signer to write oracle data
3. Uses `write_from_ix_unchecked` for direct writes

This trades decentralization for efficiency—suitable when you control the cranker.

### Modular Account Initialization

The program separates account creation into dedicated instructions:

* `init_state`: Creates the authorization state account
* `init_oracle`: Creates the quote storage account

This allows conditional initialization only when needed.

## The On-Chain Program

### Program Structure

Use the current Pinocchio and Switchboard dependencies:

```toml
[dependencies]
switchboard-on-demand = { version = "0.13.0", features = ["pinocchio", "devnet"] }
pinocchio = { version = "0.11.2", features = ["cpi"] }
solana-msg = "2.2.1"
```

```rust
use pinocchio::{entrypoint, AccountView, Address, ProgramResult};
use pinocchio::error::ProgramError;
use solana_msg::msg;
use switchboard_on_demand::{
    check_pubkey_eq, get_slot, Instructions, OracleQuote, QuoteVerifier
};

mod utils;
use utils::{init_quote_account_if_needed, init_state_account_if_needed};

entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Address,
    accounts: &mut [AccountView],
    instruction_data: &[u8],
) -> ProgramResult {
    match instruction_data[0] {
        0 => crank(program_id, accounts)?,    // Write oracle data
        1 => read(program_id, accounts)?,     // Read and verify
        2 => init_state(program_id, accounts)?, // Initialize state
        3 => init_oracle(program_id, accounts)?, // Initialize oracle account
        _ => return Err(ProgramError::InvalidInstructionData),
    }
    Ok(())
}
```

The instruction discriminator is a single byte—no Anchor discriminator overhead.

### Instruction 0: `crank`

The crank instruction writes oracle data for authorized signers:

```rust
pub fn crank(program_id: &Address, accounts: &mut [AccountView]) -> ProgramResult {
    let [quote, queue, state, payer, instructions_sysvar, _clock_sysvar]: &mut [AccountView; 6] =
        accounts.try_into().map_err(|_| ProgramError::NotEnoughAccountKeys)?;

    // Validate state account belongs to this program
    if !is_state_account(state, program_id) {
        msg!("Invalid state account");
        return Err(ProgramError::Custom(2));
    }

    // Check payer matches authorized signer stored in state
    let state_data = state.try_borrow()?;
    if !check_pubkey_eq(&state_data[..32], payer.address()) {
        return Err(ProgramError::Custom(1)); // UnauthorizedSigner
    }

    // DANGER: Only use this if you trust the signer!
    // Bypasses cryptographic verification for speed
    OracleQuote::write_from_ix_unchecked(&*instructions_sysvar, quote, queue.address(), 0);

    Ok(())
}
```

**Key points:**

* Uses `AccountView` accessors for low-overhead account reads and writes
* `write_from_ix_unchecked` directly writes oracle data without Ed25519 verification
* Only \~90 CU for the entire operation

### Instruction 1: `read`

The read instruction verifies and displays oracle data:

```rust
pub fn read(_program_id: &Address, accounts: &mut [AccountView]) -> ProgramResult {
    let [quote, queue, clock_sysvar, slothashes_sysvar, instructions_sysvar]: &mut [AccountView; 5] =
        accounts.try_into().map_err(|_| ProgramError::NotEnoughAccountKeys)?;

    let slot = get_slot(&*clock_sysvar);

    // Verify the oracle data with staleness check
    let quote_data = QuoteVerifier::new()
        .slothash_sysvar(&*slothashes_sysvar)
        .ix_sysvar(&*instructions_sysvar)
        .clock_slot(slot)
        .queue(&*queue)
        .max_age(30)  // Reject data older than 30 slots (~12 seconds)
        .verify_account(queue.address(), quote)
        .unwrap();

    msg!("Quote slot: {}", quote_data.slot());

    // Display each feed's data
    for (index, feed_info) in quote_data.feeds().iter().enumerate() {
        msg!("Feed #{}: {}", index + 1, feed_info.hex_id());
        msg!("Value: {}", feed_info.value());
    }

    Ok(())
}
```

**Key points:**

* Uses `QuoteVerifier` for secure verification
* `max_age(30)` ensures data freshness (30 slots ≈ 12 seconds)
* Iterates through all feeds in the quote

### Instruction 2: `init_state`

Initializes the authorization state account:

```rust
pub fn init_state(program_id: &Address, accounts: &mut [AccountView]) -> ProgramResult {
    let [state, payer, system_program]: &mut [AccountView; 3] =
        accounts.try_into().map_err(|_| ProgramError::NotEnoughAccountKeys)?;

    // Create the state account PDA
    init_state_account_if_needed(program_id, state, payer, system_program)?;

    // Store the authorized signer (payer becomes the admin)
    state.try_borrow_mut()?[..32].copy_from_slice(payer.address().as_ref());

    Ok(())
}
```

The state account stores a single 32-byte pubkey—the authorized cranker.

### Instruction 3: `init_oracle`

Initializes the oracle quote account:

```rust
pub fn init_oracle(program_id: &Address, accounts: &mut [AccountView]) -> ProgramResult {
    let [quote, queue, payer, system_program, instructions_sysvar]: &mut [AccountView; 5] =
        accounts.try_into().map_err(|_| ProgramError::NotEnoughAccountKeys)?;

    // Parse feed info from the Ed25519 instruction data
    let quote_data = Instructions::parse_ix_data_unverified(&*instructions_sysvar, 0)
        .map_err(|_| ProgramError::InvalidInstructionData)?;

    // Create the quote account as a PDA derived from queue + feed IDs
    init_quote_account_if_needed(
        program_id,
        quote,
        queue,
        payer,
        system_program,
        &quote_data,
    )?;

    Ok(())
}
```

## Account Initialization Helpers

The `utils.rs` module handles PDA derivation and account creation:

### Quote Account Initialization

```rust
pub const ORACLE_ACCOUNT_SIZE: usize = 8 + 32 + 1024; // discriminator + queue + data

pub fn init_quote_account_if_needed(
    program_id: &Address,
    oracle_account: &mut AccountView,
    queue_account: &mut AccountView,
    payer: &mut AccountView,
    system_program: &mut AccountView,
    oracle_quote: &OracleQuote,
) -> Result<(), ProgramError> {
    // Skip if already initialized
    if oracle_account.lamports() != 0 {
        msg!("Oracle account already initialized");
        return Ok(());
    }

    // Derive PDA from queue + feed IDs
    let feed_ids = oracle_quote.feed_ids();
    let mut seeds: Vec<&[u8]> = Vec::with_capacity(feed_ids.len() + 1);
    seeds.push(queue_account.address().as_ref());
    for feed_id in &feed_ids {
        seeds.push(feed_id.as_ref());
    }

    let (canonical_address, bump) = Address::find_program_address(&seeds, program_id);

    if canonical_address != *oracle_account.address() {
        return Err(ProgramError::InvalidArgument);
    }

    // Create account via CPI to system program
    // ... (invoke_signed with seeds)
}
```

### State Account Initialization

```rust
pub const STATE_ACCOUNT_SIZE: usize = 32; // Single pubkey

pub fn init_state_account_if_needed(
    program_id: &Address,
    state_account: &mut AccountView,
    payer: &mut AccountView,
    system_program: &mut AccountView,
) -> Result<(), ProgramError> {
    if state_account.lamports() != 0 {
        return Ok(());
    }

    // Derive PDA from "state" seed
    let (expected_key, bump) = Address::find_program_address(&[b"state"], program_id);

    if state_account.address() != &expected_key {
        return Err(ProgramError::InvalidArgument);
    }

    // Create account via CPI
    // ... (invoke_signed with ["state", bump] seeds)
}
```

## The TypeScript Client

The client handles initialization, quote fetching, and transaction building:

```typescript
import * as sb from "@switchboard-xyz/on-demand";
import { OracleQuote } from "@switchboard-xyz/on-demand";
import { PublicKey } from "@solana/web3.js";

const FEED_ID = "4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812";

async function main() {
  // Step 1: Load environment (auto-detects network)
  const { keypair, connection, crossbar, queue } = await sb.AnchorUtils.loadEnv();

  // Load your deployed program ID
  const advancedProgramId = new PublicKey("YOUR_PROGRAM_ID");

  // Step 2: Derive the canonical quote account
  // Note: includes program ID for program-owned accounts
  const [quoteAccount] = OracleQuote.getCanonicalPubkey(
    queue.pubkey,
    [FEED_ID],
    advancedProgramId  // Program ID for custom derivation
  );

  // Step 3: Fetch the Ed25519 quote instruction
  const quoteIx = await queue.fetchQuoteIx(crossbar, [FEED_ID], {
    variableOverrides: {},
  });

  // Step 4: Decode and inspect the quote
  const decodedQuote = OracleQuote.decode(quoteIx.data);
  console.log("Quote slot:", decodedQuote.slot.toString());
  console.log("Feeds:", decodedQuote.feeds.length);
  decodedQuote.feeds.forEach((feed, idx) => {
    console.log(`  Feed ${idx}: ${feed.value}`);
  });

  // Step 5: Check if accounts need initialization
  const [stateAccount] = PublicKey.findProgramAddressSync(
    [Buffer.from("state")],
    advancedProgramId
  );

  const instructions = [quoteIx];

  // Add init_state if needed
  const stateInfo = await connection.getAccountInfo(stateAccount);
  if (!stateInfo) {
    instructions.push(createInitStateIx(advancedProgramId, keypair.publicKey));
  }

  // Add init_oracle if needed
  const quoteInfo = await connection.getAccountInfo(quoteAccount);
  if (!quoteInfo) {
    instructions.push(createInitOracleIx(
      advancedProgramId, quoteAccount, queue.pubkey, keypair.publicKey
    ));
  }

  // Step 6: Add crank and read instructions
  instructions.push(createCrankIx(
    advancedProgramId, quoteAccount, queue.pubkey, stateAccount, keypair.publicKey
  ));
  instructions.push(createReadIx(advancedProgramId, quoteAccount, queue.pubkey));

  // Step 7: Build V0 transaction with priority fees
  const tx = await sb.asV0Tx({
    connection,
    ixs: instructions,
    signers: [keypair],
    computeUnitPrice: 10_000,
    computeUnitLimitMultiple: 1.1,
  });

  // Step 8: Simulate and send
  const sim = await connection.simulateTransaction(tx);
  console.log(sim.value.logs?.join("\n"));

  if (!sim.value.err) {
    const sig = await connection.sendTransaction(tx);
    console.log("Transaction:", sig);
  }
}
```

`OracleQuote.decode(...)` parses the Ed25519 quote instruction payload. Stored quote-program accounts are variable-length; read them with the Rust `SwitchboardQuote`/`PackedFeedInfo` account types instead of fixed offsets. See [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts).

## 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/solana/feeds/advanced
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Build and Deploy the Program

Unlike the basic example, the advanced program must be deployed:

```bash
# Build the Pinocchio program
cargo build-sbf --manifest-path programs/advanced-oracle-example/Cargo.toml

# Deploy to devnet
solana program deploy target/deploy/advanced_oracle_example.so
```

### 4. Run the Example

```bash
# Using default BTC/USD feed
npm run update

# Using a custom feed ID
npm run update -- --feedId YOUR_FEED_ID
```

### Expected Output

```
Network detected: devnet
Queue selected: FdRnYujMnYbAJp5P2rkEYZCbF2TKs2D2yXZ7MYq89Hms
Quote Account (derived): 8Js7NsQ7sF3WLJN3JC4LJQGz8kHiEJwZ7sdGTtJC5J7d

Decoded Oracle Quote:
  Version: 1
  Slot: 298234567
  Feeds:
    Feed 0:
      Feed Hash: 4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812
      Value: 97234.5
      Min Oracle Samples: 3

State account already initialized
Quote account already initialized
Transaction instruction count: 3
  - Ed25519 verification
  - crank
  - read

Performance Stats - Min: 45ms | Median: 52ms | Mean: 54.3ms | Count: 1
Quote slot: 298234567
Feed #1: 4cd1cad...
Value: 97234.5
```

## When to Use This Pattern

Use the advanced pattern when:

| Scenario                               | Recommendation |
| -------------------------------------- | -------------- |
| High-frequency cranking (sub-second)   | Advanced       |
| Compute-sensitive DeFi protocols       | Advanced       |
| You control the cranker infrastructure | Advanced       |
| Learning / prototyping                 | Basic          |
| Decentralized, trustless updates       | Basic          |
| Simple integrations                    | Basic          |

## Security Considerations

### `write_from_ix_unchecked` Risks

This function bypasses cryptographic verification. Only use it when:

1. **You control the signer**: The authorized cranker is your infrastructure
2. **You trust all transaction accounts**: Malicious accounts could corrupt data
3. **You have external monitoring**: Detect and respond to anomalies

### Admin Authorization Trade-offs

| Aspect           | Managed Updates (Basic)   | Admin Auth (Advanced) |
| ---------------- | ------------------------- | --------------------- |
| Trust Model      | Trustless (cryptographic) | Trusted admin         |
| Cost             | Higher CU                 | \~90% lower CU        |
| Decentralization | Anyone can update         | Only admin            |
| Security         | Ed25519 verified          | Admin key security    |

### Recommendations

1. **Rotate admin keys** periodically
2. **Use multisig** for production admin accounts
3. **Monitor for anomalies** in oracle values
4. **Consider hybrid approaches**: Admin writes + periodic cryptographic verification

## Next Steps

* **Authority-Updated Feeds**: Publish quotes from your own trusted wallet or PDA in the [Authority-Updated Feeds](/docs-by-chain/solana-svm/price-feeds/authority-updated-feeds) guide
* **Custom Feeds**: Learn to create custom data feeds in [Custom Feeds](/custom-feeds/build-and-deploy-feed)
* **Multiple Feeds**: Batch multiple price feeds in a single transaction
* **Randomness**: Explore verifiable randomness in the [Randomness Tutorial](/docs-by-chain/solana-svm/randomness)


# Quote Program Accounts

Solana/SVM feed-hash integrations use canonical quote-program accounts. These accounts are derived from the queue public key and feed ID, then written by managed updates from `queue.fetchManagedUpdateIxs(...)`.

Use this path for new custom feeds and feed-hash integrations. The older `PullFeed.fetchUpdateIx(...)` path targets classic PullFeed accounts and requires compatible legacy queue/gateway support.

## Derive The Account

```ts
import { OracleQuote } from "@switchboard-xyz/on-demand";

const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedId]);
```

The same feed ID and queue always derive the same quote account. If your program accepts a quote account, constrain it to the canonical address for the queue and feed ID.

## Assemble Managed Updates

Keep the Ed25519 and quote-program instructions returned by `fetchManagedUpdateIxs(...)` adjacent. `asV0Tx(...)` finalizes their position-dependent indices after the complete instruction order is known, so omit the legacy `instructionIdx` option:

```ts
const updateIxs = await queue.fetchManagedUpdateIxs(crossbar, [feedId], {
  payer: keypair.publicKey,
});

const tx = await asV0Tx({
  connection,
  ixs: [...setupIxs, ...updateIxs, consumerIx],
  signers: [keypair],
});
```

If you compile a transaction without `asV0Tx`, finalize the complete ordered array immediately before compilation:

```ts
import { finalizeManagedUpdateInstructions } from "@switchboard-xyz/on-demand";

const finalIxs = finalizeManagedUpdateInstructions([
  ...setupIxs,
  ...updateIxs,
  consumerIx,
]);
```

## Read Stored Data

Stored quote accounts are variable-length. Do not parse feed values by hard-coding byte offsets from a simulation or one account instance.

In Rust, use the SDK account types:

* `SwitchboardQuote`: stored quote-program account
* `PackedFeedInfo`: one feed result inside the quote
* `feeds_slice()` / `feeds()`: access feed results
* `feed_id` / `feed_id()`: 32-byte feed ID
* `feed_value` / `feed_value()`: raw `i128` value, scaled by Switchboard precision
* `value()`: decimal value helper
* `min_oracle_samples` / `min_oracle_samples()`: oracle-sample quorum recorded with the feed

```rust
use switchboard_on_demand::QuoteVerifier;

let quote = QuoteVerifier::new()
    .queue(&ctx.accounts.queue)
    .slothash_sysvar(&ctx.accounts.slothashes)
    .ix_sysvar(&ctx.accounts.instructions)
    .clock_slot(ctx.accounts.clock.slot)
    .max_age(50)
    .verify_account(&ctx.accounts.quote_account)?;

for feed in quote.feeds() {
    msg!("Feed {}: {}", feed.hex_id(), feed.value());
}
```

## JavaScript Decode Scope

`OracleQuote.decode(...)` parses the Ed25519 quote instruction payload used by managed updates. It is useful when inspecting a quote instruction before it is written on-chain.

It is not a stable raw account decoder for stored quote-program account data. For stored account parsing, use the Rust/on-chain `SwitchboardQuote` account types until a dedicated JavaScript account decoder is available.

## Troubleshooting

If `queue.fetchManagedUpdateIxs(...)` returns Ed25519 + quote-program instructions but `PullFeed.fetchUpdateIx(...)` or `pullFeedSubmitResponseConsensus` returns `ORACLE_UNAVAILABLE`, the integration is using the classic PullFeed path against quote-program infrastructure. Move the integration to managed quote-program updates and canonical quote accounts.

The classic PullFeed update helpers reject an unexpected median-response feed hash before constructing signature or submit instructions. Do not catch that error and substitute a different or default account.

Classic PullFeed remains available for existing integrations. With on-demand `3.10.6`, its update methods forward the exact on-chain scaled variance value. New feed-hash integrations should use managed quote-program updates.

This is separate from feed-parameter scaling errors. If simulation succeeds but signed updates fail with oracle validation errors such as `RangeExceeded`, check the feed parameter units, especially raw v2 `maxJobRangePct`.


# Authority-Updated Feeds

Authority-updated feeds let you publish Switchboard quote accounts on Solana using a trusted authority instead of Switchboard's oracle-signing flow.

In this model, the authority signs the update directly and `quote_program` stores the result in the same quote-account format used by Surge. The authority can be:

* a wallet
* a PDA signed by another Solana program with `invoke_signed`

Throughout this page, "authority-updated feed" means an authority-owned quote account in `quote_program`.

## When To Use This Feature

Use authority-updated feeds when your application is the source of truth for a value and you want to publish that value through Switchboard quote-account tooling.

Common examples:

* protocol-defined mark or fair values
* internal pricing models
* program-owned data streams published from a PDA
* integrations that want a stable quote-account interface without oracle signatures

Use the standard oracle-backed update path instead if you need Switchboard oracle verification and the corresponding trust model.

## What Gets Created

A single authority-updated quote account can contain one or more feed IDs and their latest values.

On the first successful update, `quote_program`:

* derives the quote PDA
* creates the quote account
* writes the payload

There is no separate initialize instruction. Creation happens on first write.

## Quote Account Identity

The quote account PDA is derived from:

```
quote_account = PDA(
  [
    b"AUTH",
    authority_pubkey,
    feed_id_1,
    feed_id_2,
    ...
  ],
  quote_program_id
)
```

Two consequences matter in practice:

* the authority is part of the quote stream identity
* feed order is part of the quote stream identity

These feed bundles produce different quote accounts:

```
[BTC, ETH]
[ETH, BTC]
```

Recommended practice:

* choose one canonical ordering and use it everywhere
* for human-labeled feeds, sort alphabetically by symbol or another stable label before building the payload
* keep the same ordered feed list when deriving the PDA, building the payload, and storing configuration in your app

If the order changes, the quote account address changes.

## High-Level Flow

### Wallet authority

1. Build an authority quote payload with the feed IDs, values, and slot.
2. Derive the quote PDA from the authority and the ordered feed list.
3. Send `FeedAuthorityUpdate`.
4. `quote_program` validates the signer, derivation, and slot progression.
5. The quote account is created on first use or updated if it already exists.

### PDA authority

1. Your program derives or receives its authority PDA.
2. Your program builds or receives the authority quote payload.
3. Your program CPI-calls `quote_program::FeedAuthorityUpdate`.
4. Your program signs the CPI with `invoke_signed`.
5. `quote_program` applies the same validation and writes the update.

## Accounts

`FeedAuthorityUpdate` expects:

1. `quote_account` - writable
2. `authority` - signer
3. `clock_sysvar`
4. `payer` - signer and rent payer on first use
5. `system_program`

Notes:

* `authority` can be a wallet or PDA
* `quote_account` is always a PDA owned by `quote_program`
* `payer` funds initialization, but the authority defines the stream identity

## Payload Format

Authority updates use a dedicated payload format:

```
[4 bytes]  scheme tag           = "AUTH"
[1 byte]   feed_count
[N bytes]  PackedFeedInfo[feed_count]
[8 bytes]  slot (u64 LE)
[1 byte]   version
[4 bytes]  tail discriminator   = "SBOD"
```

Each `PackedFeedInfo` is:

```
[32 bytes] feed_id
[16 bytes] feed_value (i128 LE)
[1 byte]   min_oracle_samples
```

Notes:

* `feed_id` is a 32-byte feed identifier
* `feed_value` is stored as signed `i128`
* `min_oracle_samples` is retained for compatibility with shared quote tooling; SDK helpers default it to `1`
* the payload must include at least one feed

## Validation Rules

`quote_program::FeedAuthorityUpdate` rejects the update unless all of the following are true:

* `authority` signed the transaction
* the payload parses as a valid authority quote payload
* the payload contains between `1` and `13` feed IDs
* the payload contains no duplicate feed IDs
* the provided `quote_account` matches the PDA derived from `AUTH + authority + ordered feed IDs`
* if the quote account already exists, the stored authority matches the signer
* the payload slot is strictly less than the current cluster slot at execution time
* the payload slot is greater than or equal to the last stored slot

The current feed-ID limit is:

* maximum feed count: `13`
* maximum feed-ID bytes used in PDA seeds: `416`

Practical slot guidance:

* fetch a recent slot before building the payload
* treat the slot as monotonic per quote account
* do not reuse an older slot after a newer update has already landed

## What Gets Stored On-Chain

Authority-backed quotes use the existing Switchboard quote-account layout.

For authority quotes:

* the first 32-byte namespace field stores the authority pubkey
* the quote data section stores the encoded authority payload
* shared decoders reconstruct the quote as `sourceScheme = "authority"`

For oracle-backed quotes, that same namespace field stores a queue pubkey instead. The layout stays compatible, but consumers must inspect the source scheme before applying trust assumptions.

## Reading And Trust

Authority-updated feeds are not oracle-verified quotes. Consumers should trust them only to the extent that they trust the configured authority.

When reading an authority quote:

* decode it through the normal quote readers or SDK helpers
* check that `sourceScheme` is `authority`
* treat the authority pubkey as the publisher identity
* do not assume oracle signatures or `QuoteVerifier`-style oracle trust guarantees

This model is a good fit when your application, service, or program is intentionally the publisher of record.

## TypeScript SDK Helpers

The TypeScript SDK exposes helpers in `javascript/on-demand/src/classes/oracleQuote.ts`:

* `OracleQuote.buildAuthorityQuotePayload(...)`
* `OracleQuote.deriveAuthorityQuotePubkey(...)`
* `OracleQuote.buildFeedAuthorityUpdateInstruction(...)`

Example:

```ts
import { OracleQuote } from "@switchboard-xyz/on-demand";

const authority = myAuthorityPubkey;
const payer = myWallet.publicKey;

const feeds = [
  { symbol: "BTC", feedHash: btcFeedId, value: 123_000000000000000000n },
  { symbol: "ETH", feedHash: ethFeedId, value: 456_000000000000000000n },
].sort((a, b) => a.symbol.localeCompare(b.symbol));

const payload = OracleQuote.buildAuthorityQuotePayload(
  feeds.map(({ feedHash, value }) => ({
    feedHash,
    value,
  })),
  recentSlot
);

const orderedFeedIds = feeds.map(({ feedHash }) => feedHash);

const [quoteAccount] = OracleQuote.deriveAuthorityQuotePubkey(
  authority,
  orderedFeedIds
);

const ix = OracleQuote.buildFeedAuthorityUpdateInstruction({
  authority,
  payer,
  payload,
  quoteAccount,
});
```

If you set `minOracleSamples` explicitly, treat it as an unscaled oracle-sample count and use the same ordered feed list for both payload construction and PDA derivation.

## Rust SDK Helpers

The Rust SDK exposes the same concepts in `switchboard-on-demand`:

* `build_authority_quote_payload(...)`
* `derive_authority_quote_pubkey(...)`
* `build_feed_authority_update_instruction(...)`

These helpers are useful for:

* off-chain Rust clients
* integration tests
* Solana programs preparing CPI inputs

## Best Practices

* Pick a canonical feed ordering before you publish the first update.
* Alphabetical ordering by symbol is a good default when feeds have stable human-readable names.
* If you only work with raw feed IDs, sort them by a stable application-level rule and never change it.
* Persist the authority, ordered feed list, and derived quote account together in your configuration.
* Keep feed bundles small and avoid duplicate feed IDs.
* Separate authority-backed handling from oracle-backed handling in downstream code.
* Only publish or consume authority quotes in places where the authority is an acceptable trust anchor.

## Summary

Authority-updated feeds let a wallet or PDA publish Switchboard quote accounts directly into Solana `quote_program`.

Key properties:

* the authority owns the quote stream identity
* feed order matters
* first write initializes the account
* the current limit is `13` feed IDs per quote
* shared readers decode these quotes as `sourceScheme = "authority"`

Use this flow when you want Switchboard-compatible quote accounts but the data should be trusted because of your authority signer, not because of Switchboard oracle signatures.


# Surge Price Feeds

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

## The Future of Oracle Technology

Switchboard Surge is the industry's fastest oracle data delivery system, providing sub-100ms latency through direct WebSocket streaming. Built for the next generation of DeFi applications, trading systems, and real-time dashboards.

## Key Innovation

Traditional oracles require multiple steps—gathering prices, writing to blockchain state, reaching consensus, and then making data available—resulting in 2-10 seconds of latency.

Switchboard oracles must pass a hardware proof when joining the network, ensuring they run only verified Switchboard code. This allows oracles to stream price data directly from sources to your application via WebSocket, achieving sub-100ms latency.

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Price Sources   │────▶│  Oracle Network  │────▶│  Surge Gateway   │
│   (CEX, DEX)     │     │ (SAIL Verified)  │     │   (WebSocket)    │
└──────────────────┘     └──────────────────┘     └────────┬─────────┘
                                                          │
                                               ┌──────────▼──────────┐
                                               │   Your Application  │
                                               │  • Event Listeners  │
                                               │  • Price Handlers   │
                                               │  • Oracle Quotes    │
                                               └─────────────────────┘
```

## Key Features

**Unmatched Performance** — Sub-100ms latency with direct WebSocket streaming and event-driven updates. No polling required.

**Zero Setup** — No data feed accounts, on-chain deployment, or SOL funding needed. Just use your keypair and connection to start streaming.

**Cost Efficiency** — Subscription-based pricing with no gas fees for receiving updates. Reduced on-chain costs when converting to Oracle Quotes.

**Seamless Integration** — TypeScript/JavaScript SDK, WebSocket API for any language, and Oracle Quote conversion for on-chain use.

**Enterprise-Grade Reliability** — 99.9% uptime SLA with global infrastructure, automatic failover, and professional support.

## User Flow

Surge works the same way regardless of your target chain:

1. **Subscribe** — All Surge subscriptions are managed on Solana, regardless of which chain you're building on. Connect your Solana wallet at the [subscription portal](https://explorer.switchboardlabs.xyz/subscriptions).
2. **Authenticate** — The SDK authenticates your session by signing with your Solana keypair. If the keypair does not have an active subscription, `connectAndSubscribe` will fail.
3. **Stream Prices** — Once subscribed, prices stream directly to your application via WebSocket. No on-chain reads required—this is what enables sub-100ms latency.
4. **Use Prices** — When you need prices on-chain, convert the Surge update to your chain's format and submit it. Switchboard provides SDKs for Solana, EVM, and Sui.

## Getting Started

### 1. Subscribe

Connect your wallet and subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions). If you are an AI agent or wish to subscribe programmatically rather than through the UI, see the [Surge Subscription Guide](/ai-agents-llms/surge-subscription-guide).

### 2. Install the SDK

```bash
npm install @switchboard-xyz/on-demand@3.10.6
# or
yarn add @switchboard-xyz/on-demand@3.10.6
```

### 3. Connect and Stream

```typescript
import * as sb from "@switchboard-xyz/on-demand";

// Initialize with keypair and connection (uses on-chain subscription)
const surge = new sb.Surge({ connection, keypair });
// `connection` is a Solana RPC Connection from @solana/web3.js.

// Discover available feeds
const availableFeeds = await surge.getSurgeFeeds();
console.log(`${availableFeeds.length} feeds available`);

// Subscribe to specific feeds
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'ETH/USD' },
  { symbol: 'SOL/USD' }
]);

// Handle price updates
surge.on('signedPriceUpdate', (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();
  metrics.perFeedMetrics.forEach((feed) => {
    console.log(`${feed.symbol}: ${prices[feed.feed_hash]}`);
  });
});
```

## Pricing & Limits

| Plan           | Price       | Quote Interval | Max Feeds | Max Connections |
| -------------- | ----------- | -------------- | --------- | --------------- |
| **Plug**       | Free        | 10s            | 2         | 1               |
| **Pro**        | \~$3,000/mo | 450ms          | 100       | 10              |
| **Enterprise** | \~$7,500/mo | 0ms            | 300       | 15              |

Subscriptions are paid in SWTCH tokens. For custom limits or dedicated support, contact <sales@switchboard.xyz>.

## Primary Use Cases

### Perpetual Exchanges

Surge is the perfect oracle solution for perpetual trading platforms:

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const market = this.markets.get(feed.symbol);

    // Update mark price instantly
    market.oraclePrice = price;

    // Trigger liquidations if needed
    const underwaterPositions = await this.findUnderwaterPositions(market);
    for (const position of underwaterPositions) {
      if (this.isLiquidatable(position, market.oraclePrice)) {
        const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
        await this.liquidatePosition(position, crankIxs);
      }
    }
  }
});
```

### Oracle-Based AMMs

Build the next generation of AMMs that use real-time oracle prices:

```typescript
class OracleAMM {
  private latestUpdate: sb.SurgeUpdate;

  constructor(private surge: sb.Surge) {
    surge.on('signedPriceUpdate', this.handlePriceUpdate.bind(this));
  }

  async handlePriceUpdate(response: sb.SurgeUpdate) {
    const metrics = response.getLatencyMetrics();
    if (metrics.isHeartbeat) return;

    this.latestUpdate = response;
    const prices = response.getFormattedPrices();

    for (const feed of metrics.perFeedMetrics) {
      const pair = this.pairs.get(feed.symbol);
      pair.oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
      pair.lastUpdate = Date.now();
    }
  }

  async executeSwap(tokenIn: string, tokenOut: string, amountIn: number) {
    const pair = `${tokenIn}/${tokenOut}`;
    const latestPrice = this.pairs.get(pair).oraclePrice;
    const amountOut = amountIn * latestPrice * (1 - this.swapFee);

    // Convert to Oracle Quote for on-chain execution
    const crankIxs = this.latestUpdate.toQuoteIx(queue.pubkey, keypair.publicKey);

    return await this.program.methods
      .swap(amountIn, amountOut)
      .accounts({ amm: this.ammPda, queue: this.queuePubkey })
      .preInstructions(crankIxs)
      .rpc();
  }
}
```

### High-Frequency Trading & Arbitrage

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const dexPrice = await getDexPrice(feed.symbol);

    const spread = Math.abs(dexPrice - oraclePrice) / oraclePrice;
    if (spread > MIN_PROFIT_THRESHOLD) {
      const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
      await executeArbitrage(crankIxs, calculateOptimalSize(spread));
    }
  }
});
```

### Liquidation Engines

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const positions = await getPositionsByCollateral(feed.symbol);

    for (const position of positions) {
      const ltv = calculateLTV(position, price);
      if (ltv > LIQUIDATION_THRESHOLD) {
        const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
        await liquidatePosition(position, crankIxs);
      }
    }
  }
});
```

## Technical Specifications

### Program ID

```
orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz
```

### Latency Breakdown

* Oracle processing: \~10ms
* Network transmission: \~20-50ms
* Client processing: \~10ms
* **Total: <100ms**

### Discovering Available Feeds

Use the `getSurgeFeeds()` method to see all available trading pairs:

```typescript
const surge = new sb.Surge({ connection, keypair });
// `connection` is a Solana RPC Connection from @solana/web3.js.
const feeds = await surge.getSurgeFeeds();

feeds.forEach(feed => {
  console.log(`${feed.symbol}`);
});
```

### Supported Assets

* All major cryptocurrency pairs
* Multiple exchange sources available
* New pairs added regularly
* Custom feeds available on request

Note: Surge does not support custom feeds created with the [feed builder](https://explorer.switchboardlabs.xyz/feed-builder).

## FAQ

### How is Surge different from traditional oracles?

Surge streams data directly to your application via WebSocket, bypassing the blockchain entirely for reads. This eliminates gas costs and reduces latency from seconds to milliseconds.

### Can I use Surge data on-chain?

Yes! Surge updates can be converted to Oracle Quote format for on-chain use: `response.toQuoteIx(queue.pubkey, keypair.publicKey)`

### What's the reliability?

Surge operates with 99.9% uptime SLA, automatic failover, and global redundancy. Enterprise customers get dedicated infrastructure.

### How do I handle disconnections?

The SDK includes automatic reconnection logic with exponential backoff. Your application will seamlessly recover from network interruptions.

## Next Steps

* [Surge Tutorial](/docs-by-chain/solana-svm/surge/surge-tutorial) - Step-by-step implementation guide
* [Crossbar Gateway](/tooling/crossbar) - Stream prices to your frontend
* [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol) - Advanced HTTP + WebSocket protocol
* [Explore code examples](https://github.com/switchboard-xyz/sb-on-demand-examples)
* [Join our Discord](https://discord.gg/TJAv6ZYvPC)


# Surge Tutorial

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

This tutorial walks you through implementing Switchboard Surge for real-time price streaming on Solana.

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

## Prerequisites

* Node.js 18+
* Solana keypair with an active Surge subscription ([subscribe here](https://explorer.switchboardlabs.xyz/subscriptions))
* Basic TypeScript knowledge

## Installation

```bash
npm install @switchboard-xyz/on-demand@3.10.6
# or
yarn add @switchboard-xyz/on-demand@3.10.6
```

## Basic Implementation

Connect to Surge and stream real-time prices:

```typescript
import * as sb from "@switchboard-xyz/on-demand";

// Load environment (keypair, connection, queue, etc.)
const { keypair, connection, queue } = await sb.AnchorUtils.loadEnv();

// Initialize Surge client with keypair and connection
const surge = new sb.Surge({ connection, keypair });

// Auth note: the SDK signs with your keypair to authenticate the session.
// If the keypair has no active Surge subscription, connectAndSubscribe will fail.

// Discover available feeds
const availableFeeds = await surge.getSurgeFeeds();
console.log(`Found ${availableFeeds.length} available feeds`);

// Subscribe to price feeds
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'SOL/USD' },
]);

// Handle real-time updates
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();

  // Skip heartbeat messages
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  metrics.perFeedMetrics.forEach((feed) => {
    console.log(`${feed.symbol}: ${prices[feed.feed_hash]}`);
    console.log(`  Source → Oracle: ${feed.sourceToOracleMs}ms`);
    console.log(`  Emit Latency: ${feed.emitLatencyMs}ms`);
  });
});
```

## Converting to Oracle Quotes

When you need to use Surge prices on-chain, convert them to Oracle Quotes:

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  // Convert Surge update to on-chain instructions
  const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);

  // Build transaction with oracle quote update
  const tx = await sb.asV0Tx({
    connection,
    ixs: [
      ...crankIxs,
      await program.methods
        .yourInstruction()
        .accounts({ /* ... */ })
        .instruction()
    ],
    signers: [keypair],
    computeUnitPrice: 20_000,
    computeUnitLimitMultiple: 1.3,
  });

  await connection.sendTransaction(tx);
});
```

## Streaming to Frontend with Crossbar

Crossbar is Switchboard's local gateway service for streaming prices to frontend applications.

### Setting Up Crossbar

```bash
# Using Docker Compose (recommended)
git clone https://github.com/switchboard-xyz/crossbar
cd crossbar
docker-compose up -d

# Crossbar will be available at:
# HTTP: http://localhost:8080
# WebSocket: ws://localhost:8080/ws
```

### React Integration

```typescript
import { useEffect, useState } from 'react';

interface PriceData {
  symbol: string;
  price: number;
  source_ts_ms: number;
  feedHash: string;
}

export function PriceFeed({ symbol }: { symbol: string }) {
  const [priceData, setPriceData] = useState<PriceData | null>(null);

  useEffect(() => {
    const websocket = new WebSocket('ws://localhost:8080/ws');

    websocket.onopen = () => {
      websocket.send(JSON.stringify({
        type: 'subscribe',
        feeds: [symbol]
      }));
    };

    websocket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'price_update' && data.symbol === symbol) {
        setPriceData({
          symbol: data.symbol,
          price: data.price,
          source_ts_ms: data.source_ts_ms,
          feedHash: data.feedHash
        });
      }
    };

    return () => websocket.close();
  }, [symbol]);

  if (!priceData) return <div>Loading...</div>;

  return (
    <div className="price-feed">
      <h3>{priceData.symbol}</h3>
      <div className="price">${priceData.price.toFixed(2)}</div>
      <div className="latency">
        Latency: {Date.now() - priceData.source_ts_ms}ms
      </div>
    </div>
  );
}
```

## Use Case Examples

### Perpetual Exchange

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const market = perpetuals.get(feed.symbol);

    // Update mark price instantly
    market.markPrice = price;

    // Check liquidations with latest price
    const underwaterPositions = await market.checkLiquidations(price);
    for (const position of underwaterPositions) {
      const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
      await liquidatePosition(position, crankIxs);
    }
  }
});
```

### Arbitrage Bot

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const dexPrice = await getDexPrice(feed.symbol);

    const spread = Math.abs(dexPrice - oraclePrice) / oraclePrice;
    if (spread > MIN_PROFIT_THRESHOLD) {
      const crankIxs = response.toQuoteIx(queue.pubkey, keypair.publicKey);
      await executeArbitrageTrade(crankIxs, calculateOptimalSize(spread));
    }
  }
});
```

## Error Handling

```typescript
surge.on('error', (error) => {
  console.error('Surge error:', error);
});

surge.on('close', () => {
  console.log('Connection closed');
  // SDK handles automatic reconnection
});
```

## Next Steps

* Explore [code examples](https://github.com/switchboard-xyz/sb-on-demand-examples)
* Learn about [Crossbar gateway](/tooling/crossbar)
* Join our [Discord](https://discord.gg/TJAv6ZYvPC) for support


# Prediction Market

Prediction markets allow users to trade on the outcomes of future events, creating powerful mechanisms for aggregating information and forecasting. These markets require reliable oracle data to resolve outcomes fairly and transparently.

Switchboard enables prediction market protocols to access verified real-world data for market resolution, ensuring trustless and accurate settlement of positions.


# Prediction Market Tutorial

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

This tutorial demonstrates how to **verify oracle feed configurations on-chain** using Kalshi prediction market data. You'll learn a critical security pattern that prevents oracle substitution attacks.

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

## The Problem: Oracle Substitution Attacks

When using oracle data in your program, how do you know the oracle is fetching data from the sources you expect? A malicious actor could:

1. Create a similar-looking oracle feed with different (manipulated) data sources
2. Pass that feed to your program
3. Exploit your program with incorrect data

**Example Attack:**

* Your program expects BTC price from Binance + Coinbase
* Attacker creates a feed that looks similar but fetches from a manipulated source
* Your liquidation logic uses the wrong price

## The Solution: Feed ID Verification

Switchboard feed IDs are **deterministic SHA-256 hashes** of the feed's protobuf definition:

```
Feed Definition → Protobuf Encoding → SHA-256 Hash → Feed ID
```

By recreating the expected feed configuration on-chain and comparing its hash to the oracle's feed ID, you cryptographically prove the oracle uses exactly the data sources you expect.

## What You'll Build

A Solana program that:

1. Receives oracle data for a Kalshi prediction market order
2. Recreates the expected feed configuration on-chain
3. Verifies the feed ID matches before trusting the data

## Prerequisites

* Rust and Cargo installed
* Anchor framework familiarity
* Solana CLI installed and configured
* Kalshi API credentials (API key ID and private key)

## Key Concepts

### Feed ID Derivation

Feed IDs are derived by:

1. Constructing an `OracleFeed` protobuf message
2. Encoding it as length-delimited bytes
3. Computing SHA-256 hash

```rust
let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);
let feed_id = hash(&bytes).to_bytes();
```

### QuoteVerifier

The `QuoteVerifier` uses a builder pattern to verify Ed25519 signatures from oracle operators:

```rust
let quote = QuoteVerifier::new()
    .queue(queue_account)
    .slothash_sysvar(slothashes)
    .ix_sysvar(instructions)
    .clock_slot(current_slot)
    .verify_instruction_at(0)?;
```

### Variable Overrides

Kalshi requires authentication. Variables like `${KALSHI_API_KEY_ID}` are placeholders that get replaced at runtime when fetching the quote:

```typescript
const quote = await queue.fetchQuoteIx(crossbar, [feed], {
  variableOverrides: {
    KALSHI_API_KEY_ID: "your-key-id",
    KALSHI_SIGNATURE: signature,
    KALSHI_TIMESTAMP: timestamp,
  },
});
```

## The On-Chain Program

### Dependencies

```toml
[dependencies]
anchor-lang = "0.31.1"
switchboard-on-demand = { version = "0.13.0", features = ["anchor", "devnet"] }
switchboard-protos = { version = "0.2.6", features = ["serde"] }
prost = "0.13"
solana-program = ">=2,<3"
faster-hex = "0.10.0"
```

> **Note:** The current example program uses `switchboard-on-demand 0.13.0` with the `anchor` and `devnet` features for the `anchor-lang 0.31.1` toolchain.

### Program Structure

```rust
use anchor_lang::prelude::*;
use switchboard_on_demand::{SlotHashes, Instructions, QuoteVerifier};
use switchboard_protos::OracleFeed;
use switchboard_protos::OracleJob;
use switchboard_protos::oracle_job::oracle_job::{KalshiApiTask, JsonParseTask, Task};
use switchboard_protos::oracle_job::oracle_job::task;
use switchboard_on_demand::QueueAccountData;
use switchboard_on_demand::default_queue;
use prost::Message;
use solana_program::hash::hash;

declare_id!("YOUR_PROGRAM_ID");

#[program]
pub mod prediction_market {
    use super::*;

    pub fn verify_kalshi_feed(
        ctx: Context<VerifyFeed>,
        order_id: String,
    ) -> Result<()> {
        // Step 1: Create QuoteVerifier with builder pattern
        let mut verifier = QuoteVerifier::new();
        verifier
            .queue(ctx.accounts.queue.as_ref())
            .slothash_sysvar(ctx.accounts.slothashes.as_ref())
            .ix_sysvar(ctx.accounts.instructions.as_ref())
            .clock_slot(Clock::get()?.slot);

        // Step 2: Verify the Ed25519 instruction at index 0
        let quote = verifier.verify_instruction_at(0).unwrap();

        // Step 3: Extract feed ID from verified quote
        let feeds = quote.feeds();
        require!(!feeds.is_empty(), ErrorCode::NoOracleFeeds);

        let feed = &feeds[0];
        let actual_feed_id = feed.feed_id();

        // Step 4: Recreate expected feed ID and verify match
        require!(
            *actual_feed_id == create_kalshi_feed_id(&order_id)?,
            ErrorCode::FeedMismatch
        );

        msg!("Feed ID verification successful!");
        msg!("Feed ID: {}", faster_hex::hex_string(actual_feed_id));
        msg!("Order ID: {}", order_id);

        Ok(())
    }
}
```

### Feed ID Recreation

The critical function that recreates the expected feed configuration:

```rust
fn create_kalshi_feed_id(order_id: &str) -> Result<[u8; 32]> {
    // Build the Kalshi API URL
    let url = format!(
        "https://api.elections.kalshi.com/trade-api/v2/portfolio/orders/{}",
        order_id
    );

    // Construct the exact feed definition
    let feed = OracleFeed {
        name: Some("Kalshi Order Price".to_string()),
        jobs: vec![
            OracleJob {
                tasks: vec![
                    // Task 1: Fetch from Kalshi API
                    Task {
                        task: Some(task::Task::KalshiApiTask(KalshiApiTask {
                            url: Some(url.clone()),
                            api_key_id: Some("${KALSHI_API_KEY_ID}".to_string()),
                            signature: Some("${KALSHI_SIGNATURE}".to_string()),
                            timestamp: Some("${KALSHI_TIMESTAMP}".to_string()),
                            ..Default::default()
                        })),
                    },
                    // Task 2: Parse JSON response
                    Task {
                        task: Some(task::Task::JsonParseTask(JsonParseTask {
                            path: Some("$.order.yes_price_dollars".to_string()),
                            ..Default::default()
                        })),
                    },
                ],
                weight: None,
            }
        ],
        min_job_responses: Some(1), // unscaled job/source quorum
        min_oracle_samples: Some(1), // unscaled oracle/signature quorum
        max_job_range_pct: Some(0), // Intentional for this single-source verification; use a positive scaled value for normal multi-source feeds.
    };

    // Encode as protobuf and hash
    let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);
    Ok(hash(&bytes).to_bytes())
}
```

### Account Context

```rust
#[derive(Accounts)]
pub struct VerifyFeed<'info> {
    /// The Switchboard queue - must be the default queue
    #[account(address = default_queue())]
    pub queue: AccountLoader<'info, QueueAccountData>,

    /// SlotHashes sysvar for signature verification
    pub slothashes: Sysvar<'info, SlotHashes>,

    /// Instructions sysvar for Ed25519 verification
    pub instructions: Sysvar<'info, Instructions>,
}

#[error_code]
pub enum ErrorCode {
    #[msg("No oracle feeds available")]
    NoOracleFeeds,

    #[msg("Feed hash mismatch - oracle feed does not match expected configuration")]
    FeedMismatch,
}
```

## The TypeScript Client

### Kalshi Authentication

Kalshi uses RSA-PSS-SHA256 signatures for API authentication:

```typescript
import * as crypto from "crypto";
import * as fs from "fs";

function loadPrivateKey(keyPath: string): crypto.KeyObject {
  const privateKeyPem = fs.readFileSync(keyPath, "utf8");
  return crypto.createPrivateKey(privateKeyPem);
}

function createSignature(
  privateKey: crypto.KeyObject,
  timestamp: string,
  method: string,
  path: string
): string {
  const message = `${timestamp}${method}${path}`;
  const messageBuffer = Buffer.from(message, "utf8");

  const signature = crypto.sign("sha256", messageBuffer, {
    key: privateKey,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
    saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
  });

  return signature.toString("base64");
}
```

### Complete Client Flow

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

async function verifyKalshiFeed(
  apiKeyId: string,
  privateKeyPath: string,
  orderId: string
) {
  // Step 1: Load Switchboard environment
  const { program, keypair, connection } = await sb.AnchorUtils.loadEnv();
  const queue = await sb.Queue.loadDefault(program!);
  const crossbar = new CrossbarClient("https://crossbar.switchboardlabs.xyz");

  // Step 2: Create Kalshi authentication
  const privateKey = loadPrivateKey(privateKeyPath);
  const method = "GET";
  const path = `/trade-api/v2/portfolio/orders/${orderId}`;
  const timestamp = Date.now().toString();
  const signature = createSignature(privateKey, timestamp, method, path);

  // Step 3: Define the oracle feed
  const oracleFeed = {
    name: "Kalshi Order Price",
    minJobResponses: 1, // unscaled job/source quorum
    minOracleSamples: 1, // unscaled oracle/signature quorum
    maxJobRangePct: 0, // Intentional for this single-source verification; use a positive scaled value for normal multi-source feeds.
    jobs: [
      {
        tasks: [
          {
            kalshiApiTask: {
              url: `https://api.elections.kalshi.com${path}`,
              apiKeyId: "${KALSHI_API_KEY_ID}",
              signature: "${KALSHI_SIGNATURE}",
              timestamp: "${KALSHI_TIMESTAMP}",
            },
          },
          {
            jsonParseTask: {
              path: "$.order.yes_price_dollars",
            },
          },
        ],
      },
    ],
  };

  // Step 4: Simulate the feed (optional, for testing)
  const simulation = await crossbar.simulateFeed(oracleFeed, true, {
    KALSHI_SIGNATURE: signature,
    KALSHI_TIMESTAMP: timestamp,
    KALSHI_API_KEY_ID: apiKeyId,
  });
  console.log("Simulated value:", simulation.results[0]);

  // Step 5: Fetch quote instruction with credentials
  const quoteIx = await queue.fetchQuoteIx(crossbar, [oracleFeed], {
    numSignatures: 1,
    variableOverrides: {
      KALSHI_SIGNATURE: signature,
      KALSHI_TIMESTAMP: timestamp,
      KALSHI_API_KEY_ID: apiKeyId,
    },
  });

  // Step 6: Create verification instruction
  const verifyIx = await yourProgram.methods
    .verifyKalshiFeed(orderId)
    .accounts({
      queue: queue.pubkey,
      slothashes: sb.SYSVAR_SLOTHASHES_PUBKEY,
      instructions: sb.SYSVAR_INSTRUCTIONS_PUBKEY,
    })
    .instruction();

  // Step 7: Build and send transaction
  const tx = await sb.asV0Tx({
    connection,
    ixs: [quoteIx, verifyIx],
    signers: [keypair],
    computeUnitPrice: 20_000,
    computeUnitLimitMultiple: 1.1,
  });

  const sim = await connection.simulateTransaction(tx);
  console.log("Verification logs:", sim.value.logs);
}
```

## 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/solana/prediction-market
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Build and Deploy the Program

```bash
anchor build
anchor deploy --provider.cluster devnet
```

### 4. Get Kalshi API Credentials

1. Sign up at [Kalshi](https://kalshi.com)
2. Generate API credentials in your account settings
3. Download your private key PEM file

### 5. Run the Verification

```bash
npm run start -- \
  --api-key-id YOUR_API_KEY_ID \
  --private-key-path /path/to/kalshi/private-key.pem \
  --order-id YOUR_ORDER_ID
```

### Expected Output

```
Kalshi Feed Verification Test
==================================

Configuration:
  API Key ID: abc123...
  Order ID: 12345678-1234-1234-1234-123456789012
  Crossbar URL: https://crossbar.switchboardlabs.xyz

Solana Configuration:
  Wallet: 7Js...
  Queue: FdRn...

Creating Oracle Feed Definition...
Simulating Feed with Crossbar...
  Simulation Result: 0.65...

Fetching quote instruction...
  Successfully fetched quote instruction

Transaction Simulated

Transaction Simulation Logs:
[
  "Program YOUR_PROGRAM_ID invoke",
  "Feed ID verification successful!",
  "Feed ID: 4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812",
  "Order ID: 12345678-1234-1234-1234-123456789012",
  "Program YOUR_PROGRAM_ID success"
]
```

## Use Cases

### 1. Prediction Market Settlement

Before settling prediction market positions, verify the oracle is using the correct data source:

```rust
// Verify feed configuration before settlement
verify_kalshi_feed(ctx, order_id)?;

// Now safe to use the oracle value
let price = feeds[0].value();
settle_positions(price)?;
```

### 2. Conditional Payments

Release funds only when verified oracle data meets conditions:

```rust
verify_kalshi_feed(ctx, order_id)?;

let yes_price = feeds[0].value();
if yes_price > threshold {
    release_funds()?;
}
```

### 3. DeFi Protocol Integration

Verify oracle configuration before using prices for:

* Liquidations
* Collateral calculations
* Interest rate adjustments

### 4. Compliance & Audit Trails

Prove on-chain that specific data sources were used:

```rust
emit!(FeedVerified {
    feed_id: actual_feed_id,
    order_id: order_id,
    timestamp: Clock::get()?.unix_timestamp,
});
```

## Security Best Practices

### Always Verify Feed Configuration

```rust
// Good: Verify before trusting data
require!(
    *actual_feed_id == create_kalshi_feed_id(&order_id)?,
    ErrorCode::FeedMismatch
);
let price = feeds[0].value();

// Bad: Trust without verification
let price = feeds[0].value(); // Dangerous!
```

### Validate Queue Account

```rust
// Good: Ensure data comes from trusted Switchboard queue
#[account(address = default_queue())]
pub queue: AccountLoader<'info, QueueAccountData>,
```

### Use QuoteVerifier

```rust
// Good: Cryptographically verify oracle signatures
let quote = QuoteVerifier::new()
    .queue(queue)
    .slothash_sysvar(slothashes)
    .ix_sysvar(instructions)
    .clock_slot(slot)
    .verify_instruction_at(0)?;
```

## Extending the Pattern

### Generic HTTP APIs

```rust
Task {
    task: Some(task::Task::HttpTask(HttpTask {
        url: Some("https://api.example.com/data".to_string()),
        method: Some(Method::Get as i32),
        ..Default::default()
    })),
}
```

### Polymarket Integration

```rust
Task {
    task: Some(task::Task::HttpTask(HttpTask {
        url: Some(format!(
            "https://clob.polymarket.com/event/{}",
            event_id
        )),
        ..Default::default()
    })),
}
```

### Multi-Source Validation

Verify multiple feeds use approved sources:

```rust
for (i, feed) in feeds.iter().enumerate() {
    require!(
        *feed.feed_id() == expected_feed_ids[i],
        ErrorCode::FeedMismatch
    );
}
```

## Next Steps

* **Price Feeds**: Learn basic oracle integration in [Basic Price Feed](/docs-by-chain/solana-svm/price-feeds/basic-price-feed)
* **Custom Feeds**: Create your own feed definitions in [Custom Feeds](/custom-feeds/build-and-deploy-feed)
* **Randomness**: Explore verifiable randomness in [Randomness](/docs-by-chain/solana-svm/randomness/randomness-tutorial)


# Randomness

Blockchain users want randomness for many applications like gaming, NFT mints, lotteries, and more. However, this poses a fundamental challenge to blockchains, which are deterministic computers replicated across many nodes across the globe. Each node needs to produce the exact same output when given the same sequence of inputs.

<figure><img src="/files/hqZiVzDcFisv0Ko9DIqg" alt=""><figcaption></figcaption></figure>

Imagine if an on-chain lottery was deciding whether to mint an NFT to Alice or Bob. If blockchain nodes ran their own randomness and some decided that the NFT would go to Alice, and others to Bob, there would be a state mismatch.

<figure><img src="/files/b5FAOjoWFqsBuEfq5ykn" alt=""><figcaption></figcaption></figure>

This is where oracles come in. An oracle can run the randomness off-chain and then post a single result to the blockchain, ensuring that all nodes agree on the result of the randomness.

<figure><img src="/files/VvEFCi1KYVCekLCqFs4e" alt=""><figcaption></figcaption></figure>

However, as a third-party source of randomness, it's critical to make sure that nefarious actors cannot control the oracle and bias the randomness in their favor.

<figure><img src="/files/lsY5MpB2NSUNrQxRwNjn" alt=""><figcaption></figcaption></figure>

As an oracle provider, Switchboard's network serves as a trusted and verified third-party that can post fair random numbers to the blockchain.

<figure><img src="/files/FzjwQbEW0q1Nik84M5yI" alt=""><figcaption></figcaption></figure>

## Switchboard’s approach

Switchboard leverages Trusted Execution Environments (TEEs), which are protected areas inside of a computer's processing unit that cannot be altered or inspected. This means:

* No one, including the oracle operator, can alter the code that’s running on the TEEs
* No one, including the oracle operator, can see what’s going on inside the chip, only inputs and outputs.

This means that Switchboard oracles can generate safe and fair randomness that is free from malicious influence. As an extra layer of protection, Switchboard network incentives ensure that oracle oeprators that misbehave by experiencing downtime or withholding results can have their $SWTCH stake slashed.

## How to Use Switchboard Randomness

To understand the flow, it's helpful to visualize the following 5 parties.

* **Alice**: blockchain user
* **App**: on-chain application
* **Switchboard Contract**: on-chain contract that handles anything Switchboard-related.
* **Crossbar**: server that helps you talk to oracles
* **Oracle**: generates randomness

<figure><img src="/files/aiqbP0k5zb27cvYFnKAl" alt=""><figcaption></figcaption></figure>

There are two stages, requesting and resolving the randomness.

### Request Randomness

* First, **Alice** talks to the **App** requesting some random event.
* The **App** then generates a randomness request with a unique ID and sends it to the **Switchboard contract**.
* The **Switchboard contract** responds to the **App** with an oracle assignment.
* The **App** responds to **Alice** with the oracle assignment and randomness ID.

### Resolve Randomness

* **Alice** sends the oracle assignment, randomness ID, and some other data to **Crossbar** to get the randomness.
* **Crossbar** asks the **Oracle** to generate randomness.
* The **Oracle** creates a randomness object and sends it to **Crossbar** which passes it back to **Alice**.
* **Alice** sends the randomness object to the **App**.
* The **App** asks the **Switchboard contract** to verify that the randomness it received from Alice is correct.
* If all is well, the **Switchboard contract** sends verification to the **App**, resolving the random event.

## Solana Technical Quick Reference

* **Account structure**: use a Switchboard randomness account (commit/reveal state parsed by `RandomnessAccountData`) plus your app-owned state account/PDA (for app fields like `randomness_account` and `commit_slot`).
* **PDA seeds**: Switchboard randomness account is created with `sb.Randomness.create(...)`. App PDA seeds are app-specific; tutorial examples use `[b"playerState", user.key().as_ref()]` and `[b"stateEscrow"]`.
* **Oracle assignment**: assignment happens through the Switchboard request/commit flow and is checked during reveal/settlement. Store and verify the same randomness account reference across commit and settle.
* **Generation window duration**: treat this as policy-based. A strict freshness policy example is `seed_slot == current_slot - 1`, while less strict policies can be chosen for lower-sensitivity flows.
* **Reveal readiness check**: call `RandomnessAccountData::get_value(clock.slot)`. At settle time, success means randomness is ready; error means not yet resolved.

For complete Solana code (account structs, commit/reveal instruction flow, and slot checks), see the [Randomness Tutorial](/docs-by-chain/solana-svm/randomness/randomness-tutorial).

## Next Step

Read the [Randomness Tutorial](/docs-by-chain/solana-svm/randomness/randomness-tutorial) for full account structures, PDA examples, and commit/reveal validation patterns.

\--


# Randomness Tutorial

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

This tutorial demonstrates how to integrate **verifiable randomness** into your Solana program using Switchboard's commit-reveal pattern. You'll build a provably fair coin flip game.

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

## Why Verifiable Randomness?

On-chain randomness is hard. Naive approaches fail because:

* **Block hashes are predictable**: Validators can see future block data
* **Timestamps are manipulable**: Validators control block timestamps
* **External sources are untrusted**: Off-chain randomness can be faked

Switchboard solves this with a **commit-reveal pattern** where neither party knows the outcome until after commitment.

## How It Works

```
1. COMMIT    →    2. GENERATE    →    3. REVEAL
   Player            Oracle             Settlement
   commits to        generates          Player reveals
   slothash          randomness         and uses value
```

1. **Commit**: Player commits to using a specific Solana slothash
2. **Generate**: Oracle generates randomness based on the committed slot
3. **Reveal**: Player reveals the randomness and uses it in their program

This is secure because:

* The player commits before knowing the randomness
* The oracle generates randomness after commitment
* Neither party can manipulate the outcome

## What You'll Build

A coin flip game where:

* Player guesses heads or tails
* Switchboard generates verifiable random outcome
* Winner receives double their wager

## Prerequisites

* Rust and Cargo installed
* Anchor framework 0.31.1
* Solana CLI installed and configured
* Node.js and pnpm
* A Solana keypair with SOL

## Key Concepts

### Randomness Account

A dedicated Solana account that stores:

* The committed slothash
* The seed slot
* The revealed random value (after revelation)

```typescript
const [randomness, createIx] = await sb.Randomness.create(sbProgram, rngKp, queue);
```

### RandomnessAccountData

The on-chain struct for parsing randomness state:

```rust
use switchboard_on_demand::accounts::RandomnessAccountData;

let randomness_data = RandomnessAccountData::parse(
    ctx.accounts.randomness_account_data.data.borrow()
).unwrap();
```

> **Note:** Randomness is not read from `quote.feeds()`. For Solana commit-reveal, call `RandomnessAccountData::get_value(clock.slot)`, which returns a 32-byte randomness value (for example, `random_bytes[0] % 2` for a coin flip).

### Slot-Based Freshness

Randomness must be used within a specific slot window:

```rust
// Ensure randomness was committed in the previous slot
if randomness_data.seed_slot != clock.slot - 1 {
    return Err(ErrorCode::RandomnessExpired.into());
}
```

### Collateral on Commit (Critical!)

**Always take payment when committing, not when revealing.**

```rust
// CORRECT: Take collateral at commit time
pub fn coin_flip(ctx: Context<CoinFlip>, ...) -> Result<()> {
    // Validate randomness...

    // Take wager NOW, before randomness is revealed
    transfer(from_user, to_escrow, wager)?;

    Ok(())
}
```

Why? If you take payment on reveal, a malicious user could:

1. Commit to randomness
2. Wait for reveal
3. Only reveal if they won (selective revelation attack)

## The On-Chain Program

### Dependencies

```toml
[dependencies]
anchor-lang = "0.31.1"
switchboard-on-demand = { version = "0.13.0", features = ["anchor"] }
```

### Program Structure

```rust
use anchor_lang::prelude::*;
use switchboard_on_demand::accounts::RandomnessAccountData;

declare_id!("YOUR_PROGRAM_ID");

#[program]
pub mod sb_randomness {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let player_state = &mut ctx.accounts.player_state;
        player_state.latest_flip_result = false;
        player_state.randomness_account = Pubkey::default();
        player_state.wager = 100; // lamports
        player_state.bump = ctx.bumps.player_state;
        player_state.allowed_user = ctx.accounts.user.key();
        Ok(())
    }

    pub fn coin_flip(
        ctx: Context<CoinFlip>,
        randomness_account: Pubkey,
        guess: bool, // true = heads, false = tails
    ) -> Result<()> {
        let clock = Clock::get()?;
        let player_state = &mut ctx.accounts.player_state;

        // Record the user's guess
        player_state.current_guess = guess;

        // Parse the randomness account
        let randomness_data = RandomnessAccountData::parse(
            ctx.accounts.randomness_account_data.data.borrow()
        ).unwrap();

        // SECURITY: Verify randomness is fresh (committed in previous slot)
        if randomness_data.seed_slot != clock.slot - 1 {
            msg!("seed_slot: {}", randomness_data.seed_slot);
            msg!("current slot: {}", clock.slot);
            return Err(ErrorCode::RandomnessExpired.into());
        }

        // SECURITY: Ensure randomness hasn't been revealed yet
        if !randomness_data.get_value(clock.slot).is_err() {
            return Err(ErrorCode::RandomnessAlreadyRevealed.into());
        }

        // Store commit slot for later verification
        player_state.commit_slot = randomness_data.seed_slot;

        // CRITICAL: Take collateral NOW, not on reveal!
        transfer(
            ctx.accounts.system_program.to_account_info(),
            ctx.accounts.user.to_account_info(),
            ctx.accounts.escrow_account.to_account_info(),
            player_state.wager,
            None,
        )?;

        // Store randomness account reference
        player_state.randomness_account = randomness_account;

        msg!("Coin flip initiated, randomness requested.");
        Ok(())
    }

    pub fn settle_flip(ctx: Context<SettleFlip>, escrow_bump: u8) -> Result<()> {
        let clock = Clock::get()?;
        let player_state = &mut ctx.accounts.player_state;

        // SECURITY: Verify randomness account matches stored reference
        if ctx.accounts.randomness_account_data.key() != player_state.randomness_account {
            return Err(ErrorCode::InvalidRandomnessAccount.into());
        }

        // Parse randomness data
        let randomness_data = RandomnessAccountData::parse(
            ctx.accounts.randomness_account_data.data.borrow()
        ).unwrap();

        // SECURITY: Verify seed_slot matches commit
        if randomness_data.seed_slot != player_state.commit_slot {
            return Err(ErrorCode::RandomnessExpired.into());
        }

        // Get the revealed random value
        let revealed_random_value = randomness_data
            .get_value(clock.slot)
            .map_err(|_| ErrorCode::RandomnessNotResolved)?;

        // Use randomness to determine outcome
        // Even = heads (true), Odd = tails (false)
        let randomness_result = revealed_random_value[0] % 2 == 0;

        player_state.latest_flip_result = randomness_result;

        if randomness_result {
            msg!("FLIP_RESULT: Heads");
        } else {
            msg!("FLIP_RESULT: Tails");
        }

        // Settle the wager
        if randomness_result == player_state.current_guess {
            msg!("You win!");
            // Pay out double the wager
            let seeds = &[b"stateEscrow".as_ref(), &[escrow_bump]];
            transfer(
                ctx.accounts.system_program.to_account_info(),
                ctx.accounts.escrow_account.to_account_info(),
                ctx.accounts.user.to_account_info(),
                player_state.wager * 2,
                Some(&[seeds]),
            )?;
        } else {
            msg!("You lose!");
            // Escrow keeps the wager
        }

        Ok(())
    }
}
```

### Account Structures

```rust
#[account]
pub struct PlayerState {
    allowed_user: Pubkey,        // Who can play
    latest_flip_result: bool,    // Last flip outcome
    randomness_account: Pubkey,  // Reference to Switchboard randomness
    current_guess: bool,         // Player's guess
    wager: u64,                  // Bet amount
    bump: u8,                    // PDA bump
    commit_slot: u64,            // Slot when committed
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = user,
        seeds = [b"playerState", user.key().as_ref()],
        space = 8 + 100,
        bump
    )]
    pub player_state: Account<'info, PlayerState>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct CoinFlip<'info> {
    #[account(
        mut,
        seeds = [b"playerState", user.key().as_ref()],
        bump = player_state.bump
    )]
    pub player_state: Account<'info, PlayerState>,
    pub user: Signer<'info>,
    /// CHECK: Validated manually in handler
    pub randomness_account_data: AccountInfo<'info>,
    /// CHECK: Escrow PDA
    #[account(mut, seeds = [b"stateEscrow"], bump)]
    pub escrow_account: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct SettleFlip<'info> {
    #[account(
        mut,
        seeds = [b"playerState", user.key().as_ref()],
        bump = player_state.bump
    )]
    pub player_state: Account<'info, PlayerState>,
    /// CHECK: Validated manually in handler
    pub randomness_account_data: AccountInfo<'info>,
    /// CHECK: Escrow PDA
    #[account(mut, seeds = [b"stateEscrow"], bump)]
    pub escrow_account: AccountInfo<'info>,
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
```

### Error Codes

```rust
#[error_code]
pub enum ErrorCode {
    #[msg("Unauthorized access attempt.")]
    Unauthorized,
    #[msg("Game is still active.")]
    GameStillActive,
    #[msg("Not enough funds to play.")]
    NotEnoughFundsToPlay,
    #[msg("Randomness already revealed.")]
    RandomnessAlreadyRevealed,
    #[msg("Randomness not yet resolved.")]
    RandomnessNotResolved,
    #[msg("Randomness has expired.")]
    RandomnessExpired,
    #[msg("Invalid randomness account.")]
    InvalidRandomnessAccount,
}
```

## The TypeScript Client

### Setup

```typescript
import * as anchor from "@coral-xyz/anchor";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import * as sb from "@switchboard-xyz/on-demand";

async function main() {
  // Load environment
  const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();

  // Get the default Switchboard queue
  const queue = await sb.getDefaultQueue(connection.rpcEndpoint);

  // Load your program
  const myProgram = await loadMyProgram(program.provider);
  const sbProgram = await loadSbProgram(program.provider);
}
```

### Create Randomness Account

```typescript
// Generate keypair for randomness account
const rngKp = Keypair.generate();

// Create the randomness account
const [randomness, createIx] = await sb.Randomness.create(sbProgram, rngKp, queue);

// Send creation transaction
const createTx = await sb.asV0Tx({
  connection,
  ixs: [createIx],
  payer: keypair.publicKey,
  signers: [keypair, rngKp],
  computeUnitPrice: 75_000,
  computeUnitLimitMultiple: 1.3,
});

await connection.sendTransaction(createTx);
```

### Commit Phase

```typescript
// Get user's guess from command line
const userGuess = process.argv[2] === "heads"; // true = heads

// Create commit instruction
const commitIx = await randomness.commitIx(queue);

// Create your program's coin flip instruction
const coinFlipIx = await myProgram.methods
  .coinFlip(rngKp.publicKey, userGuess)
  .accounts({
    playerState: playerStateAccount,
    user: keypair.publicKey,
    randomnessAccountData: rngKp.publicKey,
    escrowAccount: escrowAccount,
    systemProgram: SystemProgram.programId,
  })
  .instruction();

// Bundle commit + coin_flip in same transaction
const commitTx = await sb.asV0Tx({
  connection,
  ixs: [commitIx, coinFlipIx],
  payer: keypair.publicKey,
  signers: [keypair],
  computeUnitPrice: 75_000,
  computeUnitLimitMultiple: 1.3,
});

const commitSig = await connection.sendTransaction(commitTx);
await connection.confirmTransaction(commitSig, "confirmed");
console.log("Committed! Transaction:", commitSig);
```

### Reveal Phase

```typescript
// Wait for slot to advance
console.log("Waiting for randomness generation...");
await new Promise(resolve => setTimeout(resolve, 3000));

// Create reveal instruction
const revealIx = await randomness.revealIx();

// Create your program's settle instruction
const settleFlipIx = await myProgram.methods
  .settleFlip(escrowBump)
  .accounts({
    playerState: playerStateAccount,
    randomnessAccountData: rngKp.publicKey,
    escrowAccount: escrowAccount,
    user: keypair.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .instruction();

// Bundle reveal + settle in same transaction
const revealTx = await sb.asV0Tx({
  connection,
  ixs: [revealIx, settleFlipIx],
  payer: keypair.publicKey,
  signers: [keypair],
  computeUnitPrice: 75_000,
  computeUnitLimitMultiple: 1.3,
});

const revealSig = await connection.sendTransaction(revealTx);
await connection.confirmTransaction(revealSig, "confirmed");
console.log("Revealed! Transaction:", revealSig);

// Parse result from logs
const tx = await connection.getParsedTransaction(revealSig, {
  maxSupportedTransactionVersion: 0,
});
const resultLog = tx?.meta?.logMessages?.find(line =>
  line.includes("FLIP_RESULT")
);
console.log("Result:", resultLog);
```

### Retry Logic

Network issues can cause commit/reveal to fail. Add retry logic:

```typescript
async function retryCommit(
  randomness: sb.Randomness,
  queue: any,
  maxRetries = 3
): Promise<anchor.web3.TransactionInstruction> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Commit attempt ${attempt}/${maxRetries}...`);
      return await randomness.commitIx(queue);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      console.log(`Failed, retrying in 2s...`);
      await new Promise(r => setTimeout(r, 2000));
    }
  }
  throw new Error("All commit attempts failed");
}

async function retryReveal(
  randomness: sb.Randomness,
  maxRetries = 5
): Promise<anchor.web3.TransactionInstruction> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Reveal attempt ${attempt}/${maxRetries}...`);
      return await randomness.revealIx();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      console.log(`Failed, retrying in 2s...`);
      await new Promise(r => setTimeout(r, 2000));
    }
  }
  throw new Error("All reveal attempts failed");
}
```

## Running the Example

### 1. Clone the Repository

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/solana/randomness/coin-flip
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Point Solana CLI at Devnet

```bash
solana config set --url devnet
solana airdrop 2
```

### 4. Run the Example Against the Preconfigured Devnet Program

The checked-in `Anchor.toml` already includes a devnet program ID for `sb_randomness`, so you can run the example directly:

```bash
npm run start -- heads
# or
npm run start -- tails
```

### 5. Deploy Your Own Program (Optional)

If you want to deploy your own instance instead of using the preconfigured devnet program:

```bash
anchor keys sync
anchor build
anchor deploy
anchor idl init --filepath target/idl/sb_randomness.json YOUR_PROGRAM_ADDRESS
```

The example script resolves the example program ID from `Anchor.toml`. To override that at runtime, set `SB_RANDOMNESS_PROGRAM_ID`:

```bash
SB_RANDOMNESS_PROGRAM_ID=YOUR_PROGRAM_ADDRESS npm run start -- heads
```

### Expected Output

```
Setup...
Program 93tkpep2PYDxweHHi2vQBpi7eTBF23y8LGdiLMt5R9f2
Queue account FdRnYujMnYbAJp5P2rkEYZCbF2TKs2D2yXZ7MYq89Hms

Initialize the game states...
  Transaction Signature abc123...

Commit to randomness...
  Transaction Signature commitTx def456...

Reveal the randomness...
  Transaction Signature revealTx ghi789...

Your guess is Heads

And the random result is ... Heads!
You win!

Game completed!
```

## Security Best Practices

### 1. Always Take Collateral at Commit Time

```rust
// CORRECT
pub fn coin_flip(...) -> Result<()> {
    // ... validate randomness ...
    transfer(user, escrow, wager)?;  // Take payment HERE
    Ok(())
}

// WRONG - vulnerable to selective revelation
pub fn settle_flip(...) -> Result<()> {
    transfer(user, escrow, wager)?;  // DON'T take payment here!
    // ... use randomness ...
}
```

### 2. Validate Slot Freshness

```rust
// Ensure randomness was committed recently
if randomness_data.seed_slot != clock.slot - 1 {
    return Err(ErrorCode::RandomnessExpired.into());
}
```

### 3. Verify Randomness Account Reference

```rust
// Store at commit time
player_state.randomness_account = randomness_account;

// Verify at reveal time
if ctx.accounts.randomness_account_data.key() != player_state.randomness_account {
    return Err(ErrorCode::InvalidRandomnessAccount.into());
}
```

### 4. Check Randomness Not Already Revealed

```rust
// At commit time, ensure randomness isn't already revealed
if !randomness_data.get_value(clock.slot).is_err() {
    return Err(ErrorCode::RandomnessAlreadyRevealed.into());
}
```

## Use Cases

### Gaming & Gambling

* Casino games (dice, slots, roulette)
* Provably fair betting
* Skill-based games with random elements

### NFT Minting

* Random trait assignment
* Fair rarity distribution
* Blind box reveals

### Lotteries

* Ticket drawing
* Raffle winners
* Prize distribution

### Fair Distribution

* Airdrop selection
* Whitelist randomization
* Token allocation

## Advanced Topics

### Reusing Randomness Accounts

Save the keypair to reuse across sessions:

```typescript
import * as fs from "fs";

const KEYPAIR_PATH = "randomness-keypair.json";

// Load or create
let rngKp: Keypair;
if (fs.existsSync(KEYPAIR_PATH)) {
  const data = JSON.parse(fs.readFileSync(KEYPAIR_PATH, "utf8"));
  rngKp = Keypair.fromSecretKey(new Uint8Array(data));
} else {
  rngKp = Keypair.generate();
  fs.writeFileSync(KEYPAIR_PATH, JSON.stringify(Array.from(rngKp.secretKey)));
}
```

### Multiple Random Values

The revealed value is 32 bytes. Use different bytes for different outcomes:

```rust
let random_bytes = randomness_data.get_value(clock.slot)?;

// Use different bytes for different purposes
let coin_flip = random_bytes[0] % 2 == 0;
let dice_roll = (random_bytes[1] % 6) + 1;  // 1-6
let card_draw = random_bytes[2] % 52;       // 0-51
```

## Next Steps

* **Price Feeds**: Learn oracle integration in [Basic Price Feed](https://github.com/switchboard-xyz/gitbook-on-demand/blob/main/docs-by-chain/solana-svm/randomness/price-feeds/basic-price-feed.md)
* **Prediction Markets**: See feed verification in [Prediction Market](https://github.com/switchboard-xyz/gitbook-on-demand/blob/main/docs-by-chain/solana-svm/randomness/prediction-market.md)
* **Custom Feeds**: Create your own feeds in [Custom Feeds](/custom-feeds/build-and-deploy-feed)


# X402 Micropayments

X402 is a micropayment protocol that enables pay-per-request access to premium data sources on Solana. By integrating X402 with Switchboard, you can access paywalled APIs and RPC endpoints directly from oracle jobs, paying only for the data you consume.

## What is X402?

X402 (named after the HTTP 402 "Payment Required" status code) is a protocol for micropayments on Solana. It allows data providers to monetize their APIs and services on a per-request basis, while consumers can access premium data without subscriptions or upfront commitments.

Key benefits:

* **Pay-per-use**: Only pay for the data you actually consume
* **No subscriptions**: Access premium data without monthly commitments
* **Instant payments**: USDC micropayments settle immediately on Solana
* **Seamless integration**: Works with standard HTTP APIs via authentication headers

**Prerequisite:** A Solana keypair funded with USDC on mainnet-beta (including a USDC associated token account with balance) is required to generate PAYMENT-SIGNATURE headers.

## How It Works with Switchboard

Switchboard integrates with X402 through **variable overrides**, a powerful feature that allows you to inject dynamic values into oracle job definitions at runtime. This enables oracles to authenticate with paywalled endpoints without storing sensitive credentials on-chain or in IPFS.

```
┌─────────────────────────────────────────────────────────────────────────┐
│                           X402 + Switchboard Flow                       │
└─────────────────────────────────────────────────────────────────────────┘

  ┌──────────┐      1. Derive            ┌────────────────┐
  │   Your   │      PAYMENT-SIGNATURE   │  x402 v2 Client │
  │   App    │ ────────────────────────► │                │
  └──────────┘                           └────────────────┘
       │                                         │
       │ 2. Pass headers as                      │
       │    variable overrides                   │
       ▼                                         │
  ┌──────────┐                                   │
  │Crossbar  │ ◄─────────────────────────────────┘
  │          │     PAYMENT-SIGNATURE header
  └──────────┘
       │
       │ 3. Fetch oracle update
       │    with auth headers
       ▼
  ┌──────────┐      4. Authenticated     ┌────────────────┐
  │  Oracle  │      HTTP request         │  Paywalled     │
  │  (TEE)   │ ────────────────────────► │  RPC/API       │
  └──────────┘                           └────────────────┘
       │                                         │
       │ 5. Return signed                        │
       │    oracle data                          │
       ▼                                         │
  ┌──────────┐                                   │
  │  Quote   │ ◄─────────────────────────────────┘
  │ Account  │       Verified data
  └──────────┘
```

The key innovation is that oracle feeds are defined **inline** (not stored on IPFS), with placeholder variables like `${X402_PAYMENT_SIGNATURE}` that get replaced at runtime with actual authentication headers.

## Key Concepts

### x402 v2 Client

Create an x402 v2 client that can sign USDC payments on Solana:

```typescript
import { x402Client } from "@x402/fetch";
import { registerExactSvmScheme } from "@x402/svm/exact/client";
import { toClientSvmSigner } from "@x402/svm";
import { createKeyPairSignerFromBytes } from "@solana/kit";

const signer = await createKeyPairSignerFromBytes(keypair.secretKey);
const client = new x402Client();
registerExactSvmScheme(client, { signer: toClientSvmSigner(signer) });
```

### Variable Overrides

Instead of hardcoding authentication headers in your feed definition, you use placeholders:

```typescript
const ORACLE_FEED = {
  name: "X402 Paywalled RPC Call",
  jobs: [{
    tasks: [{
      httpTask: {
        url: "https://paywalled-api.example.com",
        headers: [
          { key: "PAYMENT-SIGNATURE", value: "${X402_PAYMENT_SIGNATURE}" }
        ]
      }
    }]
  }]
};
```

At runtime, you derive the actual headers and pass them as overrides:

```typescript
const instructions = await queue.fetchManagedUpdateIxs(crossbar, [ORACLE_FEED], {
  variableOverrides: {
    X402_PAYMENT_SIGNATURE: paymentSignature
  }
});
```

### Quote Accounts

Verified oracle data is stored in a canonical **quote account** derived from the feed hash. This allows your on-chain program to read the authenticated data:

```typescript
const feedId = FeedHash.computeOracleFeedId(ORACLE_FEED);
const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedId]);
```

## Use Cases

* **Premium RPC Endpoints**: Access high-performance, paywalled Solana RPC nodes
* **Authenticated APIs**: Fetch data from APIs requiring micropayment authentication
* **Dynamic Authentication**: Support custom authentication schemes without storing credentials
* **Pay-per-Request Data**: Access expensive data sources only when needed

## Next Steps

* Follow the [X402 Tutorial](/docs-by-chain/solana-svm/x402/x402-tutorial) for a complete implementation walkthrough
* Explore [Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) for more dynamic feed patterns
* Learn about [Custom Feeds](/custom-feeds/build-and-deploy-feed) for building your own oracle jobs


# X402 Tutorial

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

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

## The Problem: Accessing Paywalled Data in Oracles

Many valuable data sources—premium RPC endpoints, institutional APIs, proprietary market data—require payment or authentication. Traditional oracles can't access these sources because:

1. **No way to pay**: Oracles can't hold funds or make payments on your behalf
2. **Static credentials**: Storing API keys in feed definitions (on IPFS or on-chain) exposes them publicly
3. **Per-request pricing**: Many premium services charge per-request, incompatible with polling oracles

**X402 solves this** by enabling micropayments directly in HTTP requests. The oracle authenticates with the paywalled API using a PAYMENT-SIGNATURE header you provide at runtime, without ever exposing credentials or requiring the oracle to hold funds.

## What We're Building

In this tutorial, we'll fetch data from a **paywalled Helius RPC endpoint** using X402 micropayments. The flow works like this:

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                              X402 Oracle Flow                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   YOUR APP                           ORACLE (TEE)           PAYWALLED API   │
│   ────────                           ────────────           ─────────────   │
│                                                                             │
│   1. Derive PAYMENT-SIGNATURE ─────┐                                        │
│      (USDC auth)                   │                                        │
│                               ▼                                             │
│   2. Define feed with    ┌─────────┐                                        │
│      ${PLACEHOLDER}  ───►│Crossbar │                                        │
│      variables           └────┬────┘                                        │
│                               │                                             │
│   3. Pass headers as          │    4. Oracle makes               ┌────────┐ │
│      variable overrides ──────┼─────► authenticated ────────────►│ Helius │ │
│                               │       HTTP request               │  RPC   │ │
│                               │       with PAYMENT-SIGNATURE     └───┬────┘ │
│                               │       header                         │      │
│                               │                                      │      │
│                               │    5. Paywalled data ◄───────────────┘      │
│                               │       returned                              │
│                               ▼                                             │
│   6. Signed oracle data  ┌─────────┐                                        │
│      in quote account ◄──│ Oracle  │                                        │
│                          │Response │                                        │
│                          └─────────┘                                        │
└─────────────────────────────────────────────────────────────────────────────┘
```

**Specifically, we'll:**

* Call `getBlockHeight` on a paywalled Helius RPC endpoint
* Pay for the request using USDC micropayments via the X402 protocol
* Receive the block height as verified oracle data on-chain

This pattern works for any paywalled HTTP API—you're not limited to RPC endpoints.

## Prerequisites

* [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools) installed and configured
* Node.js 20+
* A Solana keypair with **USDC on mainnet-beta** (the X402 payment token)
  * Ensure the wallet has a USDC associated token account (ATA) with balance

## Installation

Clone the examples repository and install dependencies:

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples.git
cd sb-on-demand-examples/solana/x402
pnpm install
```

### Dependencies

The example uses these X402-specific packages:

```json
{
  "@x402/fetch": "^2.2.0",
  "@x402/svm": "^2.2.0"
}
```

## How X402 Authentication Works

Unlike standard oracle feeds (stored on IPFS with a feed hash), X402 feeds are defined **inline** in your code with placeholder variables. At runtime, you:

1. **Derive a PAYMENT-SIGNATURE header** from the X402 protocol (this authorizes your USDC payment)
2. **Replace placeholders** with the header via `variableOverrides`
3. **Oracle executes** the authenticated request inside a TEE (Trusted Execution Environment)

The oracle never sees your wallet or credentials—it just receives the pre-signed authentication headers.

## Implementation

### Step 1: Set Up Imports and Constants

```typescript
import { PublicKey, Connection, Keypair } from "@solana/web3.js";
import * as sb from "@switchboard-xyz/on-demand";
import { OracleQuote } from "@switchboard-xyz/on-demand";
import { FeedHash, OracleJob } from "@switchboard-xyz/common";
import { x402Client, x402HTTPClient } from "@x402/fetch";
import { registerExactSvmScheme } from "@x402/svm/exact/client";
import { toClientSvmSigner } from "@x402/svm";
import { createKeyPairSignerFromBytes } from "@solana/kit";
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";

const URL = "https://helius.api.corbits.dev";
const RPC_METHOD = "getBlockHeight";
const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
```

### Step 2: Define the Oracle Feed with Placeholders

This is the key difference from standard feeds. Instead of storing the feed on IPFS, we define it inline with **placeholder variables** for the authentication headers:

```typescript
const ORACLE_FEED = {
  name: "X402 Paywalled RPC Call",
  minJobResponses: 1, // unscaled job/source quorum
  minOracleSamples: 1, // unscaled oracle/signature quorum
  maxJobRangePct: 0, // Intentional for this single-use flow; use a positive scaled value for normal multi-source feeds.
  jobs: [
    {
      tasks: [
        {
          // Task 1: Make an authenticated HTTP POST request to the paywalled RPC
          httpTask: {
            url: URL,
            method: OracleJob.HttpTask.Method.METHOD_POST,
            body: JSON.stringify({
              jsonrpc: "2.0",
              id: 1,
              method: RPC_METHOD,  // "getBlockHeight"
            }),
            headers: [
              {
                // X402 payment proof - proves you've authorized the USDC payment
                key: "PAYMENT-SIGNATURE",
                value: "${X402_PAYMENT_SIGNATURE}",
              },
            ],
          },
        },
        {
          // Task 2: Extract the result from the JSON-RPC response
          // Response format: { "jsonrpc": "2.0", "result": 123456789, "id": 1 }
          jsonParseTask: {
            path: "$.result",
          },
        },
      ],
    },
  ],
};
```

**Key points:**

* `${X402_PAYMENT_SIGNATURE}` is a placeholder that gets replaced at runtime
* The oracle sees the actual header values, but they're never stored permanently anywhere
* `minJobResponses: 1` and `minOracleSamples: 1` because X402 payments are single-use (you can't have multiple oracles reuse the same payment signature)

### Step 3: Initialize the x402 v2 Client

Create an x402 v2 client that can sign Solana payments:

```typescript
// Load Solana environment from `solana config get`
const { program, keypair, connection, crossbar } = await sb.AnchorUtils.loadEnv();
console.log("Wallet:", keypair.publicKey.toBase58());

// Create a Solana signer for x402 v2
const signer = await createKeyPairSignerFromBytes(keypair.secretKey);

// Register the Exact SVM scheme (supports v2 + v1)
const client = new x402Client();
registerExactSvmScheme(client, { signer: toClientSvmSigner(signer) });
```

### Step 4: Check USDC Balance

X402 payments are made in USDC. Verify you have sufficient balance before proceeding:

```typescript
async function checkUsdcBalance(
  connection: Connection,
  keypair: Keypair,
  usdcMint: PublicKey
): Promise<void> {
  const usdcTokenAccount = await getAssociatedTokenAddress(
    usdcMint,
    keypair.publicKey
  );
  const tokenAccountInfo = await getAccount(connection, usdcTokenAccount);
  const usdcBalance = Number(tokenAccountInfo.amount) / 1_000_000;
  console.log("USDC balance:", usdcBalance.toFixed(6), "USDC");
}

await checkUsdcBalance(connection, keypair, USDC);
```

### Step 5: Derive PAYMENT-SIGNATURE

This is where the magic happens. The x402 v2 client derives a `PAYMENT-SIGNATURE` header that:

* Proves you've authorized a USDC payment for this specific request
* Is bound to the exact URL, method, and body (can't be reused for different requests)
* Is single-use (can't be replayed)

```typescript
// Fetch the payment requirements (402) and derive a signature header
const response = await fetch(URL, {
  method: "POST",
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: RPC_METHOD,
  }),
});

const httpClient = new x402HTTPClient(client);
const paymentRequired = httpClient.getPaymentRequiredResponse(
  name => response.headers.get(name),
  await response.json().catch(() => undefined)
);
const paymentPayload = await client.createPaymentPayload(paymentRequired);
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
const paymentSignature = paymentHeaders["PAYMENT-SIGNATURE"];
```

**Important:** These headers are single-use. Once the oracle uses them, they can't be used again.

### Step 6: Fetch Managed Update with Variable Overrides

Now we bring it all together. We pass our PAYMENT-SIGNATURE header as **variable overrides**, which replace the `${PLACEHOLDER}` values in our feed definition:

```typescript
// Load Switchboard queue
const queue = await sb.Queue.loadDefault(program);

// Compute the feed ID (hash of the inline feed definition)
// This is deterministic - same feed definition = same ID
const feedId = FeedHash.computeOracleFeedId(ORACLE_FEED);
console.log("Feed ID:", `0x${feedId.toString("hex")}`);

// Derive the quote account where verified data will be stored
const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedId]);
console.log("Quote Account:", quoteAccount.toBase58());

// Fetch managed update instructions
// This sends our feed definition + variable overrides to Crossbar,
// which coordinates with an oracle to execute the authenticated request
const instructions = await queue.fetchManagedUpdateIxs(
  crossbar,
  [ORACLE_FEED],  // Our inline feed definition with placeholders
  {
    // CRITICAL: numSignatures MUST BE 1 for X402 requests
    // (payment signatures are single-use, can't be shared across oracles)
    numSignatures: 1,

    // Variable overrides replace ${PLACEHOLDER} values in the feed
    variableOverrides: {
      X402_PAYMENT_SIGNATURE: paymentSignature,
    },
    payer: keypair.publicKey,
  }
);
```

The returned `instructions` contain:

1. **Ed25519 signature verification** - Proves the oracle signed the response
2. **Quote program update** - Writes verified data to the quote account

### Step 7: Build and Send Transaction

Finally, bundle the oracle instructions with your program's instruction and submit:

```typescript
// Build transaction with oracle update and your program instruction
const tx = await sb.asV0Tx({
  connection,
  ixs: [
    ...instructions,  // Oracle update (Ed25519 verify + quote write)
    readOracleIx,     // Your program reads from the quote account
  ],
  signers: [keypair],
  computeUnitPrice: 20_000,
  computeUnitLimitMultiple: 1.1,
});

// Simulate to verify everything works
// (safe here because this is the same transaction we'll send)
const sim = await connection.simulateTransaction(tx);
console.log(sim.value.logs?.join("\n"));

// Send the transaction
const signature = await connection.sendTransaction(tx);
console.log("Transaction:", signature);
```

After the transaction confirms, the `quoteAccount` contains the verified block height from the paywalled RPC.

## Important Constraints

### Why numSignatures Must Be 1

X402 payment signatures are **single-use**. Each signature can only authenticate one oracle request. If you set `numSignatures: 2`, the second oracle would try to reuse the same signature and fail.

```typescript
const instructions = await queue.fetchManagedUpdateIxs(crossbar, [ORACLE_FEED], {
  numSignatures: 1,  // REQUIRED for X402 - headers can't be shared
  // ...
});
```

### Simulation Warning: Don't Pay Twice

The X402 payment is charged when the oracle makes the HTTP request, not when your transaction lands on-chain. This means:

* **Crossbar simulation** (`crossbar.simulateFeed()`) will charge you
* **On-chain simulation** (`connection.simulateTransaction()`) is safe (no HTTP call)

```typescript
// DON'T DO THIS - you'll pay for the request but get no on-chain result
// const simFeed = await crossbar.simulateFeed(ORACLE_FEED, true, {
//   X402_PAYMENT_SIGNATURE: paymentSignature
// });

// This is SAFE - simulates the transaction, not the HTTP request
const sim = await connection.simulateTransaction(tx);
```

### Why Inline Feeds?

Standard Switchboard feeds are stored on IPFS and referenced by hash. X402 feeds must be defined inline because:

1. Payment signatures change with every request
2. Headers contain sensitive payment authorization
3. The feed definition contains runtime placeholders, not static values

## Running the Example

```bash
pnpm start
```

Expected output:

```
Wallet: <your-wallet-address>
RPC Method: getBlockHeight
USDC balance: 10.500000 USDC
X402 v2 client initialized

Deriving X402 PAYMENT-SIGNATURE...
X402 PAYMENT-SIGNATURE generated
Feed ID: 0x<feed-hash>
Quote Account: <quote-account-address>

Fetching managed update instructions with X402 variable overrides...
Generated instructions: 2
   - Ed25519 signature verification
   - Quote program verified_update
   - Variable overrides: X402_PAYMENT_SIGNATURE

Building transaction...
Simulating transaction...
<transaction logs>

Simulation succeeded!
```

## Next Steps

* Learn about [Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) for other dynamic patterns
* Explore [Custom Feeds](/custom-feeds/build-and-deploy-feed) for building your own oracle jobs
* Check out other [Solana examples](https://github.com/switchboard-xyz/sb-on-demand-examples/tree/main/solana)
* Join our [Discord](https://discord.gg/TJAv6ZYvPC) for support


# EVM

Learn how to build and use programs that call Switchboard data feeds on EVM chains like [Ethereum](https://ethereum.org), [Monad](https://monad.xyz), and [Hyperliquid](https://hyperliquid.xyz).

If you need to create a custom data feed, check out the [custom feeds section](/custom-feeds/build-and-deploy-feed).

If you are integrating a Feed Builder feed or any `bytes32` feed ID from the explorer on EVM, use the v2 feed-hash flow:

1. Fetch the definition with `/v2/fetch/{feedId}`
2. Simulate with `/v2/simulate/{feedHashes}` or `CrossbarClient.simulateFeed(...)`
3. Build the on-chain payload with `/v2/update/{feedHashes}?chain=evm&network=mainnet|testnet&use_timestamp=true`

The legacy `/simulate/evm` and `/updates/evm` routes are only for older aggregator-based integrations.

## Monad Example Network Switch

The packaged Monad examples in `sb-on-demand-examples/evm` now share one network selector:

* `NETWORK=monad-testnet`
* `NETWORK=monad-mainnet`

`RPC_URL` remains optional as an override, but the example scripts now verify that it matches the selected network before they broadcast transactions. The Monad guides under this section use that shared env contract.

## Deployments

The Switchboard contract has been deployed to the following EVM networks:

| Chain    | Network | Chain ID | Address                                      |
| -------- | ------- | -------- | -------------------------------------------- |
| Arbitrum | Mainnet | 42161    | `0xAd9b8604b6B97187CDe9E826cDeB7033C8C37198` |
| Arbitrum | Sepolia | 421614   | `0xA2a0425fA3C5669d384f4e6c8068dfCf64485b3b` |
| Core     | Mainnet | 1116     | `0x33A5066f65f66161bEb3f827A3e40fce7d7A2e6C` |
| Core     | Testnet | 1114     | `0x2f833D73bA1086F3E5CDE9e9a695783984636A76` |
| HyperEVM | Mainnet | 999      | `0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347` |
| Monad    | Mainnet | 143      | `0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67` |
| Monad    | Testnet | 10143    | `0x6724818814927e057a693f4e3A172b6cC1eA690C` |
| Morph    | Mainnet | -        | `0x33A5066f65f66161bEb3f827A3e40fce7d7A2e6C` |
| Morph    | Holesky | -        | `0x3c1604DF82FDc873D289a47c6bb07AFA21f299e5` |


# Price Feeds

Access to reliable, real-world data is essential for decentralised applications (dApps), particularly in Decentralised Finance (DeFi). Real-time asset prices, forming the backbone of any DeFi protocol, are among the most critical data points.

This is where Data Feeds come in. Think of them as secure bridges connecting the off-chain world of financial markets to your on-chain smart contracts. They provide a continuous stream of verified, aggregated price data for a wide range of assets, enabling your dApp to react to market fluctuations and operate correctly.


# 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](/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@1.1.0
```

### 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, it is not a missing deployment step. Check oracle/gateway availability and oracle-side validation errors such as `RangeExceeded`, especially if raw v2 `maxJobRangePct` was not scaled by `1e9`; see [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units) |
| 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](/custom-feeds/build-and-deploy-feed) for specialized data
* Join the [Switchboard Discord](https://discord.gg/TJAv6ZYvPC) for support


# Surge Price Feeds

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

## The Future of Oracle Technology

Switchboard Surge is the industry's fastest oracle data delivery system, providing sub-100ms latency through direct WebSocket streaming. Built for the next generation of DeFi applications, trading systems, and real-time dashboards.

## Key Innovation

Traditional oracles require multiple steps—gathering prices, writing to blockchain state, reaching consensus, and then making data available—resulting in 2-10 seconds of latency.

Switchboard oracles must pass a hardware proof when joining the network, ensuring they run only verified Switchboard code. This allows oracles to stream price data directly from sources to your application via WebSocket, achieving sub-100ms latency.

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Price Sources   │────▶│  Oracle Network  │────▶│  Surge Gateway   │
│   (CEX, DEX)     │     │ (SAIL Verified)  │     │   (WebSocket)    │
└──────────────────┘     └──────────────────┘     └────────┬─────────┘
                                                          │
                                               ┌──────────▼──────────┐
                                               │   Your Application  │
                                               │  • Event Listeners  │
                                               │  • Price Handlers   │
                                               │  • EVM Converter    │
                                               └─────────────────────┘
```

## Key Features

**Unmatched Performance** — Sub-100ms latency with direct WebSocket streaming and event-driven updates. No polling required.

**Zero Setup** — No data feed accounts or on-chain deployment needed. Just use your Solana keypair (subscription owner) and connection to start streaming.

**Cost Efficiency** — Subscription-based pricing with no gas fees for receiving updates. Reduced on-chain costs when submitting to contracts.

**Seamless Integration** — TypeScript/JavaScript SDK, WebSocket API for any language, and EVM format conversion for on-chain use.

**Enterprise-Grade Reliability** — 99.9% uptime SLA with global infrastructure, automatic failover, and professional support.

## User Flow

Surge works the same way regardless of your target chain:

1. **Subscribe** — All Surge subscriptions are managed on Solana, regardless of which chain you're building on. Connect your Solana wallet at the [subscription portal](https://explorer.switchboardlabs.xyz/subscriptions).
2. **Authenticate** — The SDK authenticates your session by signing with your Solana keypair. If the keypair does not have an active subscription, `connectAndSubscribe` will fail.
3. **Stream Prices** — Once subscribed, prices stream directly to your application via WebSocket. No on-chain reads required—this is what enables sub-100ms latency.
4. **Use Prices** — When you need prices on-chain, convert the Surge update to your chain's format and submit it. Switchboard provides SDKs for Solana, EVM, and Sui.

## Getting Started

### 1. Subscribe

Connect your wallet and subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions). If you are an AI agent or wish to subscribe programmatically rather than through the UI, see the [Surge Subscription Guide](/ai-agents-llms/surge-subscription-guide).

### 2. Install the SDK

```bash
npm install @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/common@5.8.5
# or
yarn add @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/common@5.8.5
```

### 3. Connect and Stream

```typescript
import * as sb from "@switchboard-xyz/on-demand";
import { EVMUtils } from "@switchboard-xyz/common";

// Initialize with Solana keypair and connection (uses on-chain subscription)
const surge = new sb.Surge({ connection, keypair }); // keypair = Solana keypair with active Surge subscription
// `connection` is a Solana RPC Connection from @solana/web3.js, not an EVM provider.

// Auth note: the SDK signs with your keypair to authenticate the session.
// If the keypair has no active Surge subscription, connectAndSubscribe will fail.

// Discover available feeds
const availableFeeds = await surge.getSurgeFeeds();
console.log(`${availableFeeds.length} feeds available`);

// Subscribe to specific feeds
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'ETH/USD' },
]);

// Handle price updates
surge.on('signedPriceUpdate', (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();
  metrics.perFeedMetrics.forEach((feed) => {
    console.log(`${feed.symbol}: ${prices[feed.feed_hash]}`);

    // Convert to EVM format when needed for on-chain use
    const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(response.getRawResponse());
  });
});
```

## Pricing & Limits

| Plan           | Price       | Quote Interval | Max Feeds | Max Connections |
| -------------- | ----------- | -------------- | --------- | --------------- |
| **Plug**       | Free        | 10s            | 2         | 1               |
| **Pro**        | \~$3,000/mo | 450ms          | 100       | 10              |
| **Enterprise** | \~$7,500/mo | 0ms            | 300       | 15              |

Subscriptions are paid in SWTCH tokens. For custom limits or dedicated support, contact <sales@switchboard.xyz>.

## Primary Use Cases

### Perpetual Exchanges

Surge is the perfect oracle solution for perpetual trading platforms:

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));

    // Update mark price instantly
    await updateMarkPrice(feed.symbol, price);

    // Check for liquidations with latest price
    const liquidations = await checkLiquidations(feed.symbol, price);
    if (liquidations.length > 0) {
      const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(response.getRawResponse());
      await executeLiquidations(liquidations, evmEncoded);
    }
  }
});
```

### Oracle-Based AMMs

Build the next generation of AMMs that use real-time oracle prices:

```typescript
class OracleAMM {
  private latestUpdate: sb.SurgeUpdate;

  async handlePriceUpdate(response: sb.SurgeUpdate) {
    const metrics = response.getLatencyMetrics();
    if (metrics.isHeartbeat) return;

    this.latestUpdate = response;
    const prices = response.getFormattedPrices();

    for (const feed of metrics.perFeedMetrics) {
      const pair = this.pairs.get(feed.symbol);
      pair.oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
      pair.lastUpdate = Date.now();
    }
  }

  async executeSwap(tokenIn: string, tokenOut: string, amountIn: number) {
    const pair = `${tokenIn}/${tokenOut}`;
    const latestPrice = this.pairs.get(pair).oraclePrice;
    const amountOut = amountIn * latestPrice * (1 - this.swapFee);

    const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(this.latestUpdate.getRawResponse());
    return await this.contract.swap(amountIn, amountOut, evmEncoded);
  }
}
```

### High-Frequency Trading & Arbitrage

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const dexPrice = await getDexPrice(feed.symbol);

    const spread = Math.abs(dexPrice - oraclePrice) / oraclePrice;
    if (spread > MIN_PROFIT_THRESHOLD) {
      const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(response.getRawResponse());
      await executeArbitrage(evmEncoded, calculateOptimalSize(spread));
    }
  }
});
```

### Liquidation Engines

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const positions = await getPositionsByCollateral(feed.symbol);

    for (const position of positions) {
      const ltv = calculateLTV(position, price);
      if (ltv > LIQUIDATION_THRESHOLD) {
        const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(response.getRawResponse());
        await liquidatePosition(position, evmEncoded);
      }
    }
  }
});
```

## Technical Specifications

### Latency Breakdown

* Oracle processing: \~10ms
* Network transmission: \~20-50ms
* Client processing: \~10ms
* **Total: <100ms**

### Discovering Available Feeds

Use the `getSurgeFeeds()` method to see all available trading pairs:

```typescript
const surge = new sb.Surge({ connection, keypair }); // Solana keypair with active Surge subscription
// `connection` is a Solana RPC Connection from @solana/web3.js, not an EVM provider.
const feeds = await surge.getSurgeFeeds();

feeds.forEach(feed => {
  console.log(`${feed.symbol}`);
});
```

### Supported Assets

* All major cryptocurrency pairs
* Multiple exchange sources available
* New pairs added regularly
* Custom feeds available on request

Note: Surge does not support custom feeds created with the [feed builder](https://explorer.switchboardlabs.xyz/feed-builder).

## FAQ

### How is Surge different from traditional oracles?

Surge streams data directly to your application via WebSocket, bypassing the blockchain entirely for reads. This eliminates gas costs and reduces latency from seconds to milliseconds.

### Can I use Surge data on-chain?

Yes! Surge updates can be converted to EVM-compatible format using `EVMUtils.convertSurgeUpdateToEvmFormat()` and submitted to your smart contracts.

### What's the reliability?

Surge operates with 99.9% uptime SLA, automatic failover, and global redundancy. Enterprise customers get dedicated infrastructure.

### How do I handle disconnections?

The SDK includes automatic reconnection logic with exponential backoff. Your application will seamlessly recover from network interruptions.

## Next Steps

* [Surge Tutorial](/docs-by-chain/evm/surge/surge-tutorial) - Step-by-step implementation guide
* [Crossbar Gateway](/tooling/crossbar) - Stream prices to your frontend
* [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol) - Advanced HTTP + WebSocket protocol
* [Explore code examples](https://github.com/switchboard-xyz/sb-on-demand-examples)
* [Join our Discord](https://discord.gg/TJAv6ZYvPC)


# Surge 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) (see `scripts/surgeToEvmConversion.ts`)

This tutorial walks you through converting Switchboard Surge real-time price updates into EVM-compatible format for use with your smart contracts.

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

## What You'll Build

A TypeScript script that:

* Receives Surge real-time price updates
* Converts them to EVM-encoded format
* Submits the data to your smart contracts

## Prerequisites

* **Bun** or Node.js 20+
* Basic understanding of hexadecimal encoding

If you're also streaming Surge updates yourself, note that the SDK authenticates by signing with a Solana keypair that has an active Surge subscription. Without an active subscription, `connectAndSubscribe` will fail.

## The Conversion Flow

```
Surge WebSocket → SurgeRawGatewayResponse → EVMUtils.convertSurgeUpdateToEvmFormat() → bytes → Smart Contract
```

## The SurgeRawGatewayResponse Structure

When you receive a Surge update, it has this structure:

```typescript
interface SurgeRawGatewayResponse {
  type: 'bundle_update';
  feed_bundle_id: string;
  feed_values: Array<{
    value: string;           // Price value as string (wei-like format)
    feed_hash: string;       // 32-byte hex feed identifier
    symbol: string;          // Human-readable symbol (e.g., "BTC/USD")
    source: string;          // Data source
  }>;
  oracle_response: {
    oracle_pubkey: string;   // Oracle's public key
    eth_address: string;     // Oracle's Ethereum address
    signature: string;       // Base64-encoded 64-byte signature
    recovery_id: number;     // ECDSA recovery ID (v value)
    timestamp: number;       // Unix timestamp in seconds
    slot: number;            // Solana slot number
    // ... additional fields
  };
}
```

## The Conversion Script

Here's a complete example that converts Surge updates to EVM format:

```typescript
import { EVMUtils, type SurgeRawGatewayResponse } from '@switchboard-xyz/common';
import * as fs from 'fs';

// Sample Surge update data
const sampleSurgeUpdate: SurgeRawGatewayResponse = {
  type: 'bundle_update',
  feed_bundle_id: 'sample-bundle-id',
  feed_values: [
    {
      value: '1000000000000000000',  // 1e18
      feed_hash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
      symbol: 'BTC/USD',
      source: 'switchboard'
    },
    {
      value: '2500000000000000000',  // 2.5e18
      feed_hash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
      symbol: 'ETH/USD',
      source: 'switchboard'
    }
  ],
  oracle_response: {
    oracle_pubkey: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
    eth_address: '0x742d35Cc6634C0532925a3b8D4C0B4E5C0C8b6C9',
    signature: 'Zl+8HHAyFbKTmaH66HEkQ/4nKRGYKWV8YOjPT9JcGdEhZzy+qI9OKhF3m+nmz9mbegPJRtIJdLfdi1o7wjZCaw==',
    checksum: 'sample-checksum',
    recovery_id: 0,
    oracle_idx: 0,
    timestamp: Math.floor(Date.now() / 1000),
    recent_hash: '0xdeadbeefcafebabe',
    slot: 12345678
  },
  source_ts_ms: Date.now(),
  seen_at_ts_ms: Date.now(),
  triggered_on_price_change: true,
  message: 'Sample surge update'
};

async function convertSurgeToEvm() {
  // Load surge data (from file or use sample)
  let surgeData: SurgeRawGatewayResponse;

  const surgeDataFile = process.env.SURGE_DATA_FILE;
  if (surgeDataFile && fs.existsSync(surgeDataFile)) {
    const fileContent = fs.readFileSync(surgeDataFile, 'utf-8');
    surgeData = JSON.parse(fileContent);
  } else {
    surgeData = sampleSurgeUpdate;
  }

  // Perform the conversion
  const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(surgeData, {
    minOracleSamples: 1 // unscaled oracle-sample quorum
  });

  console.log('Encoded Data:', evmEncoded);
  console.log('Length:', (evmEncoded.length - 2) / 2, 'bytes');

  return evmEncoded;
}

convertSurgeToEvm();
```

### The convertSurgeUpdateToEvmFormat Function

```typescript
import { EVMUtils } from '@switchboard-xyz/common';

const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(surgeData, {
  minOracleSamples: 1  // Minimum oracle samples required; unscaled count
});
```

This function takes the raw Surge response and returns a `0x`-prefixed hex string that your smart contract can parse.

## EVM Data Structure

The encoded data follows this binary format:

| Field                | Size     | Description                    |
| -------------------- | -------- | ------------------------------ |
| Slot                 | 8 bytes  | Solana slot number             |
| Timestamp            | 8 bytes  | Unix timestamp                 |
| Number of Feeds      | 1 byte   | Count of feeds in update       |
| Number of Signatures | 1 byte   | Count of oracle signatures     |
| Feed Data            | Variable | Per-feed data (see below)      |
| Signature Data       | Variable | Per-signature data (see below) |

### Feed Data (per feed)

| Field       | Size     | Description            |
| ----------- | -------- | ---------------------- |
| Feed Hash   | 32 bytes | Feed identifier        |
| Value       | 16 bytes | Price value (int128)   |
| Min Samples | 1 byte   | Minimum oracle samples |

### Signature Data (per signature)

| Field       | Size     | Description            |
| ----------- | -------- | ---------------------- |
| Signature   | 64 bytes | ECDSA signature (r, s) |
| Recovery ID | 1 byte   | ECDSA recovery ID (v)  |

## Parsing the Encoded Data

To understand what the encoded data contains, you can parse it:

```typescript
function parseEvmEncodedData(evmEncoded: string) {
  const hexData = evmEncoded.slice(2); // Remove 0x prefix
  let offset = 0;

  // Parse header
  const slot = parseInt(hexData.slice(offset, offset + 16), 16);
  offset += 16;

  const timestamp = parseInt(hexData.slice(offset, offset + 16), 16);
  offset += 16;

  const numFeeds = parseInt(hexData.slice(offset, offset + 2), 16);
  offset += 2;

  const numSigs = parseInt(hexData.slice(offset, offset + 2), 16);
  offset += 2;

  console.log('Slot:', slot);
  console.log('Timestamp:', new Date(timestamp * 1000).toISOString());
  console.log('Number of Feeds:', numFeeds);
  console.log('Number of Signatures:', numSigs);

  // Parse feeds
  for (let i = 0; i < numFeeds; i++) {
    const feedHash = '0x' + hexData.slice(offset, offset + 64);
    offset += 64;
    const value = '0x' + hexData.slice(offset, offset + 32);
    offset += 32;
    const minSamples = parseInt(hexData.slice(offset, offset + 2), 16);
    offset += 2;

    console.log(`Feed ${i + 1}:`, { feedHash, value, minSamples });
  }

  // Parse signatures
  for (let i = 0; i < numSigs; i++) {
    const signature = '0x' + hexData.slice(offset, offset + 128);
    offset += 128;
    const recoveryId = parseInt(hexData.slice(offset, offset + 2), 16);
    offset += 2;

    console.log(`Signature ${i + 1}:`, {
      signature: signature.slice(0, 20) + '...',
      recoveryId
    });
  }
}
```

## Using with Smart Contracts

Once you have the encoded data, submit it to Switchboard:

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

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

contract SurgePriceConsumer {
    ISwitchboard public immutable switchboard;

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

    function updateFromSurge(bytes calldata surgeUpdateData) external payable {
        // Get the required fee
        bytes[] memory updates = new bytes[](1);
        updates[0] = surgeUpdateData;

        uint256 fee = switchboard.getFee(updates);
        require(msg.value >= fee, "Insufficient fee");

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

        // Data is now verified and available via latestUpdate()
    }
}
```

### TypeScript Integration

```typescript
import * as ethers from "ethers";
import { EVMUtils, type SurgeRawGatewayResponse } from "@switchboard-xyz/common";

async function submitSurgeUpdate(
  contract: ethers.Contract,
  switchboard: ethers.Contract,
  surgeData: SurgeRawGatewayResponse
) {
  // Convert Surge update to EVM format
  const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(surgeData, {
    minOracleSamples: 1 // unscaled oracle-sample quorum
  });

  // Get fee and submit
  const fee = await switchboard.getFee([evmEncoded]);
  const tx = await contract.updateFromSurge(evmEncoded, { value: fee });

  await tx.wait();
  console.log("Surge update submitted:", tx.hash);
}
```

## Full Integration Pattern

Here's how to combine Surge WebSocket streaming with EVM submission:

```typescript
import { EVMUtils, type SurgeRawGatewayResponse } from "@switchboard-xyz/common";
import * as ethers from "ethers";

// Connect to your contract
const provider = new ethers.JsonRpcProvider("https://rpc.hyperliquid.xyz/evm");
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const contract = new ethers.Contract(contractAddress, abi, signer);

// Handle incoming Surge updates
async function handleSurgeUpdate(surgeData: SurgeRawGatewayResponse) {
  try {
    // Convert to EVM format
    const evmEncoded = EVMUtils.convertSurgeUpdateToEvmFormat(surgeData, {
      minOracleSamples: 1 // unscaled oracle-sample quorum
    });

    // Submit to chain
    const updates = [evmEncoded];
    const fee = await switchboard.getFee(updates);

    const tx = await contract.updatePrices(updates, { value: fee });
    console.log("Submitted:", tx.hash);

    await tx.wait();
    console.log("Confirmed");

  } catch (error) {
    console.error("Failed to submit:", error);
  }
}

// Connect to Surge WebSocket (pseudo-code)
// const ws = new WebSocket(SURGE_WS_URL);
// ws.onmessage = (event) => {
//   const surgeData = JSON.parse(event.data);
//   handleSurgeUpdate(surgeData);
// };
```

## 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
```

### 3. Run the Conversion Example

```bash
# With sample data
bun run surge-convert

# With custom surge data file
SURGE_DATA_FILE=path/to/surge-data.json bun run surge-convert
```

### Expected Output

```
Using sample surge data
Input Surge Update Summary:
   - Type: bundle_update
   - Feed Count: 2
   - Timestamp: 1734523800 (2024-12-18T12:30:00.000Z)
   - Slot: 12345678

Converting to EVM format...

EVM Conversion Results:
   - Encoded Data: 0x00000000bc614e0000000000675946f80201...
   - Length: 284 hex characters (142 bytes)

Parsing EVM Structure:
   - Slot: 12345678
   - Timestamp: 1734523800 (2024-12-18T12:30:00.000Z)
   - Number of Feeds: 2
   - Number of Signatures: 1
   - Feed Data:
     Feed 1:
       - Hash: 0x1234567890abcdef...
       - Value: 0x0de0b6b3a7640000
       - Min Samples: 1
     Feed 2:
       - Hash: 0xabcdef1234567890...
       - Value: 0x22b1c8c1227a0000
       - Min Samples: 1
```

## Troubleshooting

| Error                 | Solution                                                            |
| --------------------- | ------------------------------------------------------------------- |
| `Invalid surge data`  | Ensure the input matches `SurgeRawGatewayResponse` structure        |
| `Missing signature`   | The `oracle_response.signature` field must be a valid base64 string |
| `Invalid recovery_id` | Must be 0 or 1                                                      |
| `Fee errors on-chain` | Query `switchboard.getFee()` with your encoded data                 |

## Next Steps

* Learn about [on-demand price feeds](/docs-by-chain/evm/price-feeds/price-feeds-tutorial) for pull-based updates
* Explore [randomness integration](/docs-by-chain/evm/randomness/randomness-tutorial) for gaming and NFTs
* Check out the [Sui Surge tutorial](/docs-by-chain/sui/surge/surge-tutorial) for comparison


# Randomness

Blockchain users want randomness for many applications like gaming, NFT mints, lotteries, and more. However, this poses a fundamental challenge to blockchains, which are deterministic computers replicated across many nodes across the globe. Each node needs to produce the exact same output when given the same sequence of inputs.

<figure><img src="/files/hqZiVzDcFisv0Ko9DIqg" alt=""><figcaption></figcaption></figure>

Imagine if an on-chain lottery was deciding whether to mint an NFT to Alice or Bob. If blockchain nodes ran their own randomness and some decided that the NFT would go to Alice, and others to Bob, there would be a state mismatch.

<figure><img src="/files/b5FAOjoWFqsBuEfq5ykn" alt=""><figcaption></figcaption></figure>

This is where oracles come in. An oracle can run the randomness off-chain and then post a single result to the blockchain, ensuring that all nodes agree on the result of the randomness.

<figure><img src="/files/VvEFCi1KYVCekLCqFs4e" alt=""><figcaption></figcaption></figure>

However, as a third-party source of randomness, it's critical to make sure that nefarious actors cannot control the oracle and bias the randomness in their favor.

<figure><img src="/files/lsY5MpB2NSUNrQxRwNjn" alt=""><figcaption></figcaption></figure>

As an oracle provider, Switchboard's network serves as a trusted and verified third-party that can post fair random numbers to the blockchain.

<figure><img src="/files/FzjwQbEW0q1Nik84M5yI" alt=""><figcaption></figcaption></figure>

## Switchboard's approach

Switchboard leverages Trusted Execution Environments (TEEs), which are protected areas inside of a computer's processing unit that cannot be altered or inspected. This means:

* No one, including the oracle operator, can alter the code that’s running on the TEEs
* No one, including the oracle operator, can see what’s going on inside the chip, only inputs and outputs.

This means that Switchboard oracles can generate safe and fair randomness that is free from malicious influence. As an extra layer of protection, Switchboard network incentives ensure that oracle oeprators that misbehave by experiencing downtime or withholding results can have their $SWTCH stake slashed.

## How to Use Switchboard Randomness

For the current EVM interface, the public randomness flow uses `createRandomness`, `settleRandomness`, and `getRandomness`. `revealRandomness` and `getRandomnessResult` are not current Switchboard methods.

The canonical JS/TS ABI lives at `@switchboard-xyz/on-demand-solidity/abis/Switchboard.json`.

To understand the flow, it's helpful to visualize the following 5 parties.

* **Alice**: blockchain user
* **App**: on-chain application
* **Switchboard Contract**: on-chain contract that handles anything Switchboard-related.
* **Crossbar**: server that helps you talk to oracles
* **Oracle**: generates randomness

<figure><img src="/files/aiqbP0k5zb27cvYFnKAl" alt=""><figcaption></figcaption></figure>

There are two stages, requesting and resolving the randomness.

### Request Randomness

* First, **Alice** talks to the **App** requesting some random event.
* The **App** then generates a randomness request with a unique ID and sends it to the **Switchboard contract**.
* The **Switchboard contract** responds to the **App** with an oracle assignment.
* The **App** responds to **Alice** with the oracle assignment and randomness ID.

### Resolve Randomness

* **Alice** sends the oracle assignment, randomness ID, and some other data to **Crossbar** to get the randomness.
* **Crossbar** asks the **Oracle** to generate randomness.
* The **Oracle** creates a randomness object and sends it to **Crossbar** which passes it back to **Alice**.
* **Alice** sends the randomness object to the **App**.
* The **App** asks the **Switchboard contract** to verify that the randomness it received from Alice is correct.
* If all is well, the **Switchboard contract** sends verification to the **App**, resolving the random event.

\--


# Randomness Tutorial

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

## Try It Out

Play Pancake Stacker directly in your browser! Connect your wallet (MetaMask or Phantom), enter the contract address, and start flipping pancakes.

[**Play Pancake Stacker**](https://switchboard-xyz.github.io/sb-on-demand-examples/evm/randomness/pancake-stacker/ui/index.html)

**Contract Address (Monad):** `0x8A48241ba47298BBCb417834C6A95860D4273B6B`

***

This tutorial walks you through building **Pancake Stacker**, a simple on-chain game that demonstrates Switchboard's randomness system. You'll learn how to request, resolve, and use verifiable randomness in your EVM smart contracts.

> **Version source of truth:** [SDK Version Matrix](/tooling/sdk-version-matrix)
>
> **API note:** The current EVM randomness methods are `createRandomness`, `settleRandomness`, and `getRandomness`. Use the canonical ABI at `@switchboard-xyz/on-demand-solidity/abis/Switchboard.json`. `revealRandomness` and `getRandomnessResult` are not part of the current interface.

## What You'll Build

A game where players flip pancakes onto a stack:

* Each flip has a **2/3 chance** of landing successfully
* Successful flips increase your stack height
* A failed flip knocks over your entire stack (resets to 0)
* Randomness is generated off-chain by Switchboard oracles and verified on-chain

## Mapping to the Randomness Flow

Before diving into code, let's connect this example to the [conceptual flow](/docs-by-chain/evm/randomness#how-to-use-switchboard-randomness) described in the Randomness Overview. The five parties are:

| Conceptual Party         | In Pancake Stacker                              |
| ------------------------ | ----------------------------------------------- |
| **Alice**                | You (running the script or using the UI)        |
| **App**                  | `PancakeStacker` smart contract                 |
| **Switchboard Contract** | `ISwitchboard` interface                        |
| **Crossbar**             | `CrossbarClient` from `@switchboard-xyz/common` |
| **Oracle**               | Switchboard oracle (generates the randomness)   |

The two-stage flow maps directly to our two main functions:

* **Request Randomness** → `flipPancake()`
* **Resolve Randomness** → `catchPancake()`

## Prerequisites

* **Foundry** installed for Solidity development
* **Bun** or Node.js 20+
* Native tokens for gas (e.g., MON for Monad)
* Basic understanding of Solidity and ethers.js

## Installation

1. **Solidity SDK:**

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

2. **TypeScript SDK** (for off-chain randomness resolution):

```bash
npm install @switchboard-xyz/common@5.8.5 ethers
```

3. **Forge remappings** - Add to `remappings.txt`:

```
@switchboard-xyz/on-demand-solidity/=node_modules/@switchboard-xyz/on-demand-solidity
```

***

## The Smart Contract

### Imports and State Variables

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import { ISwitchboard } from '@switchboard-xyz/on-demand-solidity/interfaces/ISwitchboard.sol';
import { SwitchboardTypes } from '@switchboard-xyz/on-demand-solidity/libraries/SwitchboardTypes.sol';

contract PancakeStacker {

    // Pending flip randomness ID for each user (bytes32(0) = no pending flip)
    mapping(address => bytes32) public pendingFlips;

    // Current stack height for each player
    mapping(address => uint256) public stackHeight;

    // Reference to the Switchboard contract
    ISwitchboard public switchboard;

    constructor(address _switchboard) {
        require(_switchboard != address(0), "Invalid switchboard address");
        switchboard = ISwitchboard(_switchboard);
    }
```

The contract tracks:

* `pendingFlips`: Maps each user to their pending randomness request ID
* `stackHeight`: Maps each user to their current pancake stack count
* `switchboard`: Reference to the Switchboard contract for randomness operations

### Events

```solidity
    event PancakeFlipRequested(address indexed user, bytes32 randomnessId);
    event PancakeLanded(address indexed user, uint256 newStackHeight);
    event StackKnockedOver(address indexed user);
    event SettlementFailed(address indexed user);
```

Events allow the off-chain script (or UI) to track the outcome of each flip.

### Requesting Randomness: flipPancake()

This function implements the **Request Randomness** phase from the overview:

```solidity
    function flipPancake() public {
        // Check no pending flip exists
        require(pendingFlips[msg.sender] == bytes32(0), "Already have pending flip");

        // Generate unique randomnessId using sender address and last blockhash
        bytes32 randomnessId = keccak256(abi.encodePacked(msg.sender, blockhash(block.number - 1)));

        // Ask Switchboard to create a new randomness request with 1 second settlement delay
        switchboard.createRandomness(randomnessId, 1);

        // Store the randomness request as a pending flip
        pendingFlips[msg.sender] = randomnessId;

        emit PancakeFlipRequested(msg.sender, randomnessId);
    }
```

**Flow mapping:**

1. Alice calls the App (`flipPancake()`)
2. App generates a unique `randomnessId` and calls Switchboard Contract (`createRandomness`)
3. Switchboard Contract assigns an oracle and stores the request
4. App emits event with the `randomnessId` for Alice to use later

### Resolving Randomness: catchPancake()

This function implements the **Resolve Randomness** phase:

```solidity
    function catchPancake(bytes calldata encodedRandomness) public {
        // Make sure caller has a pending flip
        bytes32 randomnessId = pendingFlips[msg.sender];
        require(randomnessId != bytes32(0), "No pending flip");

        // Clear the pending flip BEFORE external calls (CEI pattern)
        delete pendingFlips[msg.sender];

        // Ask Switchboard to verify the randomness is correct
        try switchboard.settleRandomness(encodedRandomness) {

            // Verification succeeded, get the randomness value
            SwitchboardTypes.Randomness memory randomness = switchboard.getRandomness(randomnessId);

            // Verify the randomness ID matches what we requested
            require(randomness.randId == randomnessId, "Randomness ID mismatch");

            // 2/3 chance to land (0 or 1), 1/3 chance to knock over (2)
            bool landed = uint256(randomness.value) % 3 < 2;

            if (landed) {
                stackHeight[msg.sender]++;
                emit PancakeLanded(msg.sender, stackHeight[msg.sender]);
            } else {
                stackHeight[msg.sender] = 0;
                emit StackKnockedOver(msg.sender);
            }

        } catch {
            // Settlement failed - reset stack to be safe
            stackHeight[msg.sender] = 0;
            emit StackKnockedOver(msg.sender);
            emit SettlementFailed(msg.sender);
        }
    }
```

**Flow mapping:**

1. Alice sends the randomness object (obtained from Crossbar) to the App
2. App asks Switchboard Contract to verify the randomness (`settleRandomness`)
3. If valid, App retrieves the value (`getRandomness`) and applies game logic
4. App emits the outcome event

### Helper Function: getFlipData()

This view function provides the data needed for off-chain resolution:

```solidity
    function getFlipData(address user) public view returns (
        bytes32 randomnessId,
        address oracle,
        uint256 rollTimestamp,
        uint256 minSettlementDelay
    ) {
        randomnessId = pendingFlips[user];
        SwitchboardTypes.Randomness memory randomness = switchboard.getRandomness(randomnessId);
        return (randomnessId, randomness.oracle, randomness.rollTimestamp, randomness.minSettlementDelay);
    }
```

***

## The Off-Chain Script

The `stackPancake.ts` script demonstrates how to interact with the contract from off-chain. While the example repository also includes a React UI, the script is simpler to understand and follows the exact same flow.

> **Note**: The UI code does the same thing as this script, but with React state management and a visual interface. Understanding this script is sufficient to understand how any client (UI, bot, etc.) interacts with the randomness system.

### Setup

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

// Contract ABI (only the functions we need)
const PANCAKE_STACKER_ABI = [
    "function flipPancake() public",
    "function catchPancake(bytes calldata encodedRandomness) public",
    "function getFlipData(address user) public view returns (bytes32 randomnessId, address oracle, uint256 rollTimestamp, uint256 minSettlementDelay)",
    "function getPlayerStats(address user) public view returns (uint256 currentStack, bool hasPendingFlip)",
    "event PancakeLanded(address indexed user, uint256 newStackHeight)",
    "event StackKnockedOver(address indexed user)",
    "event SettlementFailed(address indexed user)",
];

async function main() {
    // Resolve the target network. The packaged example defaults to Monad testnet
    // and lets you flip to mainnet by setting NETWORK=monad-mainnet.
    const networkName = process.env.NETWORK || "monad-testnet";
    const rpcUrl =
        process.env.RPC_URL ||
        (networkName === "monad-mainnet"
            ? "https://rpc.monad.xyz"
            : "https://testnet-rpc.monad.xyz");
    const provider = new ethers.JsonRpcProvider(rpcUrl);
    const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
    const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

    const contract = new ethers.Contract(
        process.env.PANCAKE_STACKER_CONTRACT_ADDRESS!,
        PANCAKE_STACKER_ABI,
        wallet
    );
```

### Step 1: Check Current Stats

```typescript
    const [currentStack] = await contract.getPlayerStats(wallet.address);
    console.log(`Current stack: ${currentStack} pancakes`);
```

### Step 2: Request Randomness (Flip the Pancake)

```typescript
    // Call flipPancake() to request randomness
    const tx = await contract.flipPancake();
    await tx.wait();
    console.log("Flip requested:", tx.hash);
```

This triggers the **Request Randomness** phase on-chain.

### Step 3: Get Flip Data

```typescript
    // Retrieve the data needed for off-chain resolution
    const flipData = await contract.getFlipData(wallet.address);
```

The contract returns:

* `randomnessId`: Unique identifier for this request
* `oracle`: Address of the assigned oracle
* `rollTimestamp`: When the randomness was rolled
* `minSettlementDelay`: Minimum wait time before settlement

### Step 4: Resolve Randomness via Crossbar

```typescript
    // Get chain ID dynamically
    const network = await provider.getNetwork();
    const chainId = Number(network.chainId);

    // Ask Crossbar to get randomness from the oracle
    const { encoded } = await crossbar.resolveEVMRandomness({
        chainId,
        randomnessId: flipData.randomnessId,
        timestamp: Number(flipData.rollTimestamp),
        minStalenessSeconds: Number(flipData.minSettlementDelay),
        oracle: flipData.oracle,
    });
```

This is where **Crossbar** talks to the **Oracle** and returns the encoded randomness proof.

### Step 5: Settle On-Chain (Catch the Pancake)

```typescript
    // Submit the encoded randomness to the contract
    const tx2 = await contract.catchPancake(encoded);
    const receipt = await tx2.wait();
```

### Step 6: Parse Events for Outcome

```typescript
    for (const log of receipt.logs) {
        try {
            const parsed = contract.interface.parseLog(log);

            if (parsed?.name === "PancakeLanded") {
                console.log(`PANCAKE LANDED! Stack height: ${parsed.args.newStackHeight}`);
            }

            if (parsed?.name === "StackKnockedOver") {
                console.log("STACK KNOCKED OVER!");
            }

            if (parsed?.name === "SettlementFailed") {
                console.log("SETTLEMENT FAILED - stack reset");
            }
        } catch {}
    }
```

***

## Complete Flow Diagram

```
┌─────────────────────────────────────────────────────────────────────┐
│                        REQUEST RANDOMNESS                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  You (Alice)                                                        │
│      │                                                              │
│      │  1. Call flipPancake()                                       │
│      ▼                                                              │
│  PancakeStacker (App)                                               │
│      │                                                              │
│      │  2. Generate randomnessId                                    │
│      │  3. Call switchboard.createRandomness()                      │
│      ▼                                                              │
│  Switchboard Contract                                               │
│      │                                                              │
│      │  4. Assign oracle, store request                             │
│      │  5. Return to App                                            │
│      ▼                                                              │
│  PancakeStacker emits PancakeFlipRequested event                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                        RESOLVE RANDOMNESS                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  You (Alice)                                                        │
│      │                                                              │
│      │  1. Call getFlipData() to get oracle info                    │
│      │  2. Call crossbar.resolveEVMRandomness()                     │
│      ▼                                                              │
│  Crossbar                                                           │
│      │                                                              │
│      │  3. Request randomness from Oracle                           │
│      ▼                                                              │
│  Oracle                                                             │
│      │                                                              │
│      │  4. Generate randomness, sign it                             │
│      │  5. Return encoded proof to Crossbar                         │
│      ▼                                                              │
│  Crossbar returns encoded randomness to You                         │
│      │                                                              │
│      │  6. Call catchPancake(encoded)                               │
│      ▼                                                              │
│  PancakeStacker (App)                                               │
│      │                                                              │
│      │  7. Call switchboard.settleRandomness()                      │
│      ▼                                                              │
│  Switchboard Contract verifies proof                                │
│      │                                                              │
│      │  8. Return success                                           │
│      ▼                                                              │
│  PancakeStacker applies game logic, emits result event              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

***

## Security Considerations

### CEI Pattern (Checks-Effects-Interactions)

Notice that `catchPancake()` clears `pendingFlips[msg.sender]` **before** making external calls:

```solidity
// Clear the pending flip BEFORE external calls (CEI pattern)
delete pendingFlips[msg.sender];

// Then make external call
try switchboard.settleRandomness(encodedRandomness) { ... }
```

This prevents reentrancy attacks.

### Settlement Delay

The `minSettlementDelay` (set to 1 second in this example) ensures the randomness can't be resolved instantly. This gives the oracle time to generate the randomness after the request is made, preventing manipulation.

### Try-Catch for Settlement

The contract gracefully handles settlement failures by catching exceptions and resetting the player's state, rather than leaving them stuck with an unresolvable pending flip.

***

## Running the Example

### 1. Clone and Install

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/evm/randomness/pancake-stacker
bun install  # or npm install
[ -d lib/forge-std ] || forge install foundry-rs/forge-std --no-git --shallow
forge build
```

### 2. Configure Environment

> **Security:** Never use `export PRIVATE_KEY=...` in shell history. Put secrets in a local `.env` file instead.

Copy the example file and fill in your values:

```bash
cp .env.example .env
```

### 3. Deploy the Contract

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

# Monad mainnet with the same deploy flow
NETWORK=monad-mainnet bun run deploy
```

### 4. Run the Script

Update `.env` with your deployed contract address:

```bash
PRIVATE_KEY=0x...
NETWORK=monad-testnet
PANCAKE_STACKER_CONTRACT_ADDRESS=0x_your_deployed_address
RPC_URL=
```

```bash
bun run flip
```

### Expected Output

```
Current stack: 0 pancakes

Flipping pancake...
Flip requested: 0x...

Resolving randomness...
Catching pancake...

========================================
PANCAKE LANDED!
Stack height: 1 pancakes
========================================

Your stack: 1 pancakes
```

***

## Summary

You've now learned how to integrate Switchboard randomness into an EVM smart contract:

1. **Request**: Call `switchboard.createRandomness()` with a unique ID
2. **Resolve**: Use `CrossbarClient.resolveEVMRandomness()` to get the oracle's signed randomness
3. **Settle**: Call `switchboard.settleRandomness()` to verify and `getRandomness()` to retrieve the value
4. **Use**: Apply the random value to your game logic

This pattern works for any application requiring verifiable randomness: games, NFT mints, lotteries, and more.


# Monad

Monad is the primary EVM network exercised by the current `sb-on-demand-examples` repo. The packaged EVM examples now share a single network switch:

* `NETWORK=monad-testnet`
* `NETWORK=monad-mainnet`

If `NETWORK` is unset, the examples default to `monad-testnet`.

## Network Information

| Network       | Chain ID | Default RPC                     | Switchboard Proxy                            |
| ------------- | -------- | ------------------------------- | -------------------------------------------- |
| Monad Testnet | `10143`  | `https://testnet-rpc.monad.xyz` | `0x6724818814927e057a693f4e3A172b6cC1eA690C` |
| Monad Mainnet | `143`    | `https://rpc.monad.xyz`         | `0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67` |

`RPC_URL` remains available as an override, but it must still resolve to the chain implied by `NETWORK`.

## Monad Mainnet Contract Details

Monad mainnet uses an ERC1967 proxy. The address apps integrate with is the proxy at `0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67`, and the user-facing ABI is the Switchboard implementation ABI.

| Item                     | Value                                                                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Proxy address            | `0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67`                                                                                                     |
| Proxy code page          | [MonadScan proxy code](https://monadscan.com/address/0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67#code)                                            |
| Current implementation   | `0x140E3f2E66619FE1113D971291990caC0b5b72Fd`                                                                                                     |
| Implementation code page | [MonadScan implementation code](https://monadscan.com/address/0x140E3f2E66619FE1113D971291990caC0b5b72Fd#code)                                   |
| Canonical ABI            | `@switchboard-xyz/on-demand-solidity/abis/Switchboard.json`                                                                                      |
| ABI source               | [Switchboard ABI in `on-demand-solidity`](https://github.com/switchboard-xyz/sbv3/blob/main/javascript/on-demand-solidity/abis/Switchboard.json) |

> MonadScan code visibility for the live mainnet implementation is still being repaired from the exact deployment source. Until that is finished, use the package ABI above instead of guessing from an empty or stale explorer ABI.

## Randomness API Note

The current EVM randomness interface is:

* `createRandomness`
* `settleRandomness`
* `getRandomness`

`revealRandomness` and `getRandomnessResult` are not part of the current Switchboard EVM interface on Monad.

## Shared Env Contract

The runnable EVM examples use the same env model:

```bash
PRIVATE_KEY=0xyour_private_key_here
NETWORK=monad-testnet
RPC_URL=
SWITCHBOARD_ADDRESS=
```

Per-example contract addresses stay separate:

* `CONTRACT_ADDRESS` for `evm/price-feeds`
* `COIN_FLIP_CONTRACT_ADDRESS` for `evm/randomness/coin-flip`
* `PANCAKE_STACKER_CONTRACT_ADDRESS` for `evm/randomness/pancake-stacker`

## Guardrails

Before broadcasting transactions, the packaged scripts verify:

* `NETWORK` is supported
* the RPC chain ID matches `NETWORK`
* the resolved Switchboard contract has bytecode
* Monad `SWITCHBOARD_ADDRESS` overrides match the canonical address for the selected network
* any reused contract address already has deployed bytecode

## Quick Start: Price Feeds

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples.git
cd sb-on-demand-examples/evm/price-feeds
bun install
(
  cd ../randomness/coin-flip
  [ -d lib/forge-std ] || forge install foundry-rs/forge-std --no-git --shallow
)
forge build
cp .env.example .env
```

Fastest testnet path:

```bash
bun run example
```

If `CONTRACT_ADDRESS` is unset, `bun run example` deploys a fresh consumer before submitting the v2 update. If you want to deploy separately first:

```bash
bun run deploy
# Save the emitted address into CONTRACT_ADDRESS in .env, then rerun:
bun run example
```

Flip to mainnet with one env var:

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

## Quick Start: Coin Flip

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

Run on testnet:

```bash
bun run deploy
```

Save the emitted contract address into `COIN_FLIP_CONTRACT_ADDRESS`, then fund the contract bankroll before the first flip. The contract accepts any positive wager, and the packaged CLI uses `0.01 MON` by default:

```bash
cast send $COIN_FLIP_CONTRACT_ADDRESS \
  --rpc-url ${RPC_URL:-https://testnet-rpc.monad.xyz} \
  --private-key $PRIVATE_KEY \
  --value 0.01ether
```

Then run the CLI flow:

```bash
bun run flip
```

Run on mainnet:

```bash
NETWORK=monad-mainnet bun run deploy
# Save COIN_FLIP_CONTRACT_ADDRESS in .env, fund the bankroll on mainnet, then:
NETWORK=monad-mainnet bun run flip
```

## Integration Example

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

const networkName = process.env.NETWORK || "monad-testnet";
const crossbarNetwork = networkName === "monad-mainnet" ? "mainnet" : "testnet";
const rpcUrl =
  process.env.RPC_URL ||
  (networkName === "monad-mainnet"
    ? "https://rpc.monad.xyz"
    : "https://testnet-rpc.monad.xyz");
const switchboardAddress =
  networkName === "monad-mainnet"
    ? "0xB7F03eee7B9F56347e32cC71DaD65B303D5a0E67"
    : "0x6724818814927e057a693f4e3A172b6cC1eA690C";

const provider = new ethers.JsonRpcProvider(rpcUrl);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const switchboard = new ethers.Contract(
  switchboardAddress,
  ["function getFee(bytes[] calldata updates) external view returns (uint256)"],
  signer
);

const priceConsumer = new ethers.Contract(
  process.env.CONTRACT_ADDRESS!,
  ["function updatePrices(bytes[] calldata updates, bytes32[] calldata feedIds) external payable"],
  signer
);

const feedHash = "0xa0950ee5ee117b2e2c30f154a69e17bfb489a7610c508dc5f67eb2a14616d8ea";
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

// Useful when validating a custom Feed Builder feed before sending a transaction.
await crossbar.simulateFeed(feedHash, false, undefined, crossbarNetwork);

const response = await crossbar.fetchV2Update([feedHash], {
  chain: "evm",
  network: crossbarNetwork,
  use_timestamp: true,
});

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

const updates = [response.encoded];
const fee = await switchboard.getFee(updates);
const tx = await priceConsumer.updatePrices(updates, [feedHash], { value: fee });
await tx.wait();
```

## Custom Feed Troubleshooting

* Feed Builder custom feeds do not require a separate activation or permission toggle on Monad.
* Use the same `bytes32` feed hash/feed ID from Feed Builder or Explorer for the full v2 flow:
  * `GET /v2/fetch/{feedId}`
  * `GET /v2/simulate/{feedId}?network=testnet|mainnet`
  * `GET /v2/update/{feedId}?chain=evm&network=testnet|mainnet&use_timestamp=true`
* If `v2/fetch` and `v2/simulate` succeed but `v2/update` returns `ORACLE_UNAVAILABLE`, the issue is not a missing deployment step or permission. It can be managed oracle/gateway availability, or oracle-side validation rejecting the feed result. Check oracle errors for `RangeExceeded`, especially if raw v2 `maxJobRangePct` was not scaled by `1e9`; see [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units).

## Notes

* Testnet MON is available from the [Monad faucet](https://faucet.monad.xyz).
* The generic randomness helper at `evm/randomness/randomness.ts` still supports `hyperliquid-mainnet` in addition to Monad. Run it from `evm/randomness` after `bun install`, `cp .env.example .env`, and `bun run example`.


# Hyperliquid

Switchboard supports HyperEVM with the same encoded-update flow used on other EVM chains. The current examples repo does not ship a dedicated Hyperliquid runner script, but you should reuse the `evm/price-feeds` contract and deployment flow with Hyperliquid-specific chain settings.

## Network Information

| Network     | Chain ID | RPC URL                                   | Switchboard Contract                         |
| ----------- | -------- | ----------------------------------------- | -------------------------------------------- |
| **Mainnet** | 999      | `https://rpc.hyperliquid.xyz/evm`         | `0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347` |
| **Testnet** | 998      | `https://rpc.hyperliquid-testnet.xyz/evm` | TBD                                          |

## Quick Start

Clone the examples repo and build the shared price-consumer project:

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

Deploy the consumer contract to HyperEVM with the packaged Foundry script:

```bash
SWITCHBOARD_ADDRESS=0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347 \
forge script deploy/DeploySwitchboardPriceConsumer.s.sol:DeploySwitchboardPriceConsumer \
  --rpc-url https://rpc.hyperliquid.xyz/evm \
  --private-key $PRIVATE_KEY \
  --broadcast \
  -vvvv
```

## Integration Example

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

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

const switchboard = new ethers.Contract(
  "0xcDb299Cb902D1E39F83F54c7725f54eDDa7F3347",
  ["function getFee(bytes[] calldata updates) external view returns (uint256)"],
  signer
);

const priceConsumer = new ethers.Contract(
  process.env.CONTRACT_ADDRESS!,
  ["function updatePrices(bytes[] calldata updates, bytes32[] calldata feedIds) external payable"],
  signer
);

const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
const btcFeedHash = "0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812";

await crossbar.simulateFeed(btcFeedHash, false, undefined, "mainnet");

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

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

const updates = [response.encoded];
const fee = await switchboard.getFee(updates);
const tx = await priceConsumer.updatePrices(updates, [btcFeedHash], { value: fee });
await tx.wait();
```

## Notes

* The packaged `evm/price-feeds/scripts/run.ts` currently includes Monad presets, not Hyperliquid presets.
* For Hyperliquid, reuse the same contract and encoded-update flow shown above with chain ID `999`.
* Hyperliquid network docs: [Hyperliquid Docs](https://hyperliquid.gitbook.io/hyperliquid-docs) and [HyperEVM](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperevm).


# Sui

Switchboard provides on-demand oracle data for Sui Move contracts using the Quote Verifier pattern. This enables your DeFi applications to access verified, real-time price data with built-in security features.

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

**Prerequisites**

* Sui CLI installed ([Installation Guide](https://docs.sui.io/guides/developer/getting-started/sui-install))
* Basic understanding of Move and TypeScript
* Node.js 21+ and npm installed

#### Deployments

The Switchboard On-Demand service is deployed on:

* **Mainnet:** [`0xa81086572822d67a1559942f23481de9a60c7709c08defafbb1ca8dffc44e210`](https://suiscan.xyz/mainnet/object/0xa81086572822d67a1559942f23481de9a60c7709c08defafbb1ca8dffc44e210)
* **Testnet:** [`0x28005599a66e977bff26aeb1905a02cda5272fd45bb16a5a9eb38e8659658cff`](https://suiscan.xyz/testnet/object/0x28005599a66e977bff26aeb1905a02cda5272fd45bb16a5a9eb38e8659658cff)

#### 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/).


# Price Feeds

Access to reliable, real-world data is essential for decentralised applications (dApps), particularly in Decentralised Finance (DeFi). Real-time asset prices, forming the backbone of any DeFi protocol, are among the most critical data points.

This is where Data Feeds come in. Think of them as secure bridges connecting the off-chain world of financial markets to your on-chain smart contracts. They provide a continuous stream of verified, aggregated price data for a wide range of assets, enabling your dApp to react to market fluctuations and operate correctly.


# 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)

## 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.16 @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) section


# Surge Price Feeds

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

## The Future of Oracle Technology

Switchboard Surge is the industry's fastest oracle data delivery system, providing sub-100ms latency through direct WebSocket streaming. Built for the next generation of DeFi applications, trading systems, and real-time dashboards.

## Key Innovation

Traditional oracles require multiple steps—gathering prices, writing to blockchain state, reaching consensus, and then making data available—resulting in 2-10 seconds of latency.

Switchboard oracles must pass a hardware proof when joining the network, ensuring they run only verified Switchboard code. This allows oracles to stream price data directly from sources to your application via WebSocket, achieving sub-100ms latency.

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Price Sources   │────▶│  Oracle Network  │────▶│  Surge Gateway   │
│   (CEX, DEX)     │     │ (SAIL Verified)  │     │   (WebSocket)    │
└──────────────────┘     └──────────────────┘     └────────┬─────────┘
                                                          │
                                               ┌──────────▼──────────┐
                                               │   Your Application  │
                                               │  • Event Listeners  │
                                               │  • Price Handlers   │
                                               │  • Quote Converter  │
                                               └─────────────────────┘
```

## Key Features

**Unmatched Performance** — Sub-100ms latency with direct WebSocket streaming and event-driven updates. No polling required.

**Zero Setup** — No data feed accounts or on-chain deployment needed. Just use your keypair and connection to start streaming.

**Cost Efficiency** — Subscription-based pricing with no gas fees for receiving updates. Reduced on-chain costs when submitting to contracts.

**Seamless Integration** — TypeScript/JavaScript SDK, WebSocket API for any language, and Sui quote conversion for on-chain use.

**Enterprise-Grade Reliability** — 99.9% uptime SLA with global infrastructure, automatic failover, and professional support.

## User Flow

Surge works the same way regardless of your target chain:

1. **Subscribe** — All Surge subscriptions are managed on Solana, regardless of which chain you're building on. Connect your Solana wallet at the [subscription portal](https://explorer.switchboardlabs.xyz/subscriptions).
2. **Authenticate** — The SDK authenticates your session by signing with your Solana keypair. If the keypair does not have an active subscription, `connectAndSubscribe` will fail.
3. **Stream Prices** — Once subscribed, prices stream directly to your application via WebSocket. No on-chain reads required—this is what enables sub-100ms latency.
4. **Use Prices** — When you need prices on-chain, convert the Surge update to your chain's format and submit it. Switchboard provides SDKs for Solana, EVM, and Sui.

## Getting Started

### 1. Subscribe

Connect your wallet and subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions). If you are an AI agent or wish to subscribe programmatically rather than through the UI, see the [Surge Subscription Guide](/ai-agents-llms/surge-subscription-guide).

### 2. Install the SDK

```bash
npm install @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/sui-sdk@0.1.16 @mysten/sui@1.38.0 @solana/web3.js@1.98.4
# or
yarn add @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/sui-sdk@0.1.16 @mysten/sui@1.38.0 @solana/web3.js@1.98.4
```

### 3. Connect and Stream

```typescript
import * as sb from "@switchboard-xyz/on-demand";
import { convertSurgeUpdateToQuotes, MAINNET_QUEUE_ID } from "@switchboard-xyz/sui-sdk";
import { Transaction } from "@mysten/sui/transactions";

// Initialize with keypair and connection (uses on-chain subscription)
const surge = new sb.Surge({ connection, keypair });
// `connection` is a Solana RPC Connection from @solana/web3.js (used to verify the Solana subscription),
// not your Sui client. Keep a separate Sui client for on-chain writes.

// Auth note: the SDK signs with your keypair to authenticate the session.
// If the keypair has no active Surge subscription, connectAndSubscribe will fail.

// Discover available feeds
const availableFeeds = await surge.getSurgeFeeds();
console.log(`${availableFeeds.length} feeds available`);

// Subscribe to specific feeds
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'ETH/USD' },
]);

// Handle price updates
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();
  metrics.perFeedMetrics.forEach((feed) => {
    console.log(`${feed.symbol}: ${prices[feed.feed_hash]}`);
  });

  // Convert to on-chain Oracle Quote for Sui contracts when needed
  const ptb = new Transaction();
  const quoteData = await convertSurgeUpdateToQuotes(ptb, response, MAINNET_QUEUE_ID);

  ptb.moveCall({
    target: `${PACKAGE_ID}::your_module::your_function`,
    arguments: [quoteData],
  });
});
```

## Pricing & Limits

| Plan           | Price       | Quote Interval | Max Feeds | Max Connections |
| -------------- | ----------- | -------------- | --------- | --------------- |
| **Plug**       | Free        | 10s            | 2         | 1               |
| **Pro**        | \~$3,000/mo | 450ms          | 100       | 10              |
| **Enterprise** | \~$7,500/mo | 0ms            | 300       | 15              |

Subscriptions are paid in SWTCH tokens. For custom limits or dedicated support, contact <sales@switchboard.xyz>.

## Primary Use Cases

### Perpetual Exchanges

Surge is the perfect oracle solution for perpetual trading platforms:

```typescript
import * as sb from "@switchboard-xyz/on-demand";
import { convertSurgeUpdateToQuotes, MAINNET_QUEUE_ID } from "@switchboard-xyz/sui-sdk";
import { Transaction } from "@mysten/sui/transactions";

surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));

    // Update mark price instantly
    await updateMarkPrice(feed.symbol, price);

    // Check for liquidations with latest price
    const liquidations = await checkLiquidations(feed.symbol, price);
    if (liquidations.length > 0) {
      const ptb = new Transaction();
      const quoteData = await convertSurgeUpdateToQuotes(ptb, response, MAINNET_QUEUE_ID);
      await executeLiquidations(liquidations, ptb, quoteData);
    }
  }
});
```

### Oracle-Based AMMs

Build the next generation of AMMs that use real-time oracle prices:

```typescript
class OracleAMM {
  private latestUpdate: sb.SurgeUpdate;

  async handlePriceUpdate(response: sb.SurgeUpdate) {
    const metrics = response.getLatencyMetrics();
    if (metrics.isHeartbeat) return;

    this.latestUpdate = response;
    const prices = response.getFormattedPrices();

    for (const feed of metrics.perFeedMetrics) {
      const pair = this.pairs.get(feed.symbol);
      pair.oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
      pair.lastUpdate = Date.now();
    }
  }

  async executeSwap(tokenIn: string, tokenOut: string, amountIn: number) {
    const pair = `${tokenIn}/${tokenOut}`;
    const latestPrice = this.pairs.get(pair).oraclePrice;
    const amountOut = amountIn * latestPrice * (1 - this.swapFee);

    const ptb = new Transaction();
    const quoteData = await convertSurgeUpdateToQuotes(ptb, this.latestUpdate, MAINNET_QUEUE_ID);

    ptb.moveCall({
      target: `${PACKAGE_ID}::amm::swap`,
      arguments: [amountIn, amountOut, quoteData],
    });

    return await this.client.signAndExecuteTransaction({ transaction: ptb });
  }
}
```

### High-Frequency Trading & Arbitrage

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const oraclePrice = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const dexPrice = await getDexPrice(feed.symbol);

    const spread = Math.abs(dexPrice - oraclePrice) / oraclePrice;
    if (spread > MIN_PROFIT_THRESHOLD) {
      const ptb = new Transaction();
      const quoteData = await convertSurgeUpdateToQuotes(ptb, response, MAINNET_QUEUE_ID);
      await executeArbitrage(ptb, quoteData, calculateOptimalSize(spread));
    }
  }
});
```

### Liquidation Engines

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const metrics = response.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = response.getFormattedPrices();

  for (const feed of metrics.perFeedMetrics) {
    const price = parseFloat(prices[feed.feed_hash].replace(/[$,]/g, ''));
    const positions = await getPositionsByCollateral(feed.symbol);

    for (const position of positions) {
      const ltv = calculateLTV(position, price);
      if (ltv > LIQUIDATION_THRESHOLD) {
        const ptb = new Transaction();
        const quoteData = await convertSurgeUpdateToQuotes(ptb, response, MAINNET_QUEUE_ID);
        await liquidatePosition(position, ptb, quoteData);
      }
    }
  }
});
```

## Technical Specifications

### Latency Breakdown

* Oracle processing: \~10ms
* Network transmission: \~20-50ms
* Client processing: \~10ms
* **Total: <100ms**

### Discovering Available Feeds

Use the `getSurgeFeeds()` method to see all available trading pairs:

```typescript
const surge = new sb.Surge({ connection, keypair });
const feeds = await surge.getSurgeFeeds();

feeds.forEach(feed => {
  console.log(`${feed.symbol}`);
});
```

### Supported Assets

* All major cryptocurrency pairs
* Multiple exchange sources available
* New pairs added regularly
* Custom feeds available on request

Note: Surge does not support custom feeds created with the [feed builder](https://explorer.switchboardlabs.xyz/feed-builder).

## FAQ

### How is Surge different from traditional oracles?

Surge streams data directly to your application via WebSocket, bypassing the blockchain entirely for reads. This eliminates gas costs and reduces latency from seconds to milliseconds.

### Can I use Surge data on-chain?

Yes! Surge updates can be converted to Sui quote format using `convertSurgeUpdateToQuotes()` from the `@switchboard-xyz/sui-sdk` and submitted to your Move contracts.

### What's the reliability?

Surge operates with 99.9% uptime SLA, automatic failover, and global redundancy. Enterprise customers get dedicated infrastructure.

### How do I handle disconnections?

The SDK includes automatic reconnection logic with exponential backoff. Your application will seamlessly recover from network interruptions.

## Next Steps

* [Surge Tutorial](/docs-by-chain/sui/surge/surge-tutorial) - Step-by-step implementation guide
* [Crossbar Gateway](/tooling/crossbar) - Stream prices to your frontend
* [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol) - Advanced HTTP + WebSocket protocol
* [Explore code examples](https://github.com/switchboard-xyz/sui)
* [Join our Discord](https://discord.gg/TJAv6ZYvPC)


# Surge Tutorial

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

This tutorial shows you how to stream real-time price data via WebSocket using Switchboard Surge and submit updates to the Sui blockchain. This approach is ideal for applications requiring sub-second price updates.

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

## What You'll Build

A TypeScript application that:

* Connects to Switchboard Surge for real-time price streaming via WebSocket
* Receives signed price updates with sub-second latency
* Submits price updates to the Sui blockchain
* Tracks latency statistics and oracle performance

## 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 (in your Sui keystore) for signing Sui transactions
* A Solana keypair with an active Surge subscription ([subscribe here](https://explorer.switchboardlabs.xyz/subscriptions))

Surge subscriptions are currently Solana-only; you cannot subscribe with a Sui keypair yet.

## Key Concepts

### Surge vs On-Demand Quotes

| Feature          | On-Demand Quotes      | Surge Streaming       |
| ---------------- | --------------------- | --------------------- |
| Update frequency | Request-based         | Continuous (\~100ms)  |
| Latency          | Higher (HTTP request) | Lower (WebSocket)     |
| Use case         | Occasional reads      | Real-time apps        |
| Authentication   | None required         | Subscription required |

### The emitSurgeQuote Function

Surge provides the `emitSurgeQuote()` function from `@switchboard-xyz/sui-sdk` that converts Surge updates into Sui transactions. This handles:

* Oracle signature formatting
* Transaction building
* Quote verification setup

### Oracle Mapping

Surge returns oracle public keys, but Sui needs oracle object IDs. The example fetches a mapping from Crossbar to convert between these formats.

### Transaction Queue Management

Since Sui transactions are sequential, the example implements a queue to:

* Buffer incoming price updates
* Process one transaction at a time
* Track processing latency

## The Streaming Client

Here's the complete mainnet streaming example:

```typescript
import * as sb from '@switchboard-xyz/on-demand';
import { SuiClient } from '@mysten/sui/client';
import {
  SwitchboardClient,
  emitSurgeQuote,
} from '@switchboard-xyz/sui-sdk';
import { fromB64 } from '@mysten/bcs';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Connection, Keypair as SolanaKeypair } from '@solana/web3.js';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { Transaction } from '@mysten/sui/transactions';

// Initialize Sui clients
const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

// Oracle mapping cache
const oracleMapping = new Map<string, string>();
let lastOracleFetch = 0;
const ORACLE_CACHE_TTL = 1000 * 60 * 10; // 10 minutes

// Transaction queue management
let isTransactionProcessing = false;
const rawResponseQueue: Array<{
  rawResponse: any;
  timestamp: number;
}> = [];

// Process transaction queue - ensures only one transaction at a time
async function processTransactionQueue(): Promise<void> {
  if (isTransactionProcessing || rawResponseQueue.length === 0) {
    return;
  }

  isTransactionProcessing = true;

  try {
    const queueItem = rawResponseQueue.shift()!;
    const { rawResponse, timestamp } = queueItem;

    console.log(`Processing transaction (queue length: ${rawResponseQueue.length})`);

    const transaction = new Transaction();

    // Convert Surge update to Sui transaction
    await emitSurgeQuote(switchboardClient, transaction, rawResponse);

    const result = await suiClient.signAndExecuteTransaction({
      transaction: transaction,
      signer: suiKeypair!,
      options: {
        showEvents: true,
        showEffects: true,
      },
    });

    const processingTime = Date.now() - timestamp;
    console.log(`Transaction completed in ${processingTime}ms`);
    console.log('Transaction result:', result.digest);
  } catch (error) {
    console.error('Transaction failed:', error);
  } finally {
    isTransactionProcessing = false;

    // Process next transaction in queue if any
    if (rawResponseQueue.length > 0) {
      setImmediate(() => processTransactionQueue());
    }
  }
}

// Fetch oracle mappings from Crossbar
async function fetchOracleMappings(): Promise<Map<string, string>> {
  const now = Date.now();

  if (oracleMapping.size > 0 && now - lastOracleFetch < ORACLE_CACHE_TTL) {
    return oracleMapping;
  }

  try {
    const response = await fetch('https://crossbar.switchboard.xyz/oracles/sui');
    const oracles = (await response.json()) as Array<{
      oracle_id: string;
      oracle_key: string;
    }>;

    oracleMapping.clear();
    for (const oracle of oracles) {
      const cleanKey = oracle.oracle_key.startsWith('0x')
        ? oracle.oracle_key.slice(2)
        : oracle.oracle_key;
      oracleMapping.set(cleanKey, oracle.oracle_id);
    }

    lastOracleFetch = now;
    console.log(`Loaded ${oracleMapping.size} oracle mappings`);
    return oracleMapping;
  } catch (error) {
    console.error('Failed to fetch oracle mappings:', error);
    return oracleMapping;
  }
}

// Calculate latency statistics
function calculateStatistics(latencies: number[]) {
  const sorted = [...latencies].sort((a, b) => a - b);
  const sum = sorted.reduce((a, b) => a + b, 0);

  return {
    min: sorted[0],
    max: sorted[sorted.length - 1],
    median: sorted[Math.floor(sorted.length / 2)],
    mean: sum / sorted.length,
    count: sorted.length,
  };
}

// Load Sui keypair (for signing Sui transactions)
let suiKeypair: Ed25519Keypair | null = null;

try {
  const keystorePath = path.join(os.homedir(), '.sui', 'sui_config', 'sui.keystore');
  const keystore = JSON.parse(fs.readFileSync(keystorePath, 'utf-8'));
  const secretKey = fromB64(keystore[0]);
  suiKeypair = Ed25519Keypair.fromSecretKey(secretKey.slice(1));
} catch (error) {
  console.error('Error loading Sui keypair:', error);
}

// Load Solana keypair (subscription owner)
let solanaKeypair: SolanaKeypair | null = null;

try {
  const solanaKeypairPath =
    process.env.SOLANA_KEYPAIR_PATH ||
    path.join(os.homedir(), '.config', 'solana', 'id.json');
  const secretKey = Uint8Array.from(
    JSON.parse(fs.readFileSync(solanaKeypairPath, 'utf-8'))
  );
  solanaKeypair = SolanaKeypair.fromSecretKey(secretKey);
} catch (error) {
  console.error('Error loading Solana keypair:', error);
}

if (!suiKeypair) {
  throw new Error('Sui keypair not loaded');
}

if (!solanaKeypair) {
  throw new Error('Solana keypair not loaded');
}

// Main function
(async function main() {
  console.log('Starting Surge streaming...');
  console.log(`Using Sui keypair: ${suiKeypair!.toSuiAddress()}`);
  console.log(`Using Solana keypair: ${solanaKeypair!.publicKey.toBase58()}`);

  const latencies: number[] = [];

  // Initialize Surge with Solana keypair (subscription owner)
  const surge = new sb.Surge({
    connection: solanaConnection,
    keypair: solanaKeypair!,
    signatureScheme: 'ed25519',
  });
  // Auth note: the SDK signs with your Solana keypair to authenticate the session.
  // If the keypair has no active Surge subscription, connectAndSubscribe will fail.

  // Connect and subscribe to feeds
  await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);

  // Pre-fetch oracle mappings
  await fetchOracleMappings();

  // Listen for price updates
  surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
    const currentLatency = Date.now() - response.data.source_ts_ms;
    latencies.push(currentLatency);

    const rawResponse = response.getRawResponse();
    const stats = calculateStatistics(latencies);
    const formattedPrices = response.getFormattedPrices();
    const currentPrice = Object.values(formattedPrices)[0] || 'N/A';

    console.log(
      `Update #${stats.count} | Price: ${currentPrice} | Latency: ${currentLatency}ms | Avg: ${stats.mean.toFixed(1)}ms`
    );

    // Queue the update for processing
    rawResponseQueue.push({
      rawResponse,
      timestamp: Date.now(),
    });

    // Trigger queue processing
    processTransactionQueue();
  });

  console.log('Listening for price updates...');
})();
```

### Code Walkthrough

#### Setup

```typescript
const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');
```

Initialize the Sui client you will submit transactions through, plus a Solana RPC connection for Surge authentication. For Sui testnet, use `https://fullnode.testnet.sui.io:443`.

#### Creating Surge Connection

```typescript
const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair!,
  signatureScheme: 'ed25519',
});

await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);
```

* `connection`: Your Solana `Connection` instance
* `keypair`: Your Solana keypair (must have an active Surge subscription)
* `signatureScheme`: Use `'ed25519'` for Solana keypairs
* `connectAndSubscribe()`: Connects and subscribes to specified feeds

#### Handling Updates

```typescript
surge.on('signedPriceUpdate', async (response: sb.SurgeUpdate) => {
  const rawResponse = response.getRawResponse();
  const formattedPrices = response.getFormattedPrices();
  // ...
});
```

The `signedPriceUpdate` event fires whenever new price data arrives. Key methods:

* `getRawResponse()`: Returns the raw signed data for transaction submission
* `getFormattedPrices()`: Returns human-readable prices

#### Submitting to Sui

```typescript
const transaction = new Transaction();
await emitSurgeQuote(switchboardClient, transaction, rawResponse);

const result = await suiClient.signAndExecuteTransaction({
  transaction,
  signer: suiKeypair,
});
```

The `emitSurgeQuote()` function handles converting the Surge response into a valid Sui transaction.

## Mainnet vs Testnet

The mainnet and testnet examples are nearly identical with these differences:

| Setting        | Mainnet                               | Testnet                               |
| -------------- | ------------------------------------- | ------------------------------------- |
| RPC URL        | `https://fullnode.mainnet.sui.io:443` | `https://fullnode.testnet.sui.io:443` |
| Oracle Mapping | `/oracles/sui`                        | `/oracles/sui/testnet`                |

### Testnet Configuration

```typescript
// Testnet setup
const suiClient = new SuiClient({ url: 'https://fullnode.testnet.sui.io:443' });
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair!,
  signatureScheme: 'ed25519',
});

// Testnet oracle mapping endpoint
const response = await fetch('https://crossbar.switchboard.xyz/oracles/sui/testnet');
```

## Running the Examples

### 1. Clone the Repository

```bash
git clone https://github.com/switchboard-xyz/sb-on-demand-examples
cd sb-on-demand-examples/sui/surge/basic
```

### 2. Install Dependencies

```bash
npm install
```

### 3. Ensure Active Subscription

Your Solana keypair (default `~/.config/solana/id.json` or `SOLANA_KEYPAIR_PATH`) must have an active Surge subscription. The Sui keypair only signs Sui transactions. Subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions).

### 4. Run the Examples

```bash
# Mainnet streaming
npm run stream

# Testnet streaming
npm run stream:testnet
```

### Expected Output

```
Starting Surge streaming...
Using Sui keypair: 0x...
Using Solana keypair: 9k...
Loaded 15 oracle mappings
Listening for price updates...
Update #1 | Price: 97234.50 | Latency: 85ms | Avg: 85.0ms
Processing transaction (queue length: 0)
Transaction completed in 1234ms
Transaction result: 8Js7NsQ7...
Update #2 | Price: 97235.10 | Latency: 92ms | Avg: 88.5ms
...
```

## Adding to Your Project

### Dependencies

```bash
npm install @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/sui-sdk@0.1.16 @mysten/sui@1.38.0 @solana/web3.js@1.98.4
```

### Minimal Integration

This example assumes you already loaded a `solanaKeypair` (with an active Surge subscription) and a `suiKeypair` (for signing Sui transactions).

```typescript
import * as sb from '@switchboard-xyz/on-demand';
import { SuiClient } from '@mysten/sui/client';
import { SwitchboardClient, emitSurgeQuote } from '@switchboard-xyz/sui-sdk';
import { Transaction } from '@mysten/sui/transactions';
import { Connection } from '@solana/web3.js';

const suiClient = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const switchboardClient = new SwitchboardClient(suiClient);
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com');

const surge = new sb.Surge({
  connection: solanaConnection,
  keypair: solanaKeypair, // Solana keypair with active Surge subscription
  signatureScheme: 'ed25519',
});

await surge.connectAndSubscribe([{ symbol: 'BTC/USD' }]);

surge.on('signedPriceUpdate', async (response) => {
  const tx = new Transaction();
  await emitSurgeQuote(switchboardClient, tx, response.getRawResponse());

  // Sign and send transaction
  await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer: suiKeypair,
  });
});
```

### Multiple Feeds

```typescript
await surge.connectAndSubscribe([
  { symbol: 'BTC/USD' },
  { symbol: 'ETH/USD' },
  { symbol: 'SOL/USD' },
]);
```

### Error Handling

```typescript
surge.on('error', (error) => {
  console.error('Surge error:', error);
});

surge.on('close', () => {
  console.log('Connection closed, attempting reconnect...');
  // Implement reconnection logic
});
```

## Performance Considerations

### Transaction Queue

The example uses a queue because:

* Sui transactions are sequential per sender
* Surge updates arrive faster than transactions complete
* Queuing prevents transaction conflicts

### Latency Optimization

* Keep your Sui node geographically close
* Use dedicated RPC endpoints for production
* Consider batching updates if latency isn't critical

### Oracle Mapping Cache

The oracle mapping is cached for 10 minutes to avoid repeated API calls. Adjust `ORACLE_CACHE_TTL` based on your needs.

## Troubleshooting

### "Keypair not loaded"

* Ensure you have a valid Sui keypair in `~/.sui/sui_config/sui.keystore`
* Run `sui client new-address ed25519` to create one
* Ensure you have a valid Solana keypair at `~/.config/solana/id.json` or `SOLANA_KEYPAIR_PATH`
* Run `solana-keygen new` to create one

### "Subscription not found" or connection rejected

* Ensure your Solana keypair has an active Surge subscription
* Subscribe at [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions)

### "Oracle ID not found for key"

* The oracle mapping might be stale
* Force refresh by clearing `oracleMapping` and calling `fetchOracleMappings()`
* Check you're using the correct network (mainnet vs testnet)

### Transaction Failures

* Ensure your wallet has sufficient SUI for gas
* Check network connectivity
* Verify you're on the correct network

### High Latency

* Check your network connection
* Consider using a dedicated RPC endpoint
* Reduce logging if running in production

## Next Steps

* **Quote Verifier Pattern**: See the [Price Feeds](/docs-by-chain/sui/price-feeds) tutorial for verified on-chain price storage
* **Multiple Feeds**: Subscribe to multiple feeds for portfolio tracking
* **Custom Integration**: Use the price data to trigger your own Move contract logic


# Aptos

This guide covers the setup and use of Switchboard data feeds within your project, using the `Aggregator` module for updating feeds and integrating `Switchboard` in Move.

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

**Active Deployments**

Switchboard is currently deployed on the following networks:

* Mainnet:
  * [`0xfea54925b5ac1912331e2e62049849b37842efaea298118b66f85a59057752b8`](https://explorer.aptoslabs.com/object/0xfea54925b5ac1912331e2e62049849b37842efaea298118b66f85a59057752b8/modules/code/aggregator?network=mainnet)
* Testnet:
  * [`0x4fc1809ffb3c5ada6b4e885d4dbdbeb70cbdd99cbc0c8485965d95c2eab90935`](https://explorer.aptoslabs.com/object/0x4fc1809ffb3c5ada6b4e885d4dbdbeb70cbdd99cbc0c8485965d95c2eab90935/modules/code/aggregator?network=testnet)

**Typescript-SDK Installation**

To use Switchboard On-Demand, add the following dependencies to your project:

**NPM**

```bash
npm install @switchboard-xyz/aptos-sdk@0.1.5 @switchboard-xyz/common@5.8.5 @aptos-labs/ts-sdk@6.1.0 --save
```

**Adding Switchboard to Move Code**

To integrate Switchboard with Move, add the following dependencies to Move.toml:

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

**Example Move Code for Using Switchboard Values**

In the example.move module, use the Aggregator and CurrentResult types to access the latest feed data.

```rust
module example::switchboard_example {
    use aptos_framework::aptos_coin::AptosCoin;
    use aptos_framework::object::{Self, Object};
    use switchboard::aggregator::{Self, Aggregator, CurrentResult};
    use switchboard::decimal::Decimal;
    use switchboard::update_action;

    public entry fun my_function(account: &signer, update_data: vector<vector<u8>>) {

        // Update the feed with the provided data
        update_action::run<AptosCoin>(account, update_data);

        /**
         * You can use the following code to remove and run switchboard updates from the update_data vector,
         * keeping only non-switchboard byte vectors:
         *
         * update_action::extract_and_run<AptosCoin>(account, &mut update_data);
         */

        // Get the feed object
        let aggregator: address = @0xSomeFeedAddress;
        let aggregator: Object<Aggregator> = object::address_to_object<Aggregator>(aggregator);

        // Get the latest update info for the feed
        let current_result: CurrentResult = aggregator::current_result(aggregator);

        // Access various result properties
        let result: Decimal = aggregator::result(&current_result);              // Update result
        let (result_u128, result_neg) = decimal::unpack(result);                // Unpack result
        let timestamp_seconds = aggregator::timestamp(&current_result);         // Timestamp in seconds

        // Other properties you can use from the current result
        let min_timestamp: u64 = aggregator::min_timestamp(&current_result);    // Oldest valid timestamp used
        let max_timestamp: u64 = aggregator::max_timestamp(&current_result);    // Latest valid timestamp used
        let range: Decimal = aggregator::range(&current_result);                // Range of results
        let mean: Decimal = aggregator::mean(&current_result);                  // Average (mean)
        let stdev: Decimal = aggregator::stdev(&current_result);                // Standard deviation

        // Use the computed result as needed...
    }
}
```

Once dependencies are configured, updated aggregators can be referenced easily.

This implementation allows you to read and utilize Switchboard data feeds within Move. If you have any questions or need further assistance, please contact the Switchboard team.

**Creating an Aggregator and Sending Transactions**

Building a feed in Switchboard can be done using the Typescript SDK, or it can be done with the [Switchboard Web App](https://ondemand.switchboard.xyz/aptos/mainnet). Visit the [custom feeds section](/custom-feeds/build-and-deploy-feed) for more on designing and creating feeds.

**Building Feeds in Typescript \[optional]**

```tsx
import {
  CrossbarClient,
  SwitchboardClient,
  Aggregator,
  ON_DEMAND_MAINNET_QUEUE,
  ON_DEMAND_TESTNET_QUEUE,
} from "@switchboard-xyz/aptos-sdk";
import { OracleJob } from "@switchboard-xyz/common";
import { Aptos, Account, AptosConfig, Network } from "@aptos-labs/ts-sdk"

// get the aptos client
const config = new AptosConfig({
  network: Network.MAINNET, // network a necessary param / if not passed in, full node url is required
});
// create a SwitchboardClient using the aptos client
const aptos = new Aptos(config);
const client = new SwitchboardClient(aptos);

const crossbarClient = new CrossbarClient("http://myCrossbarDeployment.com");

// ... define some jobs ...
const jobs: OracleJob[] = [
  OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: "https://binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        }
      },
      {
        jsonParseTask: {
          path: "$.price"
        }
      }
    ],
  }),
];

const isMainnet = true; // set to false for testnet
const queue = isMainnet
  ? ON_DEMAND_MAINNET_QUEUE
  : ON_DEMAND_TESTNET_QUEUE;

// Store some job definition
const { feedHash } = await crossbarClient.store(queue, jobs);

// try creating a feed
const feedName = "BTC/USDT";

// Require only one oracle response needed
const minSampleSize = 1;

// Allow update data to be up to 60 seconds old
const maxStalenessSeconds = 60;

// If jobs diverge more than 1%, don't allow the feed to produce a valid update
const maxVariance = 1e9; // 1%, scaled by 1e9 for this chain parameter

// Require only 1 job response (unscaled count)
const minResponses = 1;

//==========================================================
// Feed Initialization On-Chain
//==========================================================

// ... get the account object for your signer with relevant key / address ...

// get the signer address
const account = Account.generate(); // Or plug in your account
const signerAddress = account.accountAddress.toString();

const aggregatorInitTx = await Aggregator.initTx(client, signerAddress, {
  name: feedName,
  minSampleSize,
  maxStalenessSeconds,
  maxVariance,
  feedHash,
  minResponses,
});

const res = await aptos.signAndSubmitTransaction({
  signer: account,
  transaction: aggregatorInitTx,
});

const result = await aptos.waitForTransaction({
  transactionHash: res.hash,
  options: {
    timeoutSecs: 30,
    checkSuccess: true,
  },
});

// Log the transaction results
console.log(result);

```

**Updating Feeds**

```tsx
// Replace with your feed ID
const aggregatorId = "0x1234567890abcdef1234567890abcdef12345678";D
const aggregator = new Aggregator(client, aggregatorId);

// Fetch and log the oracle responses
const { updates } = await aggregator.fetchUpdate();

// Create a transaction to run the feed update
const exampleAddress = "YOUR_CONTRACT_ADDRESS";
const updateTx = await client.aptos.transaction.build.simple({
  sender: signerAddress,
  data: {
    function: `${exampleAddress}::switchboard_example::my_function`,
    functionArguments: [updates],
  },
});

// Sign and submit the transaction
const res = await aptos.signAndSubmitTransaction({
  signer: account,
  transaction: updateTx!,
});

// Wait for the transaction to complete
const result = await aptos.waitForTransaction({
  transactionHash: res.hash,
  options: {
    timeoutSecs: 30,
    checkSuccess: true,
  },
});

// Log the transaction results
console.log(result);
```

### (optional) Migrating existing code to On-Demand from V2 without updating logic

#### 1. Update Move.toml

You'll need to update your `Move.toml` to include the new `switchboard_adapter` module and address. Replace the `switchboard` named address with the new `switchboard_adapter` address.

```toml
[addresses]

# remove the switchboard address
- switchboard = "0xb91d3fef0eeb4e685dc85e739c7d3e2968784945be4424e92e2f86e2418bf271"

# add the switchboard_adapter address
+ switchboard_adapter = "0x890fd4ed8a26198011e7923f53f5f1e5eeb2cc389dd50b938f16cb95164dc81c"

[dependencies]

# remove the switchboard v2 dependency
- [dependencies.Switchboard]
- git = "https://github.com/switchboard-xyz/sbv2-aptos.git"
- subdir = "move/switchboard/testnet/" # change to /mainnet/ if on mainnet - or fork and change deps for a specific commit hash
- rev = "main"

# add the on-demand adapter dependency
+ [dependencies.SwitchboardAdapter]
+ git = "https://github.com/switchboard-xyz/aptos.git"
+ subdir = "adapter/mainnet"
+ rev = "main"
```

#### 2. Update your Move Modules

You'll need to update named address `switchboard` to `switchboard_adapter` in dependencies.

```rust
module example::module {
-    use switchboard::aggregator;
-    use switchboard::math;
+    use switchboard_adapter::aggregator;
+    use switchboard_adapter::math;
    ...
}
```

The aggregator addresses you use will have to be updated to new On-Demand Aggregators that can be created from your V2 Aggregators on the Switchboard On-Demand App. Update references in your application to on-demand aggregators accordingly.

#### 3. Cranking

On-demand works on a pull-based mechanism, so you will have to crank feeds with your client-side code in order to get the latest data. This can be done using the Typescript SDK.

```typescript
import {
  Aggregator,
  SwitchboardClient,
  waitForTx,
} from "@switchboard-xyz/aptos-sdk";
import { Account, Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";

// get the aptos client
const config = new AptosConfig({
  network: Network.MAINNET, // network a necessary param / if not passed in, full node url is required
});
const aptos = new Aptos(config);

// create a SwitchboardClient using the aptos client
const client = new SwitchboardClient(aptos);

const aggregator = new Aggregator(sb, aggregatorId);

// update the aggregator every 10 seconds
setInterval(async () => {
  try {
    // fetch the latest update and tx to update the aggregator
    const { updateTx } = await aggregator.fetchUpdate({
      sender: signerAddress,
    });

    // send the tx to update the aggregator
    const tx = await aptos.signAndSubmitTransaction({
      signer: account,
      transaction: updateTx!,
    });
    const resultTx = await waitForTx(aptos, tx.hash);
    console.log(`Aggregator ${aggregatorId} updated!`);
  } catch (e) {
    console.error(`Error updating aggregator ${aggregatorId}: ${e}`);
  }
}, 10000);
```


# Iota

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

### Active Deployments

The Switchboard On-Demand service is currently deployed on the following networks:

* Mainnet: [0x8650249db8ffcffe8eb08b0696a8cb71e325f2afb9abc646f45344077b073ba1](https://explorer.iota.org/object/0x8650249db8ffcffe8eb08b0696a8cb71e325f2afb9abc646f45344077b073ba1)
* Testnet: [0xad9557529ba97ccf94a6a89d759fc8c8a6da5f0b98630c0fb53fe3b6d6a8e97a](https://explorer.iota.org/object/0xad9557529ba97ccf94a6a89d759fc8c8a6da5f0b98630c0fb53fe3b6d6a8e97a?network=testnet)

### Typescript-SDK Installation

To use Switchboard On-Demand, add the following dependencies to your project:

#### NPM

```bash
npm install @switchboard-xyz/iota-sdk@0.0.3 @switchboard-xyz/common@5.8.5 @iota/iota-sdk@1.11.0 --save
```

#### Bun

```bash
bun add @switchboard-xyz/iota-sdk@0.0.3 @switchboard-xyz/common@5.8.5 @iota/iota-sdk@1.11.0
```

#### PNPM

```bash
pnpm add @switchboard-xyz/iota-sdk@0.0.3 @switchboard-xyz/common@5.8.5 @iota/iota-sdk@1.11.0
```

### Creating an Aggregator and Sending Transactions

Building a feed in Switchboard can be done using the Typescript SDK, or it can be done with the [Switchboard Web App](https://ondemand.switchboard.xyz/iota/mainnet). Visit our [docs](https://docs.switchboard.xyz/) for more on designing and creating feeds.

#### Building Feeds

```typescript
import {
  SwitchboardClient,
  Aggregator,
} from "@switchboard-xyz/iota-sdk";
import { CrossbarClient, OracleJob } from "@switchboard-xyz/common";
import { getFullnodeUrl, IotaClient } from "@iota/iota-sdk/client";
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Transaction } from '@iota/iota-sdk/transactions';

// Add your Iota signer here
const keypair = new Ed25519Keypair();
const userAddress = keypair.getPublicKey().toIotaAddress();
const iotaClient = new IotaClient({
    url: getFullnodeUrl('testnet'),
});

// for initial testing and development, you can use the public
// https://crossbar.switchboard.xyz instance of crossbar
const crossbarClient = new CrossbarClient("https://crossbar.switchboard.xyz");

// ... define some jobs ...
const jobs: OracleJob[] = [
  OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: "https://binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        }
      },
      {
        jsonParseTask: {
          path: "$.price"
        }
      }
    ],
  }),
];


// Create a SwitchboardClient using the IotaClient configured with your favorite RPC on testnet or mainnet
const sb = new SwitchboardClient(iotaClient);
const state = await sb.fetchState();
const queue = state.oracleQueueId;

const { feedHash } = await crossbarClient.store(queue, jobs);

// try creating a feed
const feedName = "BTC/USDT";

// Require only one oracle response needed
const minSampleSize = 1;

// Allow update data to be up to 60 seconds old
const maxStalenessSeconds = 60;

// If jobs diverge more than 1%, don't allow the feed to produce a valid update
const maxVariance = 1e9; // 1%, scaled by 1e9 for this chain parameter

// Require only 1 job response (unscaled count)
const minJobResponses = 1; // unscaled job/source quorum

//==========================================================
// Feed Initialization On-Chain
//==========================================================

let transaction = new Transaction();

// add the tx to the PTB
await Aggregator.initTx(sb, transaction, {
  feedHash,
  name: feedName,
  authority: userAddress,
  minSampleSize,
  maxStalenessSeconds,
  maxVariance,
  minResponses: minJobResponses,
});

// Send the transaction
const res = await iotaClient.signAndExecuteTransaction({
  signer: keypair,
  transaction,
  options: {
    showEffects: true,
  },
});

// Capture the created aggregator ID
let aggregatorId;
res.effects?.created?.forEach((c: any) => {
  if (c.reference.objectId) {
    aggregatorId = c.reference.objectId;
  }
});

// Wait for transaction confirmation
await iotaClient.waitForTransaction({
  digest: res.digest,
});

// Log the transaction effects
console.log(res);

```

### Updating Feeds

With Switchboard On-Demand, passing the PTB into the feed update method handles the update automatically.

```typescript
const aggregator = new Aggregator(sb, aggregatorId);

// Create the PTB transaction
let feedTx = new Transaction();

// Fetch and log the oracle responses
const response = await aggregator.fetchUpdateTx(feedTx);
console.log("Fetch Update Oracle Response: ", response);

// Send the transaction
const res = await iotaClient.signAndExecuteTransaction({
  signer: keypair,
  transaction: feedTx,
  options: {
    showEffects: true,
  },
});

// Wait for transaction confirmation
await iotaClient.waitForTransaction({
  digest: res.digest,
});

// Log the transaction effects
console.log({ aggregatorId, res });
```

Note: Ensure the Switchboard Aggregator update is the first action in your PTB or occurs before referencing the feed update.

### Adding Switchboard to Move Code

To integrate Switchboard with Move, add the following dependencies to Move.toml:

```toml
[dependencies.Switchboard]
git = "https://github.com/switchboard-xyz/iota.git"
subdir = "on_demand/"
rev = "main" 

[dependencies.Iota]
override = true
git = "https://github.com/iotaledger/iota.git"
subdir = "crates/iota-framework/packages/iota-framework"
rev = "framework/testnet"
```

Once dependencies are configured, updated aggregators can be referenced easily.

### Example Move Code for Using Switchboard Values

In the example.move module, use the Aggregator and CurrentResult types to access the latest feed data.

```rust
module example::switchboard;

use switchboard::aggregator::{Aggregator, CurrentResult};
use switchboard::decimal::Decimal;

public entry fun use_switchboard_value(aggregator: &Aggregator) {

    // Get the latest update info for the feed
    let current_result = aggregator.current_result();

    // Access various result properties
    let result: Decimal = current_result.result();        // Median result
    let result_u128: u128 = result.value();               // Result as u128
    let min_timestamp_ms: u64 = current_result.min_timestamp_ms(); // Oldest data timestamp
    let max_timestamp_ms: u64 = current_result.max_timestamp_ms(); // Latest data timestamp
    let range: Decimal = current_result.range();          // Range of results
    let mean: Decimal = current_result.mean();            // Average (mean)
    let stdev: Decimal = current_result.stdev();          // Standard deviation
    let max_result: Decimal = current_result.max_result();// Max result
    let min_result: Decimal = current_result.min_result();// Min result
    let neg: bool = result.neg();                         // Check if negative (ignore for prices)

    // Use the computed result as needed...
}
```

This implementation allows you to read and utilize Switchboard data feeds within Move. If you have any questions or need further assistance, please contact the Switchboard team.


# Movement

### Examples and Source Code

Source code for the Switchboard On-Demand Movement integration can be found in the [github repo](https://github.com/switchboard-xyz/movement) along with examples.

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

### Active Deployments

The Switchboard On-Demand service is currently deployed on the following networks:

* Mainnet:
  * [0x465e420630570b780bd8bfc25bfadf444e98594357c488fe397a1142a7b11ffa](https://explorer.movementlabs.xyz/object/0x465e420630570b780bd8bfc25bfadf444e98594357c488fe397a1142a7b11ffa/modules/packages/OnDemand?network=mainnet)
* Testnet (Bardock):
  * [0x465e420630570b780bd8bfc25bfadf444e98594357c488fe397a1142a7b11ffa](https://explorer.movementlabs.xyz/object/0x465e420630570b780bd8bfc25bfadf444e98594357c488fe397a1142a7b11ffa/modules/packages/OnDemand?network=bardock+testnet)

#### Adapter Addresses

* Mainnet:
  * [0xb3654a69ba2a252849a89fa70845ad8e713a28c322dc580ae457df1f747bb74a](https://explorer.movementlabs.xyz/object/0xb3654a69ba2a252849a89fa70845ad8e713a28c322dc580ae457df1f747bb74a/modules/packages/Switchboard?network=mainnet)
* Testnet (Bardock):
  * [0xfe38ecf6fc57e742327af6e951e9fe2fcadcd6d1f1327ba2bee5a31e43d6637f](https://explorer.movementlabs.xyz/object/0xfe38ecf6fc57e742327af6e951e9fe2fcadcd6d1f1327ba2bee5a31e43d6637f/modules/packages/Switchboard?network=bardock+testnet)

***

### Typescript-SDK Installation

To use Switchboard On-Demand, add the following dependencies to your project:

#### NPM

```bash
npm install @switchboard-xyz/aptos-sdk@0.1.5 @switchboard-xyz/common@5.8.5 @aptos-labs/ts-sdk@6.1.0 --save
```

#### Bun

```bash
bun add @switchboard-xyz/aptos-sdk@0.1.5 @switchboard-xyz/common@5.8.5 @aptos-labs/ts-sdk@6.1.0
```

#### PNPM

```bash
pnpm add @switchboard-xyz/aptos-sdk@0.1.5 @switchboard-xyz/common@5.8.5 @aptos-labs/ts-sdk@6.1.0
```

### Adding Switchboard to Move Code

To integrate Switchboard with Move, add the following dependencies to Move.toml:

```toml
[addresses]
on_demand = "0x465e420630570b780bd8bfc25bfadf444e98594357c488fe397a1142a7b11ffa"

# ...

[dependencies.OnDemand]
git = "https://github.com/switchboard-xyz/movement.git"
subdir = "on_demand/"
rev = "main"
```

### Example Move Code for Using Switchboard Values

In the example.move module, use the Aggregator and CurrentResult types to access the latest feed data.

```rust
module example::switchboard_example {
    use aptos_framework::aptos_coin::AptosCoin;
    use aptos_framework::object::{Self, Object};
    use on_demand::aggregator::{Self, Aggregator, CurrentResult};
    use on_demand::decimal::Decimal;
    use on_demand::update_action;

    public entry fun my_function(account: &signer, update_data: vector<vector<u8>>) {

        // Update the feed with the provided data
        update_action::run<AptosCoin>(account, update_data);

        /**
        * You can use the following code to remove and run switchboard updates from the update_data vector,
        * keeping only non-switchboard byte vectors:
        *
        * update_action::extract_and_run<AptosCoin>(account, &mut update_data);
        */

        // Get the feed object
        let aggregator: address = @0xSomeFeedAddress;
        let aggregator: Object<Aggregator> = object::address_to_object<Aggregator>(aggregator);

        // Get the latest update info for the feed
        let current_result: CurrentResult = aggregator::current_result(aggregator);

        // Access various result properties
        let result: Decimal = aggregator::result(&current_result);              // Update result
        let (result_u128, result_neg) = decimal::unpack(result);                // Unpack result
        let timestamp_seconds = aggregator::timestamp(&current_result);         // Timestamp in seconds

        // Other properties you can use from the current result
        let min_timestamp: u64 = aggregator::min_timestamp(&current_result);    // Oldest valid timestamp used
        let max_timestamp: u64 = aggregator::max_timestamp(&current_result);    // Latest valid timestamp used
        let range: Decimal = aggregator::range(&current_result);                // Range of results
        let mean: Decimal = aggregator::mean(&current_result);                  // Average (mean)
        let stdev: Decimal = aggregator::stdev(&current_result);                // Standard deviation

        // Use the computed result as needed...
    }
}
```

Once dependencies are configured, updated aggregators can be referenced easily.

This implementation allows you to read and utilize Switchboard data feeds within Move. If you have any questions or need further assistance, please contact the Switchboard team.

### Creating an Aggregator and Sending Transactions

Building a feed in Switchboard can be done using the Typescript SDK, or it can be done with the [Switchboard Web App](https://ondemand.switchboard.xyz/aptos/mainnet). Visit the [custom feeds section](/custom-feeds/build-and-deploy-feed) for more on designing and creating feeds.

#### Building Feeds in Typescript \[optional]

```typescript
import {
  CrossbarClient,
  SwitchboardClient,
  Aggregator,
  ON_DEMAND_MAINNET_QUEUE,
  ON_DEMAND_TESTNET_QUEUE,
} from "@switchboard-xyz/aptos-sdk";
import { OracleJob } from "@switchboard-xyz/common";
import { Aptos, Account, AptosConfig, Network } from "@aptos-labs/ts-sdk"

// get the aptos client
const config = new AptosConfig({
  network: Network.MAINNET, // network a necessary param / if not passed in, full node url is required
});
const aptos = new Aptos(config);

const account = Account.generate(); // Or plug in your account

// create a SwitchboardClient using the aptos client
const client = new SwitchboardClient(aptos, "movement"); // "bardock" for testnet

// for initial testing and development, you can use the public
// https://crossbar.switchboard.xyz instance of crossbar
const crossbarClient = new CrossbarClient("https://crossbar.switchboard.xyz");

// ... define some jobs ...

const isMainnet = true; // set to false for testnet
const queue = isMainnet
  ? ON_DEMAND_MAINNET_QUEUE
  : ON_DEMAND_TESTNET_QUEUE;

const jobs: OracleJob[] = [
  OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: "https://binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        }
      },
      {
        jsonParseTask: {
          path: "$.price"
        }
      }
    ],
  }),
];

// Store some job definition
const { feedHash } = await crossbarClient.store(queue, jobs);

// try creating a feed
const feedName = "BTC/USDT";

// Require only one oracle response needed
const minSampleSize = 1;

// Allow update data to be up to 60 seconds old
const maxStalenessSeconds = 60;

// If jobs diverge more than 1%, don't allow the feed to produce a valid update
const maxVariance = 1e9; // 1%, scaled by 1e9 for this chain parameter

// Require only 1 job response (unscaled count)
const minResponses = 1;

//==========================================================
// Feed Initialization On-Chain
//==========================================================

// ... get the account object for your signer with relevant key / address ...

// get the signer address
const signerAddress = account.accountAddress.toString();

const aggregatorInitTx = await Aggregator.initTx(client, signerAddress, {
  name: feedName,
  minSampleSize,
  maxStalenessSeconds,
  maxVariance,
  feedHash,
  minResponses,
});

const res = await aptos.signAndSubmitTransaction({
  signer: account,
  transaction: aggregatorInitTx,
});

const result = await aptos.waitForTransaction({
  transactionHash: res.hash,
  options: {
    timeoutSecs: 30,
    checkSuccess: true,
  },
});

// Log the transaction results
console.log(result);

```

### Updating Feeds

```typescript
// replace with your feed address
const aggregatorId = "YOUR_FEED_ADDRESS";
const aggregator = new Aggregator(client, aggregatorId);

// Fetch and log the oracle responses
const { updates } = await aggregator.fetchUpdate();

// Create a transaction to run the feed update
// your contract address
const exampleAddress = "0x1234567890abcdef1234567890abcdef12345678";
const updateTx = await client.aptos.transaction.build.simple({
  sender: signerAddress,
  data: {
    function: `${exampleAddress}::switchboard_example::my_function`,
    functionArguments: [updates],
  },
});

// Sign and submit the transaction
const res = await aptos.signAndSubmitTransaction({
  signer: account,
  transaction: updateTx,
});

// Wait for the transaction to complete
const result = await aptos.waitForTransaction({
  transactionHash: res.hash,
  options: {
    timeoutSecs: 30,
    checkSuccess: true,
  },
});

// Log the transaction results
console.log(result);

```

### (optional) Using On-Demand with V2 interface

If you have existing code using the [Switchboard V2 interface](https://github.com/switchboard-xyz/sbv2-aptos), you can use the On-Demand adapter for full compatibility with the new On-Demand service.

#### 1. Update Move.toml

You'll need to update your `Move.toml` to include the new `switchboard` adapter address. Pick the correct one for your target network.

```diff
[addresses]
+ switchboard = "0xb3654a69ba2a252849a89fa70845ad8e713a28c322dc580ae457df1f747bb74a" # mainnet
# switchboard = "0xfe38ecf6fc57e742327af6e951e9fe2fcadcd6d1f1327ba2bee5a31e43d6637f" # testnet

[dependencies]

# nothing has to change in the switchboard v2 dependency
[dependencies.Switchboard]
git = "https://github.com/switchboard-xyz/sbv2-aptos.git"
subdir = "move/switchboard/testnet/" # change to /mainnet/ if on mainnet - or fork and change deps for a specific commit hash
rev = "main"
```

#### 2. Cranking

On-demand works on a pull-based mechanism, so you will have to crank feeds with your client-side code in order to get the latest data. This can be done using the Typescript SDK.

```typescript
const aggregator = new Aggregator(client, aggregatorId);

// update the aggregator every 10 seconds
setInterval(async () => {
  try {
    // fetch the latest update and tx to update the aggregator
    const { updateTx } = await aggregator.fetchUpdate({
      sender: signerAddress,
    });

    // send the tx to update the aggregator
    const tx = await aptos.signAndSubmitTransaction({
      signer: account,
      transaction: updateTx!,
    });
    const resultTx = await waitForTx(aptos, tx.hash);
    console.log(`Aggregator ${aggregatorId} updated!`);
  } catch (e) {
    console.error(`Error updating aggregator ${aggregatorId}: ${e}`);
  }
}, 10000);
```


# Build and Deploy Feed

This section covers how to create and deploy custom Switchboard data feeds.

### [Build with UI](/custom-feeds/build-and-deploy-feed/build-with-ui)

Use the visual interface to create feeds without writing code. Build, simulate, and publish feed definitions directly from the web app.

### [Build with TypeScript](/custom-feeds/build-and-deploy-feed/build-with-typescript)

Create and manage feeds programmatically for more complex use cases, CI/CD integration, or dynamic feed generation.

### [Deploy Feed](/custom-feeds/build-and-deploy-feed/deploy-feed)

Learn how deployment works across Solana/SVM and EVM chains, and what "deploying a feed" means for each.


# Build with UI

Build, simulate, and publish a custom Switchboard feed definition using the Feed Builder web app.

Switchboard feeds are built from **Oracle Jobs** (your data sources) and **Tasks** (the steps that fetch + transform data). The Feed Builder UI lets you assemble these visually, simulate them, and (when applicable) publish or deploy them.

> If you prefer code-first workflows, see: [Building custom feeds in TypeScript](/custom-feeds/build-and-deploy-feed/build-with-typescript).

***

## What you can do with the Feed Builder

Use the Feed Builder to:

* **Start from scratch** or **clone an existing feed**, then customize its job list.
* Build each **job** as a sequential pipeline of **tasks** (HTTP requests, JSON parsing, math transforms, DEX pricing tasks, etc.).
* **Simulate** job execution to validate that each job returns a numeric result.
* Configure feed-level **validation + freshness rules** (variance, quorum, staleness, sampling).
* Produce a feed **address/ID** you can use in on-chain programs and apps.

Open the builder here:

* <https://explorer.switchboardlabs.xyz/feed-builder>

***

## Core concepts (quick mental model)

### Feed → Jobs → Tasks

* A **Feed** is the thing your program/app reads: a single numeric value (plus metadata like timestamp/slot).
* A feed is composed of **Jobs**.
* A **Job** is a deterministic pipeline of **Tasks** (executed in order).
* Each job must end with a **numeric** output.
* The oracle network resolves the feed by aggregating job outputs (commonly a **median** across jobs).

Think of it like:

```
Feed
 ├─ Job 1: [ Task A → Task B → Task C ]  => number
 ├─ Job 2: [ Task A → Task D ]           => number
 └─ Job 3: [ Task E ]                    => number
          ↓
     Aggregate (e.g., median)
          ↓
       Feed value
```

### Queue (oracle subnet)

A **Queue** is the set of oracles that will resolve your feed. Feeds are always associated with a specific queue.

### Simulation vs deployment

* **Simulation**: run your job(s) against real sources off-chain to validate logic and observe outputs.
* **Deployment**: Store your feed definition with Crossbar to get a feed hash/ID. On all chains, feeds use canonical accounts derived from this ID—no explicit account creation needed.

This page focuses on **building and simulating** with the UI.

***

## Step-by-step: Build a feed in the UI

### 1) Choose your target network

In the upper-right, use the **network/settings** control to select the network you’re building against (e.g., mainnet vs devnet/testnet).

Why this matters:

* It determines the available queues/oracle networks and how IDs/addresses are derived.
* It affects simulation defaults and explorer routing.

### 2) Start from an existing feed (recommended)

If you’re building a common pair (like BTC/USD), start by selecting an existing feed and customizing it:

* Browse/search for the pair you want.
* Open it to inspect its configuration.
* Remove jobs you don’t want (trash/delete icon).
* Edit jobs to adjust sources or task pipelines.

This is the fastest way to learn what “good” looks like for your use case.

### 3) Add or edit jobs

Each job represents a distinct data source and/or retrieval strategy.

A solid starting point is **3+ jobs** from independent sources. For price feeds, aim for liquidity-heavy venues and reduce correlated risk.

#### Common task pipeline pattern

Most HTTP/API sources follow this pattern:

1. **HttpTask**: fetch JSON from a REST endpoint
2. **JsonParseTask**: extract a numeric field using JSONPath
3. Optional **math tasks**: normalize decimals, invert price, etc.
4. Optional **bound/validation tasks**: reject outliers

Example tasks (conceptual):

```json
[
  { "httpTask": { "url": "https://api.exchange.com/price?symbol=BTCUSD" } },
  { "jsonParseTask": { "path": "$.price" } },
  { "multiplyTask": { "multiplier": "100000000" } }
]
```

> Task reference: [Task Types Reference](/custom-feeds/task-types)

### 4) Configure feed-level validation and freshness

The Feed Builder exposes common feed-level configuration knobs:

#### Basic settings

* **Name**: the label shown in the explorer UI.
* **Authority**: the address allowed to modify feed settings later (useful for DAO/governance control).

#### Advanced settings

* **Max Variance**: maximum allowed deviation between job results for an update to be accepted.
* **Min Responses**: minimum number of successful job results required to accept an update.
* **Sample Size**: how many samples are considered when reading a feed.
* **Max Staleness**: how old a sample can be before it is considered invalid.

These parameters are your “guardrails”—they trade off liveness vs correctness. Start conservative, then tune based on observed behavior.

> Feed parameter units are surface-specific. Raw v2 `maxJobRangePct` is scaled by `1e9`, while some UI and SDK fields accept human percentages. See [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units) before copying values between surfaces.

### 5) Simulate and debug

Use the UI’s simulation flow to validate:

* Every job returns a **number**
* Results are in the same units/decimals across jobs
* Outliers are either prevented by job logic or rejected by feed-level settings

If a job fails, typical causes include:

* Bad URL / rate limits
* JSONPath returns an array or string instead of a numeric
* API returns a different schema than expected

Tip: When debugging, simplify the job:

* Start with **HttpTask + JsonParseTask**
* Add transforms only after you see a clean numeric output

### 6) Create / publish the feed

When you’re satisfied:

* Use **Connect Wallet** to associate an authority with the feed.
* The UI will create/publish the feed and redirect you to a status/details page.
* Copy the resulting **feed address/ID** — you’ll need it for on-chain integration.

***

## Best practices for robust custom feeds

### Use independent sources

Avoid three endpoints that all ultimately depend on the same upstream price.

### Normalize outputs

Make sure every job returns the same unit:

* same base/quote
* same decimals (use multiply/divide tasks to standardize)

### Prefer median-style aggregation

Median aggregation is naturally robust to single-source outliers.

### Bound outliers

Use bounding/validation where it makes sense (especially for thinly traded or highly volatile assets).

### Secrets and API keys: do it safely

If you need authenticated APIs:

* use dedicated secrets/variable mechanisms
* never hardcode keys into a job definition that you intend to share publicly

***

## Next steps

* [Build with code](/custom-feeds/build-and-deploy-feed/build-with-typescript)
* [Deploy on-chain](/custom-feeds/build-and-deploy-feed/deploy-feed)
* [Task Types Reference](/custom-feeds/task-types)


# Build with TypeScript

Design, simulate, and publish Switchboard feed definitions using TypeScript (Solana and EVM compatible patterns).

This guide is for developers who prefer code-first workflows.

You’ll learn how to:

* model a feed as a list of **Oracle Jobs**
* compose each job from sequential **Tasks**
* **simulate** jobs using Crossbar
* iterate safely and build production-grade feed definitions

> This guide focuses on designing + simulating feed definitions.\
> Deployment differs by chain — see [Deploy Feed](/custom-feeds/build-and-deploy-feed/deploy-feed).

***

## Mental model

### Oracle Jobs are “pipelines”

A job is an ordered list of tasks:

```ts
// Oracle Job
[
  httpTask,
  jsonParseTask,
  multiplyTask,
]
```

Each task runs sequentially. The job’s “current value” is updated as tasks run, and the job result is valid only if the final task produces a **numeric** value.

### Feeds are “job sets”

A feed is a set of jobs. Oracles execute the jobs and then aggregate the results (commonly median across jobs).

***

## Prerequisites

* Node.js or Bun (examples use **Bun** for simple TS execution)
* Basic TypeScript familiarity
* Your data sources (REST APIs, DEX pricing tasks, etc.)

***

## Install dependencies

### Using Bun

```bash
mkdir example
cd example
bun init
bun add @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/common@5.8.5
```

> Note: Some examples import `OracleJob` from `@switchboard-xyz/common`.\
> Adding it explicitly avoids “transitive dependency” surprises.

Keep these versions together and refresh your lockfile when upgrading. Common `5.8.5` uses the Rust/prost-compatible canonical encoding for OracleJob and OracleFeed identities. It preserves newer task fields and explicitly set optional defaults, and its encoder is isolated from other installed Common versions.

***

## Example: a minimal BTC/USDT job

This is the smallest “real” job: fetch a JSON payload and extract a price.

Create `index.ts`:

```ts
import { OracleJob, serializeOracleJob } from "@switchboard-xyz/common";

const jobs: OracleJob[] = [
  OracleJob.fromObject({
    tasks: [
      {
        httpTask: {
          url: "https://binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        },
      },
      {
        jsonParseTask: {
          path: "$.price",
        },
      },
    ],
  }),
];

console.log("Jobs JSON:\n");
console.log(JSON.stringify({ jobs: jobs.map((j) => j.toJSON()) }, null, 2));
```

At this point you have a valid feed definition (one job).

***

## Simulate your job(s) with Crossbar

Simulation runs your jobs against real sources and returns their outputs. This is how you iterate quickly before deploying or publishing.

> ⚠️ The public simulation endpoint is **heavily rate-limited**.\
> Use it for development only.

Append this to `index.ts`:

```ts
// Serialize the jobs to base64 strings.
const serializedJobs = jobs.map((oracleJob) => {
  const base64 = serializeOracleJob(oracleJob).toString("base64");
  return base64;
});

console.log("\nRunning simulation...\n");

// Call the simulation server.
const response = await fetch("https://crossbar.switchboard.xyz/api/simulate", {
  method: "POST",
  headers: [["Content-Type", "application/json"]],
  body: JSON.stringify({
    cluster: "Mainnet",
    jobs: serializedJobs,
  }),
});

// Print results
if (response.ok) {
  const data = await response.json();
  console.log(JSON.stringify(data, null, 2));
} else {
  console.error(`Simulation failed (${response.status})`);
  console.error(await response.text());
}
```

Run it:

```bash
bun run index.ts
```

You should see a response like:

```json
{
  "results": ["64158.33000000"],
  "version": "..."
}
```

***

## Building production-grade feeds

### Use multiple jobs (multiple sources)

The simplest reliability upgrade is to use several independent jobs:

* Job 1: CEX API
* Job 2: another CEX API
* Job 3: on-chain DEX price simulation task

Then rely on aggregation (median) and feed configuration (variance/quorum) to filter noise.

Conceptually:

```ts
const jobs: OracleJob[] = [
  /* source A */,
  /* source B */,
  /* source C */,
];
```

### Normalize decimals

Different sources often report:

* different quote assets
* inverted prices
* different decimal precision

Use math tasks (multiply/divide/round) so every job returns the **same unit**.

### Bound results (optional but recommended)

Bounding can be applied:

* within a job (reject a single bad API response)
* at the feed level (reject updates when jobs disagree too much)

> Feed-level validation fields use different units depending on the surface. Raw v2 `OracleFeed.maxJobRangePct` is scaled by `1e9` (`1_000_000_000` = `1%`), while `MedianTask.max_range_percent` is a human percent string. See [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units) before publishing a feed definition.

***

## Task reference

Switchboard supports many task types, including:

* [HttpTask](/custom-feeds/task-types#httptask) (REST)
* [JsonParseTask](/custom-feeds/task-types#jsonparsetask) (JSONPath extraction)
* [MedianTask](/custom-feeds/task-types#mediantask) (sub-aggregation inside a job)
* [JupiterSwapTask](/custom-feeds/task-types#jupiterswaptask) (Solana DEX price simulation)
* [SecretsTask](/custom-feeds/task-types#secretstask) (secure secret retrieval)

Full task docs:

* [Task Types Reference](/custom-feeds/task-types)

***

## Secrets, variables, and safety

### Variable overrides (`${VAR_NAME}`)

Use variable overrides to insert request-scoped values into task string fields at runtime. API keys and auth tokens are the safest default because they do not intentionally change feed semantics.

See [Data Feed Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) for the supported pattern and the full security guidance.

Override values are not included in the feed ID or signed checksum. Keep data sources, selections, and calculations fixed for permissionlessly updated feeds. Semantic overrides are supported when a controlled updater owns the request and consumers intentionally trust that caller.

***

## Where to go next

* [Deploy on-chain](/custom-feeds/build-and-deploy-feed/deploy-feed)
* [Use the visual editor](/custom-feeds/build-and-deploy-feed/build-with-ui)


# Deploy Feed

How deployment works across Solana/SVM and EVM, and what "deploying a feed" actually means per chain.

"Deploying" a Switchboard feed means making it available for use on-chain. With Switchboard's **managed update system**, this is simpler than ever:

* **Solana/SVM**: Feeds use **canonical OracleQuote accounts** derived deterministically from feed IDs. No explicit account creation needed—accounts are created automatically on first use.
* **EVM**: Feeds are identified by a deterministic `bytes32` ID. You submit oracle-signed updates to the Switchboard contract and read the latest update from storage.

Both chains follow the same pattern: **store your feed definition, get a feed ID, then use managed updates**.

***

### Prerequisites (all chains)

Before “deployment”, you should have:

* a feed definition (jobs + tasks) you have **simulated successfully**
* a clear understanding of:
  * what value your feed returns
  * decimal conventions (e.g., 1e8 vs 1e18)
  * which sources/jobs you trust and why

If you haven't designed and simulated your jobs yet, start here:

* [Build with TypeScript](/custom-feeds/build-and-deploy-feed/build-with-typescript) (code-first)
* [Build with UI](/custom-feeds/build-and-deploy-feed/build-with-ui) (UI-first)

Before you publish a v2 feed definition, confirm the feed parameter units. Raw `OracleFeed.maxJobRangePct` is scaled by `1e9`, so `1_000_000_000` means `1%`. See [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units).

***

## Solana / SVM: Deploy with Managed Updates

### What you are doing

On Solana, deployment means:

1. Choose a **queue** (oracle subnet).
2. **Store/pin** your job definition with Crossbar to get a feed hash.
3. Use the feed hash to derive the **canonical OracleQuote account** address.
4. Fetch managed update instructions and include them in your transactions.

The canonical account is created automatically on first use—no explicit initialization transaction required.

For new Solana/SVM feed-hash integrations, this quote-program path is the supported default. Do not use `PullFeed.fetchUpdateIx(...)` unless you are maintaining an existing classic PullFeed account and the selected queue/gateway environment explicitly supports the legacy PullFeed update flow.

### Requirements

* A funded Solana keypair file (payer)
* A Solana RPC URL
* Your OracleJob\[] definition

Create a keypair file if you don't have one:

```bash
solana-keygen new --outfile path/to/solana-keypair.json
```

### Install

```bash
bun add @switchboard-xyz/on-demand@3.10.6 @switchboard-xyz/common@5.8.5
```

### Deployment flow (TypeScript)

Below is the deployment flow using managed updates. You can merge this into the same project where you built/simulated your jobs.

```ts
import { CrossbarClient, OracleJob } from "@switchboard-xyz/common";
import {
  AnchorUtils,
  OracleQuote,
  getDefaultQueue,
  getDefaultDevnetQueue,
  asV0Tx,
} from "@switchboard-xyz/on-demand";

// 1) Your simulated job definitions
const jobs: OracleJob[] = [
  /* ... */
];

// 2) Choose cluster + RPC
const connection = /* new Connection(RPC_URL) */;

// 3) Choose the queue (oracle subnet)
const queue = await getDefaultQueue(connection.rpcUrl);
// or: const queue = await getDefaultDevnetQueue(connection.rpcUrl);

// 4) Store jobs with Crossbar and get a feedHash
const crossbarClient = CrossbarClient.default();
const { feedHash } = await crossbarClient.store(queue.pubkey.toBase58(), jobs);
console.log("Feed hash:", feedHash);

// 5) Derive the canonical OracleQuote account address
// This is deterministic - same feed hash always produces the same address
const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedHash]);
console.log("Quote account:", quoteAccount.toBase58());

// 6) Load payer (funded)
const payer = await AnchorUtils.initKeypairFromFile("path/to/solana-keypair.json");

// 7) Fetch managed update instructions
// This returns Ed25519 verification + quote storage instructions
const updateIxs = await queue.fetchManagedUpdateIxs(crossbarClient, [feedHash], {
  payer: payer.publicKey,
});

// 8) Build and send a transaction to verify everything works
const tx = await asV0Tx({
  connection,
  ixs: updateIxs,
  payer: payer.publicKey,
  signers: [payer],
});

const sig = await connection.sendTransaction(tx);
console.log("Transaction signature:", sig);
console.log("Feed deployed! Quote account:", quoteAccount.toBase58());
```

To read the stored quote account, parse it with the SDK/Rust quote-account types instead of fixed byte offsets. See [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts).

#### Using validation parameters

Validation parameters (min responses, max variance, staleness) are now specified when fetching updates or reading data, rather than at deployment time:

```ts
// When reading in your program, use QuoteVerifier with max_age
const quote_data = QuoteVerifier::new()
    .max_age(30)  // Reject data older than 30 slots
    .verify_account(quote)?;
```

#### Make it discoverable

Storing with Crossbar pins the feed definition to IPFS and makes it easier to view/debug in the Switchboard explorer. The feed hash is the key identifier you'll use throughout your integration.

***

## EVM: “Deploying” is publishing a feed ID + updating via the Switchboard contract

### Why there isn’t a dedicated “deploy feed” step on EVM

On EVM, a feed is identified by a **deterministic `bytes32` feed ID**. You can treat deployment as:

1. Obtain the feed ID (often from the Feed Builder / Explorer).
2. Store that feed ID in your consumer contract or app.
3. Fetch oracle-signed updates off-chain.
4. Submit updates on-chain via `updateFeeds`.

This is the same pattern Solana now uses with managed updates—no explicit account creation needed.

### On-chain: reading and updating

A typical Solidity integration looks like:

* Store the Switchboard contract address + your feedId
* When you need fresh data:
  * compute the required fee
  * submit `updateFeeds(updates)`
  * read `latestUpdate(feedId)`

```solidity
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {ISwitchboard} from "@switchboard-xyz/on-demand-solidity/ISwitchboard.sol";
import {Structs} from "@switchboard-xyz/on-demand-solidity/structs/Structs.sol";

contract Example {
    ISwitchboard switchboard;
    bytes32 feedId;

    error InsufficientFee(uint256 expected, uint256 received);
    error InvalidResult(int128 result);

    constructor(address _switchboard, bytes32 _feedId) {
        switchboard = ISwitchboard(_switchboard);
        feedId = _feedId;
    }

    function getFeedData(bytes[] calldata updates) external payable returns (int128) {
        uint256 fee = switchboard.getFee(updates);
        if (msg.value < fee) revert InsufficientFee(fee, msg.value);

        switchboard.updateFeeds{value: fee}(updates);

        Structs.Update memory latest = switchboard.latestUpdate(feedId);
        if (latest.result < 0) revert InvalidResult(latest.result);

        return latest.result;
    }
}
```

> Many feeds use `int128` scaled by `1e18` to avoid floating point issues.\
> Always consult the feed’s intended decimal convention.

### Off-chain: fetch encoded updates with Crossbar (TypeScript)

Use Crossbar to fetch oracle-signed updates (encoded) for submission:

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

const feedId = "0x...your_feed_id...";
const crossbarNetwork = "testnet"; // or "mainnet"

const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

// Recommended preflight for Feed Builder feeds:
await crossbar.fetchOracleFeed(feedId);
await crossbar.simulateFeed(feedId, false, undefined, crossbarNetwork);

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

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

const updates = [response.encoded];

// submit `updates` to your contract method that calls updateFeeds(...)
```

Recommended EVM preflight order for custom feeds:

1. `GET /v2/fetch/{feedId}` or `crossbar.fetchOracleFeed(feedId)` to confirm the definition exists
2. `GET /v2/simulate/{feedId}?network=testnet|mainnet` or `crossbar.simulateFeed(...)` to confirm the jobs resolve
3. `GET /v2/update/{feedId}?chain=evm&network=testnet|mainnet&use_timestamp=true` or `crossbar.fetchV2Update(...)` to obtain the EVM payload

Use the same deterministic `feedId` / feed hash from Feed Builder or Explorer for all three steps.

> Do not pass a Feed Builder `bytes32` feed ID into `fetchEVMResults()` or `simulateEVMFeeds()` unless you are intentionally using the legacy aggregator-based EVM flow.

***

### Do we need “deployment docs” for other chains?

Switchboard supports additional chains with chain-specific SDKs and verification flows (e.g., Move-based environments). Many of these integrations follow the **EVM-style** model: you fetch oracle consensus off-chain and include a verification/update step inside your transaction, rather than creating a dedicated on-chain “feed account”.

If you’re targeting a non-Solana chain, treat “deployment” as:

1. Create/publish a feed definition and get its feed ID/address.
2. Use the chain’s SDK to fetch and verify oracle results in your transaction flow.


# Advanced Feed Configuration

FAQ on Feed Resolution and Common Feed Examples

Switchboard Feeds enable seamless access to data from any API, oracle, major DeFi protocol, and more. Our mission is to simplify the process of retrieving diverse data types—such as price data and event data—in a secure and user-friendly manner.

Switchboard data feeds are composed of [Oracle Jobs](/custom-feeds/build-and-deploy-feed/build-with-typescript#oracle-jobs-are-pipelines), which define where to source data. Feeds specify a list of different [Task Types](/custom-feeds/task-types), which are used as instructions to fetch data.

If you are configuring feed validation, start with [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units). Raw v2 `maxJobRangePct` is scaled by `1e9`, while task-level percent fields and SDK helper fields may use human percentages.

This section will explore different task types and give an in-depth explanation on how to build oracle jobs.

## Task Runner

All Oracle Jobs are executed by the task-runner, an engine used by oracles to fetch data in a secure and efficient manner. Oracle Jobs must define an array of tasks executed sequentially. Any task producing a value (String, JSON, or Decimal) will be assigned to the job's context for that particular run, and subsequent tasks will manipulate that current task.

Here's a brief overview of what a job might look like (without including full tasks):

```typescript
// Oracle Job
[
    httpTask,
    jsonParseTask,
    multiplyTask, 
]
```

So here we'd:

1. Fetch a result from some API and set that blob to context
2. Parse context and replace with value at jsonPath specified
3. Multiply value in context by some number

See the next page for more on the task runner.


# Feed Parameter Units

Units and fixed-point scaling for Switchboard feed configuration parameters.

Feed definitions combine validation parameters, quorum parameters, freshness limits, and returned feed values. These fields do not all use the same unit or fixed-point scale.

The most common mistake is treating raw v2 `OracleFeed.maxJobRangePct` / `max_job_range_pct` as a human percent. It is a fixed-point percent scaled by `1e9`:

* `1_000_000_000` means `1%`
* `5_000_000_000` means `5%`
* `5` does not mean `5%`; it is effectively zero tolerance

| Field or surface                                         | Unit                                                            | Example                                                           | Notes                                                                                                                                                                                                       |
| -------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Raw v2 `OracleFeed.maxJobRangePct` / `max_job_range_pct` | Percent scaled by `1e9`                                         | `1_000_000_000` = `1%`; `5_000_000_000` = `5%`                    | Used in raw protobuf/JSON feed definitions. This is the feed-level job-spread tolerance oracles enforce before signing an update.                                                                           |
| SDK helper `maxVariance` inputs that scale internally    | Human percent                                                   | `1` or `1.0` = `1%`                                               | Some SDK helper methods accept the human percentage and multiply by `1e9` before sending a gateway or on-chain request. Check the method docs before passing raw integers.                                  |
| Common `FeedRequestV1.maxVarianceScaled`                 | Percent scaled by `1e9`                                         | `8_271_619` is sent exactly as `8_271_619`                        | Use this when forwarding an exact raw value read from a classic PullFeed account. It must be a nonnegative JavaScript safe integer. Do not provide it together with `maxVariance`.                          |
| Direct gateway/raw API `max_variance` fields             | Percent scaled by `1e9`                                         | `50_000_000` = `0.05%`; `1_000_000_000` = `1%`                    | Use this for raw gateway payloads and chain parameters that already expect fixed-point validation values. Some JSON routes expose this as camelCase `maxVariance` while still expecting the scaled integer. |
| `MedianTask.max_range_percent`                           | Human percent string                                            | `"2.5"` = `2.5%`                                                  | This is a task-level setting inside `MedianTask`. It is not scaled like feed-level `maxJobRangePct`.                                                                                                        |
| `minJobResponses` / `min_job_responses`                  | Unscaled job/source quorum                                      | `2` requires at least two successful job results                  | This counts successful jobs inside one oracle's feed execution. It does not change percent scaling.                                                                                                         |
| `minOracleSamples` / `min_oracle_samples`                | Unscaled oracle/signature quorum                                | `3` requires three oracle samples                                 | This counts oracle responses/signatures. It is separate from job/source quorum.                                                                                                                             |
| Feed result values                                       | Feed-specific numeric convention, often `i128` scaled by `1e18` | `1_000_000_000_000_000_000` may represent `1.0`                   | Result-value scaling is independent from validation parameter scaling. Always document the feed's value decimals separately.                                                                                |
| Staleness fields                                         | Slots, seconds, or milliseconds depending on the surface        | `maxStaleness: 150` slots; `maxAgeSeconds: 60`; `maxAgeMs: 60000` | Solana/SVM verifier settings often use slots. EVM and Move-chain examples commonly use seconds. Sui examples may use milliseconds.                                                                          |

## Practical Rules

When you create a raw v2 `OracleFeed`, use scaled integers for feed-level range validation:

```ts
const feed = {
  minJobResponses: 2,
  minOracleSamples: 3,
  maxJobRangePct: 1_000_000_000, // 1%, scaled by 1e9
  jobs,
};
```

Use `maxJobRangePct: 0` only when the flow intentionally expects a single successful job/source or identical outputs. For normal multi-source feeds, set a positive scaled tolerance.

Simulation can succeed even when signed updates fail. Simulation proves the jobs can resolve off-chain; signed updates also require oracle-side feed validation to pass. If update fetching returns `ORACLE_UNAVAILABLE` after successful simulation, inspect oracle errors for validation failures such as `RangeExceeded`, then check whether `maxJobRangePct` was scaled correctly.

## Classic PullFeed Variance

`@switchboard-xyz/on-demand@3.10.6` reads a classic PullFeed account's exact on-chain `maxVariance` integer and forwards it through `FeedRequestV1.maxVarianceScaled`. Application code should not convert that account value to a floating-point percentage and scale it again.

When calling the Common gateway API directly, use `maxVariance` for a human percentage or `maxVarianceScaled` for an already-scaled raw integer, never both. Raw values above `Number.MAX_SAFE_INTEGER` are rejected before a gateway request because the current JSON number transport cannot represent them exactly.

This feed-validation failure is separate from using the wrong Solana/SVM update path. If `PullFeed.fetchUpdateIx(...)` or `pullFeedSubmitResponseConsensus` returns `ORACLE_UNAVAILABLE` while `queue.fetchManagedUpdateIxs(...)` returns Ed25519 quote-program instructions, move the integration to canonical quote-program accounts; see [Quote Program Accounts](/docs-by-chain/solana-svm/price-feeds/quote-program-accounts).


# Data Feed Variable Overrides

Configure request-scoped values in oracle jobs without hardcoding credentials.

Variable overrides substitute request-scoped values into string fields in an oracle job. Put a placeholder such as `${MARKET_DATA_API_KEY}` in the job definition, then provide the matching non-empty value in `variableOverrides` when requesting execution.

Overrides are a general substitution feature. They can represent credentials, URLs, symbols, paths, headers, query parameters, or other string values. Whether a particular override is appropriate depends on who controls updates and what feed consumers trust.

## Trust Model

The concrete override map is execution-scoped. It is not included in the feed ID or the signed checksum. A consumer can identify the job definition containing `${VARIABLE_NAME}`, but cannot recover or independently verify the value substituted for that placeholder from the feed identity or signature.

TEE-backed execution protects how selected oracle software handles a request, but it does not make override values part of the feed definition. The execution nodes necessarily receive values that their tasks must use, so credentials should be scoped to the intended API and request path.

Choose override values according to the update model:

| Update model                | Appropriate use                                                                                                                                                                           |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permissionless feed updates | Prefer credentials and other values that do not intentionally change the data source, selected market, extraction path, or calculation. Keep semantic inputs fixed in the job definition. |
| Controlled updater          | Semantic values such as a URL, symbol, or path are supported when the caller controls every execution request and consumers intentionally trust that caller to select them.               |

One execution request should represent one customer or security domain. Do not combine unrelated customers' jobs and credentials in one request.

## Credential Example

Keep the data source and extraction logic fixed while substituting only the credential:

```typescript
const apiKey = process.env.MARKET_DATA_API_KEY;
if (!apiKey) {
  throw new Error("MARKET_DATA_API_KEY is required");
}

const job = {
  tasks: [
    {
      httpTask: {
        url: "https://api.example.com/v1/markets/BTC-USD/price",
        headers: [
          {
            key: "authorization",
            value: "Bearer ${MARKET_DATA_API_KEY}",
          },
        ],
      },
    },
    {
      jsonParseTask: {
        path: "$.price",
      },
    },
  ],
};

const response = await gateway.fetchSignaturesConsensus({
  // ...feed request fields containing job
  variableOverrides: {
    MARKET_DATA_API_KEY: apiKey,
  },
});
```

Placeholder names are case-sensitive and must match the map key exactly. Use environment variables or a secret manager as the source of credential values. Never hardcode credentials in a job, commit them, include them in errors, or log override values.

## Semantic Overrides

Semantic overrides are valid for a controlled caller. For example, a backend that owns the request and whose consumers trust its market selection can use:

```typescript
const job = {
  tasks: [
    {
      httpTask: {
        url: "https://api.example.com/v1/markets/${MARKET}/price",
      },
    },
    {
      jsonParseTask: {
        path: "$.price",
      },
    },
  ],
};

const response = await gateway.fetchSignaturesConsensus({
  // ...feed request fields containing job
  variableOverrides: {
    MARKET: "BTC-USD",
  },
});
```

This request can produce a different result for each `MARKET` value while retaining the same feed identity. Do not use this pattern on a permissionless update path where an untrusted updater could select the value.

## Jupiter and Pyth API Keys

Jupiter and Pyth both support customer-provided API keys, but their fallback rules differ:

| Task                             | Preferred task field                    | Override behavior                                                                                                                                                                                                                                                                                       |
| -------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JupiterSwapTask                  | `apiKey: "${JUPITER_API_KEY}"`          | `JUPITER_API_KEY` is a conventional example name, not reserved. The override is applied only when the field contains the matching placeholder. An unresolved placeholder fails before contacting Jupiter. If the field is omitted or empty, the oracle's configured Jupiter key is used when available. |
| OracleTask with `pythAddress`    | `pythConfigs.apiKey: "${PYTH_API_KEY}"` | A non-empty task field takes precedence. If it is omitted or empty, the reserved request override `PYTH_API_KEY` is a compatibility fallback for every eligible `pythAddress` task in that request. The fallback is not shared with another request or customer.                                        |
| OracleTask with `pythPushFeedId` | None                                    | The task reads an on-chain account and does not use Hermes authentication.                                                                                                                                                                                                                              |

For new Pyth feed definitions, use the explicit task placeholder. The request-wide `PYTH_API_KEY` fallback exists so existing `pythAddress` jobs can adopt authenticated Hermes access without changing each stored job during the Pyth Core transition.

See [Jupiter API keys](/custom-feeds/advanced-feed-configuration/decentralized-exchanges#jupiter-exchange-aggregator), [Pyth authentication and push feeds](/custom-feeds/advanced-feed-configuration/oracle-aggregator#pyth), [JupiterSwapTask](/custom-feeds/task-types#jupiterswaptask), and [OracleTask](/custom-feeds/task-types#oracletask) for task-specific examples and fields.

## Operational Guidance

* Validate that every required override is present and non-empty before sending the request.
* Use least-privilege, rate-limited credentials and rotate them through your secret manager.
* Log placeholder names or whether configuration is present, never override values.
* Keep one customer's jobs and credentials within that customer's execution request.
* For permissionless feeds, keep the endpoint, selected data, parsing path, and calculations fixed in the job definition.

## Related Resources

* [REST APIs with HttpTask](/custom-feeds/advanced-feed-configuration/rest-apis-with-httptask)
* [Variables with CacheTask](/custom-feeds/advanced-feed-configuration/variables-with-cachetask)
* [Build with TypeScript](/custom-feeds/build-and-deploy-feed/build-with-typescript)
* [Task Types Reference](/custom-feeds/task-types)


# Variables with CacheTask

Storing variables with CacheTask

You can store data in the task-runner's variable cache by using a CacheTask. This is a useful tool when you have a task that is complex, or requires the use of a lot of dynamically computed numbers.

## CacheTask

The following is an example of a CacheTask specifying multiple variables, building off of one another.

```typescript
{
  cacheTask: {
    cacheItems: [
      {
        // Create FAIR_VALUE_HEX from the hex returned from an ETH RPC Call
        variableName: "FAIR_VALUE_HEX",
        job: {
          tasks: [
            {
              httpTask: {
                url: "https://rpc.exampleRPC.org/",
                method: 2,
                headers: [
                  { key: "content-type", value: "application/json" },
                ],
                body: '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0xf5fa1728babc3f8d2a617397fac2696c958c3409","data":"0x3ca967f3"},"latest"]}',
              },
            },
            {
              jsonParseTask: {
                path: "$.result",
              },
            },
          ],
        },
      },
      // Parse the hex and scale it down 10^6, store the result in FAIR_VALUE
      {
        variableName: "FAIR_VALUE",
        job: {
          tasks: [
            {
              valueTask: {
                hex: "${FAIR_VALUE_HEX}",
              },
            },
            {
              divideTask: {
                big: "1000000",
              },
            },
          ],
        },
      },
      
      // Create a new variable, FAIR_VALUE_LOW, which is value * 0.95
      {
        variableName: "FAIR_VALUE_LOW",
        job: {
          tasks: [
            {
              valueTask: {
                big: "${FAIR_VALUE}",
              },
            },
            {
              multiplyTask: {
                big: "0.95",
              },
            },
          ],
        },
      },
      
      // Create a new variable, FAIR_VALUE_HIGH, which is value * 1.05
      {
        variableName: "FAIR_VALUE_HIGH",
        job: {
          tasks: [
            {
              valueTask: {
                big: "${FAIR_VALUE}",
              },
            },
            {
              multiplyTask: {
                big: "1.05",
              },
            },
          ],
        },
      },
    ],
  },
},
```

This large task does quite a number of things. It does the following:

1. It calls an EVM contract and parses some hex using a JsonParseTask, then creates a new variable called `FAIR_VALUE_HEX` that stores a JSON value.
2. It parses that hex into a new variable, `FAIR_VALUE`
3. It creates a low value to be used in subsequent jobs by using a `MultiplyTask` and setting 0.95 \* `FAIR_VALUE` to `FAIR_VALUE_LOW`
4. It creates a high value to be used in subsequent jobs by using the same method to set 1.05 \* `FAIR_VALUE` to `FAIR_VALUE_HIGH`

In subsequent jobs one can reference the values locked in these variables.

## Math Tasks

Variables are easily combined with math-related tasks, `AddTask`, `MultiplyTask`, `SubtractTask` , and `DivideTask` are some really common ones. These tasks will run agains whatever number is in the current\_value of the task-runner context.

They can also be combined with `ValueTask` to pull a variable into the current\_value position.

For example:

```typescript
// ... CacheTask from above
{
    valueTask: {
        big: "${FAIR_VALUE}"
    } 
},
{
    multiplyTask: {
        scalar: 2
    }
}
```

Inside of Math Tasks you can specify one of the following fields:

```typescript
/** Specifies a scalar to multiply by. */
scalar?: number | null;

/** Specifies an aggregator to multiply by. */
aggregatorPubkey?: string | null;

/** A job whose result is computed before multiplying our numerical input by that result. */
job?: oracle_job.IOracleJob | null;

/** A stringified big.js. `Accepts variable expansion syntax.` */
big?: string | null;
```


# REST APIs with HttpTask

How to use HttpTask and JsonParseTask

## HttpTask + JsonParseTask

Next to Oracle and Decentralized Exchange tasks, the most popular way to fetch data is the HttpTask combined with a JsonParseTask. The `HttpTask` has support for GET and POST methods, along with custom headers and a request body (both optional).

#### Example

```typescript
{
    httpTask: {
        url: "https://www.binance.com/api/v3/ticker/price"
    }
}
```

This [Binance ](https://www.binance.com/en/trade/BTC_USDT)api call will result in a json response of the following structurefor all tokens:

```typescript
{"symbol":"BTCUSDT","price":"64628.31000000"} ...
```

In order to pull out the `price` field from the response, we must follow the HttpTask along with a `JsonParseTask`. These tasks utilize [JsonPaths](https://goessner.net/articles/JsonPath/) in order to specify which data to put into the current context. In this case, since we just ran an HttpTask and received this JSON, we have that JSON set as the current\_value.

Now, the user can use a JsonParseTask to specify the path of the decimal they want to extract, in this case the field `BTC-USDT price`.

```typescript
{
    jsonParseTask: {
        path: "$[?(@.symbol == 'BTCUSDT')].price"
    }
}
```

### Binance BTC/USDT

```typescript
{
    httpTask: {
        url: "https://www.binance.com/api/v3/ticker/price"
    }
},
{
    jsonParseTask: {
        path: "$[?(@.symbol == 'BTCUSDT')].price"
    }
}
```

This is the assembled tasks to form a Binance BTC/USDT task.

### OKX BTC/USDT

<pre class="language-typescript"><code class="lang-typescript">{
<strong>    httpTask: {
</strong>        url: "https://www.okx.com/api/v5/market/index-tickers?quoteCcy=USD",
<strong>    },
</strong>},
{
    jsonParseTask: {
        path: '$.data[?(@.instId == "BTC-USDT")].idxPx',
    },
},
</code></pre>

Pulling the latest price of BTC in USDT terms from OKX. Just another common HttpTask example, this time getting a value from an array. The return type for the OKX value in this example is:

```typescript
// okx.com endpoint price return
{
  "code": "0",
  "msg": "",
  "data": [
    {
      "instId": "BTC-USDT",
      "idxPx": "64628",
      "high24h": "66484.2",
      "sodUtc0": "64877.1",
      "open24h": "65308.9",
      "low24h": "64319.8",
      "sodUtc8": "64827.5",
      "ts": "1718946430031"
    }
  ]
}
```

### Kraken Example

```typescript
// Also BTC/USD, but from Kraken
{
  httpTask: {
    url: "https://api.kraken.com/0/public/Ticker",
  },
},
{
  medianTask: {
    tasks: [
      {
        jsonParseTask: {
          path: "$.result.XXBTZUSD.a[0]",
        },
      },
      {
        jsonParseTask: {
          path: "$.result.XXBTZUSD.b[0]",
        },
      },
      {
        jsonParseTask: {
          path: "$.result.XXBTZUSD.c[0]",
        },
      },
    ],
  },
}
```

Here you can see that the user who made this job is using a `MedianTask` . This allows users to run multiple jobs in parallel and get the median result. Users can also specify a `maxRangePercent` in the median task in order to fail the request if that value is exceeded.

For example:

```typescript
medianTask: {
    maxRangePercent: "1.5", // if job results differ by 1.5%, the job run will fail
    tasks: [
        {
            jsonParseTask: {
                path: "$.result.XXBTZUSD.a[0]",
            },
        },
        {
            jsonParseTask: {
               path: "$.result.XXBTZUSD.b[0]",
            },
        },
        {
            jsonParseTask: {
                path: "$.result.XXBTZUSD.c[0]",
            }
        },
    ],
},
```

### Huobi Example

```typescript
// Huobi BTC/USDT
{
  httpTask: {
    url: "https://api.huobi.pro/market/tickers",
  },
},
{
  medianTask: {
    tasks: [
      {
        jsonParseTask: {
          path: "$[?(@.symbol == 'btcusdt')].bid",
        },
      },
      {
        jsonParseTask: {
          path: "$[?(@.symbol == 'btcusdt')].ask",
        },
      },
    ],
  },
},
```

## POST data with HttpTask

The following is an example with a POST request including headers and a body in an HttpTask:

```typescript
{
  httpTask: {
    url: "https://example.com",
    method: 2, // POST
    headers: [
      {
        key: "Content-Type",
        value: "application/json",
      },
      {
        key: "Accept-Language",
        value: "en-US,en;q=0.9",
      },
      {
        key: "Accept",
        value: "*/*",
      },
    ],
    body: '{"data":["chorizo","fried"],"cluster":"taco-loco"}',
  },
},
```

This is a POST request to example.com. Note that body has to be an encoded JSON string if it is set at all.


# How Feeds are Resolved

Feed resolution description and variable expansion tutorial.

## Task Runner

All Switchboard Oracle Jobs are executed by the task-runner, an engine used by oracles to fetch data in a secure and efficient manner. Understanding how Oracle jobs are processed is essential before creating them. Oracle Jobs must define an array of tasks executed sequentially. Any task producing a value (String, JSON, or Decimal) will be assigned to the job's context for that particular run.

The Task Runner Context can be interpreted as:

```typescript
// Context 
{
    current_value: string | JSON | Decimal,
    variable_cache: Map<string, string | JSON | Decimal>,
}
```

## Schemas

Switchboard feeds are composed of Oracle Jobs, a schema designed for efficient and safe fetching of arbitrary numeric data from various sources. Oracle nodes run feeds by aggregating the results of jobs within a feed definition and computing a median.

#### **Feeds Schema**

```
{
    jobs: [
        // Oracle Job 1
        {
            tasks: [ ... ]
        },
        // Oracle Job 2
        {
            tasks: [ ... ]
        }
    ]
}
```

Oracle Jobs are composed of tasks. Tasks are like instructions to fetch data or compute certain outputs. There are several [Task Types](/custom-feeds/task-types) available, and they can be strung together to create some complex logic.

#### Oracle Jobs Schema

```
{
    tasks: [
        // task 1
        {
            ...
        },
        // task 2
        {
            ...
        }
    ]
}
```

## Feed Identity

A feed ID is a commitment to canonical length-delimited protobuf bytes, not only to the decoded JSON object. Field presence and encoding order therefore matter, including explicitly set default values.

Use `@switchboard-xyz/common@5.8.5` or newer when serializing jobs and feeds, computing `FeedHash`, or storing definitions with Crossbar. Use `@switchboard-xyz/on-demand@3.10.6` or newer when requesting JavaScript updates. These versions:

* encode job and feed identities in the Rust/prost-compatible declaration order;
* preserve Pyth-push fields and explicitly set optional defaults such as `pushFeedShardId: 0` and `directRoutesOnly: false`; and
* keep the canonical encoder isolated if another Common version is also loaded.

Use the same canonical definition for hashing, storage, and update requests. For classic PullFeed accounts, the JavaScript update helpers reject any returned median-response feed hash that was not requested before constructing signature or submit instructions.

## Variable Expansion

Using [Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) and the [CacheTask](/custom-feeds/advanced-feed-configuration/variables-with-cachetask), users can assign variables in a job and use them within the same job in a downstream task.

So in a downstream task, you can invoke the variable with the syntax: `${VARIABLE_NAME}.`

Here's an HttpTask where the URL is pulled from a variable:

```
[
    // ... Cache Task or Secrets Task (or both) must come first ...
    {
        httpTask: {
            url: "${MY_CONFIGURED_HTTP_URL}"
        }
    },
    {
        jsonParseTask: {
            path: "$.price"
        }
    }
]
```

Internally, the variables are being string-replaced within the executed job definitions. This can be a good tool for deduplicating logic in complex jobs.

## Resolution

In order for a task runner result to be valid, its current\_value must be some numeric value. Intermediate calls may produce non-numeric values (like HttpTasks, JsonParseTasks, etc), but the final value will be the result of the job.


# Bounding Results

It can be useful to bound feeds by upper and lower values. These can be computed dynamically with the [CacheTask ](/custom-feeds/advanced-feed-configuration/variables-with-cachetask)and can allow for a simple sanity check / safety check on some feeds.

## Bound Task

You can use static numbers for bounding the result of a feed. Here's an example for USDT/USD from Coinbase. In this instance the protocol wants this feed to not resolve outside of those bounds.

```typescript
{
    httpTask: {
        url: "https://api.coinbase.com/v2/prices/USDT-USD/spot",
    },
},
{
    jsonParseTask: {
        path: "$.data.amount",
    },
},
{
    boundTask: {
        lowerBoundValue: "0.98",
        upperBoundValue: "1.02",
    },
},
```

Sometimes users may want to bound some value dynamically. Combining this with the `FAIR_VALUE` [Example from the previous section](/custom-feeds/advanced-feed-configuration/variables-with-cachetask#cachetask), we get:

<pre class="language-typescript"><code class="lang-typescript"><strong>// ... CacheTasks defining FAIR_VALUE LOW, FAIR_VALUE_HIGH... 
</strong><strong>{
</strong>    httpTask: {
        url: "https://api.volatile-source.com/",
    },
},
{
    jsonParseTask: {
        path: "$.price",
    },
},
{
    boundTask: {
        lowerBoundValue: "${FAIR_VALUE_LOW}",
        upperBoundValue: "${FAIR_VALUE_HIGH}",
    },
},
</code></pre>


# Decentralized Exchanges

Some common tasks relating to Decentralized Exchanges and DeFi.

## **Jupiter Exchange Aggregator**

Users can fetch data from the [Jupiter Exchange Aggregator](https://jup.ag/). This can be an effective tool for quoting assets traded on Solana.

```typescript
// KMNO/USD with 2% slippage
{
  jupiterSwapTask: {
    inTokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
    outTokenAddress: "KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", // KMNO
    slippage: 2.0,
    apiKey: "${JUPITER_API_KEY}",
  },
}
```

Supply the matching non-empty value with the execution request:

```typescript
const response = await gateway.fetchSignaturesConsensus({
  // ...feed request fields
  variableOverrides: {
    JUPITER_API_KEY: process.env.JUPITER_API_KEY!,
  },
});
```

`JUPITER_API_KEY` is a conventional name, not a reserved fallback. The override key must exactly match the placeholder in `apiKey`. If the placeholder is unresolved, the task fails before contacting Jupiter. If `apiKey` is omitted or empty, the task uses the oracle's configured Jupiter API key when one is available.

Do not hardcode an API key in the job definition. See [Data Feed Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) for request scope and trust-boundary guidance, [Pyth authentication](/custom-feeds/advanced-feed-configuration/oracle-aggregator#pyth) for its different compatibility fallback, and the [JupiterSwapTask reference](/custom-feeds/task-types#jupiterswaptask) for every field.

## **Raydium**

[Raydium](https://raydium.io/) is an active hub for DeFi activity on Solana, and Switchboard allows you to quote assets from its Concentrated and Standard pools. All you need is a pool address.

The following is an example using Raydium to quote [SLERF/SOL](https://dexscreener.com/solana/agfnrluscrd2e4nwqxw73hdbsn7ekeub2jhx7tx9ytyc), and converting to its USD value using a Pyth task with SOL/USD.

```typescript
// SLERF/USD using Raydium and Pyth
{
  lpExchangeRateTask: {
    raydiumPoolAddress: "AgFnRLUScRD2E4nWQxW73hdbSN7eKEUb2jHX7tx9YTYc", // SLERF/SOL Pool
  },
},
{
  multiplyTask: {
    job: {
      tasks: [
        {
          oracleTask: {
            pythAddress: "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG", // SOL/USD
            pythConfigs: {
              pythAllowedConfidenceInterval: 1.2,
            },
          }
        }
      ]
    }
  }
}
```

## **Orca**

You can pull exchange rates from [Orca](https://www.orca.so/pools) (Solana AMM). Plugging in any of these pools will yield the resulting price.

```typescript
// SOL/USDC Orca Pool (Aquafarms & Whirlpools)
{
    lpExchangeRateTask: {
        orcaPoolAddress: "APDFRM3HMr8CAGXwKHiu2f5ePSpaiEJhaURwhsRrUUt9",
    }
}
```

## **Meteora**

You can read in prices from [Meteora](https://meteora.ag/) using a `meteoraSwapTask`. You can use it to pull in exchange rates from DLMM pools and standard pools.

```
// mSOL/SOL using Meteora
{
  meteoraSwapTask: {
    pool: "HcjZvfeSNJbNkfLD4eEcRBr96AD3w1GpmMppaeRZf7ur",
    type: 1 // 0 for DLMM, 1 for standard
  }
}
```

## Sanctum

The LST protocol on Solana, [Sanctum](https://sanctum.so), is also supported. This task is useful for users who want to bring fair Solana LST prices into their protocols through Switchboard.

```
// bonkSOL/USD
{
  sanctumLstPriceTask: {
    lstMint: "BonK1YhkXEGLZzwtcvRTip3gAL9nCeQD7ppZBLXhtTs"
  }
}
```

## **Uniswap**

[Uniswap](https://uniswap.org/) is probably the most recognized AMM that exists. Switchboard enables users to pull prices from the protocol, along with clones on other chains.

Using the `uniswapExchangeRateTask`, users can bring in exchange rates from [Uniswap V2](https://blog.uniswap.org/uniswap-v2) and [Uniswap V3](https://blog.uniswap.org/uniswap-v3), along with any Uniswap-ABI-compatible protocols (/Uniswap clones on other chains).

Here's an example of a Uniswap Task pulling data from the [glyph.exchange](https://glyph.exchange) protocol, on [Core](https://coredao.org), which is a Uniswap V2 fork.

```typescript
// StCore/wCORE on Glyph, a Uniswap V2 Protocol
{
  uniswapExchangeRateTask: {
    provider: "https://rpc.coredao.org",
    inTokenAddress:
      "0xb3A8F0f0da9ffC65318aA39E55079796093029AD",
    outTokenAddress:
      "0x191e94fa59739e188dce837f7f6978d84727ad01",
    inTokenAmount: 1.0,
    slippage: 0.5,
    version: 0,
    routerAddress: "0xbfa47708e79d446f0ecc9a9b07a87d5aa788f9df",
    factoryAddress:
      "0x3e723c7b6188e8ef638db9685af45c7cb66f77b9",
  },
},
```

In the same task for Uniswap V3, a Quoter address should also be passed. Here's an example from [corex.network](https://corex.network), a Uniswap V3 clone also on Core.

```typescript
// StCore/wCORE on Corex, a Uniswap V3 Protocol
{
  uniswapExchangeRateTask: {
    provider: "https://rpc.coredao.org",
    inTokenAddress:
      "0xb3A8F0f0da9ffC65318aA39E55079796093029AD",
    outTokenAddress:
      "0x40375c92d9faf44d2f9db9bd9ba41a3317a2404f",
    inTokenAmount: 1.0,
    slippage: 0.5,
    version: 1,
    factoryAddress:
      "0x526190295AFB6b8736B14E4b42744FBd95203A3a",
    routerAddress: "0xcc85A7870902f5e3dCef57E4d44F42b613c87a2E",
    quoterAddress: "0xaec2F2306EBEA7f1251ccAB8A409A48a8d8aAa61",
  },
},
```

## OpenBook

The [OpenBook](https://openbookdex.com/) codebase was the first major CLOB on Solana, and we support it through the `serumSwapTask` . The following is an example of pulling a SOL/USDC price:

```json
// Openbook SOL/USDC
{
  "serumSwapTask": {
    "serumPoolAddress": "8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6"
  }
}
```


# Oracle Aggregator

Switchboard can fetch data from a number of oracles. Here's how to use them.

## Pyth

The [Pyth Oracle Network](https://pyth.network/) publishes [price feeds](https://pyth.network/developers/price-feed-ids) that the Oracle Aggregator task can read through Hermes or from an on-chain Solana push-feed account. The two paths have different authentication and freshness behavior.

### Hermes-backed feeds with `pythAddress`

Use `pythAddress` for the Hermes-backed path. It accepts existing Pyth Solana price account addresses and Pyth price-feed IDs. For authenticated Hermes access, place an API-key placeholder in `pythConfigs.apiKey`:

```typescript
// SNX/USD with a 1.2% maximum confidence interval
{
  oracleTask: {
    pythAddress: "0x39d020f60982ed892abbcd4a06a276a9f9b7bfbce003204c110b6e488f502da3",
    pythConfigs: {
      apiKey: "${PYTH_API_KEY}",
      pythAllowedConfidenceInterval: 1.2,
      maxStaleSeconds: 15,
    },
  },
}
```

Provide the key on the same execution request:

```typescript
const response = await gateway.fetchSignaturesConsensus({
  // ...feed request fields
  variableOverrides: {
    PYTH_API_KEY: process.env.PYTH_API_KEY!,
  },
});
```

For new feed definitions, prefer an explicit `pythConfigs.apiKey` placeholder. It makes the task's credential dependency visible and allows different Pyth tasks in one request to name different keys. After placeholder expansion, a non-empty task-level `apiKey` takes precedence.

Existing `pythAddress` jobs do not need to be rewritten during the Pyth Core transition. When `pythConfigs.apiKey` is omitted or empty, the task accepts the reserved request override `PYTH_API_KEY` as a compatibility fallback. One fallback value covers every `pythAddress` task in that execution request that does not set its own `apiKey`; it does not apply to other requests or become an oracle-wide key. This preserves existing feed definitions while allowing the customer making the request to pay for its Hermes calls.

The compatibility fallback assumes one execution request belongs to one customer or security domain. Do not combine unrelated customers' jobs and credentials in a single request.

### On-chain push feeds with `pythPushFeedId`

Use `pythPushFeedId` for an upgraded Solana push feed. This path derives and reads the on-chain price account; it does not call Hermes and does not use a Hermes API key.

When authoring, storing, hashing, or updating a feed that uses this task, use `@switchboard-xyz/common@5.8.5` and `@switchboard-xyz/on-demand@3.10.6` or newer. The current Common serializer preserves the Pyth-push fields and explicitly set optional defaults in the canonical feed identity.

```typescript
{
  oracleTask: {
    pythPushFeedId: "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d",
    pythConfigs: {
      pushFeedShardId: 0,
      maxStaleSeconds: 75,
    },
  },
}
```

`pushFeedShardId` defaults to `0`. `maxStaleSeconds` defaults to `15` for `pythAddress` and `75` for `pythPushFeedId`. `pythConfigs.hermesUrl` can select a different Hermes endpoint for `pythAddress`. Use the nested `pythConfigs.pythAllowedConfidenceInterval` field for confidence limits; values are percentages, so `10` means 10%. The top-level `pythAllowedConfidenceInterval` field is retained only for compatibility.

See [Data Feed Variable Overrides](/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides) for the request trust model, [Jupiter API keys](/custom-feeds/advanced-feed-configuration/decentralized-exchanges#jupiter-exchange-aggregator) for its conventional placeholder behavior, and the [OracleTask reference](/custom-feeds/task-types#oracletask) for every field.

## Chainlink

The Chainlink Oracle Network provides [145](https://docs.chain.link/data-feeds/price-feeds/addresses?network=arbitrum\&page=1) data feeds at the time of writing on the [Arbitrum L2](https://arbitrum.io/) Mainnet. Check out the available feed addresses [here](https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum\&page=1).

```
// AAVE/USD Task with accepted confidence interval of 1.2%
{
    oracleTask: {
        chainlinkAddress: "0x3c6AbdA21358c15601A3175D8dd66D0c572cc904"
    },
}
```

## Switchboard V2

Switchboard V2 (Solana Push) feeds can be referenced within on-demand feeds. It's simple, all you need is an Aggregator public key, which you can find on the [V2 Explorer App](https://app.switchboard.xyz/solana/mainnet).

Here's an example of a Switchboard Oracle Task for the price of [BTC/USD](https://app.switchboard.xyz/solana/mainnet/feed/8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee):

```
// BTC/USD Task
{
    oracleTask: {
        switchboardAddress: "8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee",
    }
}
```


# Time-Weighted Average Prices

Calculate time-weighted average prices using SurgeTwapTask

Time-weighted average price (TWAP) is a pricing algorithm that calculates the average price of an asset over a specified time period. TWAP provides manipulation-resistant pricing by averaging prices over a configurable time window, weighted by the duration each price was observed.

## SurgeTwapTask

The `surgeTwapTask` calculates TWAP from Switchboard's streaming price data. The algorithm weights each observed price by how long it was in effect:

```
TWAP = Σ(price × duration) / Σ(duration)
```

### Basic Example

```typescript
{
    surgeTwapTask: {
        symbol: "BTC/USD",
        timeInterval: "ONE_HOUR"
    }
}
```

### Parameters

| Parameter      | Type         | Required | Default   | Description                                                            |
| -------------- | ------------ | -------- | --------- | ---------------------------------------------------------------------- |
| `symbol`       | string       | Yes      | -         | Trading pair in `*/USD` format (e.g., "BTC/USD", "ETH/USD", "SOL/USD") |
| `timeInterval` | TimeInterval | No       | ONE\_HOUR | Lookback window for TWAP calculation                                   |

### Time Intervals

| Interval          | Duration | Description                                       |
| ----------------- | -------- | ------------------------------------------------- |
| `FIVE_MINUTES`    | 5 min    | Shortest window, most responsive to price changes |
| `TEN_MINUTES`     | 10 min   |                                                   |
| `FIFTEEN_MINUTES` | 15 min   |                                                   |
| `THIRTY_MINUTES`  | 30 min   |                                                   |
| `ONE_HOUR`        | 1 hour   | Default, balances responsiveness and stability    |
| `TWO_HOURS`       | 2 hours  |                                                   |
| `SIX_HOURS`       | 6 hours  |                                                   |
| `TWELVE_HOURS`    | 12 hours | Most stable, least responsive                     |

### Constraints

* Only `*/USD` pairs are supported (e.g., "BTC/USD", "SOL/USD")
* The task will fail if a non-USD quote currency is provided

## Use Cases

**Lending Protocols** — Use TWAP for liquidation price checks to prevent flash loan attacks that temporarily manipulate spot prices.

**Perpetual Exchanges** — Funding rate calculations based on TWAP reduce the impact of short-term price spikes.

**Options and Derivatives** — Settlement prices based on TWAP reduce the impact of last-minute price manipulation.

**AMMs and DEXs** — TWAP oracles provide manipulation-resistant prices for concentrated liquidity ranges or limit orders.

## Examples

### 30-Minute TWAP for SOL/USD

```typescript
{
    surgeTwapTask: {
        symbol: "SOL/USD",
        timeInterval: "THIRTY_MINUTES"
    }
}
```

### TWAP with Price Bounds

Combine TWAP with bounding to ensure the calculated average stays within expected ranges:

```typescript
{
    surgeTwapTask: {
        symbol: "ETH/USD",
        timeInterval: "ONE_HOUR"
    }
},
{
    boundTask: {
        lowerBoundValue: "1000",
        upperBoundValue: "10000"
    }
}
```

### Multiple TWAP Sources

Use a median task to aggregate TWAPs from multiple assets:

```typescript
{
    medianTask: {
        tasks: [
            {
                surgeTwapTask: {
                    symbol: "BTC/USD",
                    timeInterval: "ONE_HOUR"
                }
            },
            {
                surgeTwapTask: {
                    symbol: "ETH/USD",
                    timeInterval: "ONE_HOUR"
                }
            }
        ]
    }
}
```

## Error Handling

| Error                       | Cause                            | Solution                                           |
| --------------------------- | -------------------------------- | -------------------------------------------------- |
| `symbol is empty`           | Missing symbol parameter         | Provide a valid symbol                             |
| `only supports */USD pairs` | Non-USD quote currency           | Use USD pairs only (e.g., "BTC/USD" not "BTC/EUR") |
| `no candle data available`  | No price data in lookback window | Verify the trading pair is supported               |

## How It Works

The TWAP calculation uses Switchboard's continuous price streaming infrastructure:

1. Price ticks are accumulated into 5-minute candles
2. Each candle stores the time-weighted sum and observed duration
3. When queried, candles spanning the requested interval are aggregated
4. The final TWAP is computed by dividing total weighted sum by total duration

The system includes gap protection: if price updates are interrupted for more than 5 seconds, that gap does not contribute to the TWAP calculation. This prevents stale prices from being over-weighted during network interruptions or exchange outages.

A variance check compares the TWAP against secondary price sources. If the coefficient of variation exceeds 0.4%, the task fails to protect against compromised exchange data.


# FAQ

Frequently Asked Questions

## Which sources should I pick for my feed?

We generally recommend sourcing data from where an asset is most actively traded / liquidity is greatest for price feeds. This can mean sourcing data from a large centralized exchange versus a small AMM.

Exchanges with low liquidity/volume can become manipulable by bad actors if small enough, so it's up to feed owners to configure their sources and find something that works within their risk parameters.

## How do I get a new Task Type added?

It's recommended to exhaust all options for fetching the data you desire using existing task types before requesting new task types, but if it requires a custom On-Chain SDK, please [reach out ](https://discord.gg/TJAv6ZYvPC)and it can get added to the roadmap.


# Task Types Reference

> This documentation is automatically generated from the [job\_schemas.proto](https://github.com/switchboard-xyz/sbv3/blob/main/protos/job_schemas.proto) source file.

An **OracleJob** is a collection of tasks that are chained together to arrive at a single numerical value. Tasks execute sequentially, with each task's output feeding into the next.

Some tasks do not consume the running input (such as HttpTask and WebsocketTask), effectively resetting the running result. Others transform the current value through mathematical operations or parsing.

## Data Fetching

### AnchorFetchTask

Load a parse an Anchor based solana account.

| Field             | Type   | Description                             |
| ----------------- | ------ | --------------------------------------- |
| `program_id`      | string | Owning program of the account to parse. |
| `account_address` | string | The account to parse.                   |

***

### HttpTask

The adapter will report the text body of a successful HTTP request to the specified url, or return an error if the response status code is greater than or equal to 400.

***Input***: None

***Returns***: String representation of the http response.

***Example***: Basic HttpTask

```json
{
  "httpTask": {
    "url": "https://mywebsite.org/path"
  }
}
```

***Example***: HttpTask example with headers

```json
{
  "httpTask": {
    "url": "https://mywebsite.org/path",
    "method": "METHOD_POST",
    "headers": [
      {
        "key": "MY_HEADER_KEY",
        "value": "MY_HEADER_VALUE"
      }
    ],
    "body": "{\"MY_BODY_KEY\":\"MY_BODY_VALUE\"}"
  }
}
```

| Field     | Type   | Description                                                 |
| --------- | ------ | ----------------------------------------------------------- |
| `url`     | string | A string containing the URL to direct this HTTP request to. |
| `method`  | Method | The type of HTTP request to make.                           |
| `headers` | Header | A list of headers to add to this HttpTask.                  |
| `body`    | string | A stringified body (if any) to add to this HttpTask.        |

**Header fields**

| Field   | Type   | Description                                                                   |
| ------- | ------ | ----------------------------------------------------------------------------- |
| `key`   | string | A header key such as `Authorization` or `Content-Type`                        |
| `value` | string | A value for the given header key like `Basic MYAUTHKEY` or `application/json` |

***

### SolanaAccountDataFetchTask

Fetch the account data in a stringified buffer format.

| Field    | Type   | Description                                          |
| -------- | ------ | ---------------------------------------------------- |
| `pubkey` | string | The on-chain account to fetch the account data from. |

***

### SolanaToken2022ExtensionTask

Apply Solana Token 2022 extension modifiers to a feed. ***Input***: Token address and extension type. ***Returns***: The value associated with the token2022 extension.

| Field  | Type   | Description                                             |
| ------ | ------ | ------------------------------------------------------- |
| `mint` | string | The base58 encoded publicKey of the token mint address. |

***

### SplTokenParseTask

Fetch the JSON representation of an SPL token mint.

| Field                   | Type   | Description                                                 |
| ----------------------- | ------ | ----------------------------------------------------------- |
| `token_account_address` | string | The publicKey of a token account to fetch the mintInfo for. |
| `mint_address`          | string | The publicKey of the token mint address.                    |

***

### WebsocketTask

Opens and maintains a websocket for light speed data retrieval.

***Input***: None

***Returns***: String representation of the websocket subscription message.

***Example***: Opens a coinbase websocket

```json
{
  "websocketTask": {
    "url": "wss://ws-feed.pro.coinbase.com",
    "subscription": "{\"type\":\"subscribe\",\"product_ids\":[\"BTC-USD\"],\"channels\":[\"ticker\",{\"name\":\"ticker\",\"product_ids\":[\"BTC-USD\"]}]}",
    "maxDataAgeSeconds": 15,
    "filter": "$[?(@.type == 'ticker' && @.product_id == 'BTC-USD')]"
  }
}
```

| Field                  | Type   | Description                                                                                        |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `url`                  | string | The websocket url.                                                                                 |
| `subscription`         | string | The websocket message to notify of a new subscription.                                             |
| `max_data_age_seconds` | int32  | Minimum amount of time required between when the horses are taking out.                            |
| `filter`               | string | Incoming message JSONPath filter. Example: "$\[?(@.channel == 'ticker' && @.market == 'BTC/USD')]" |

***

## Parsing

### BufferLayoutParseTask

Return the deserialized value from a stringified buffer.

| Field    | Type            | Description                                    |
| -------- | --------------- | ---------------------------------------------- |
| `offset` | uint32          | The buffer offset to start deserializing from. |
| `endian` | Endian          | The endianness of the stored value.            |
| `type`   | BufferParseType | The type of value to deserialize.              |

***

### CronParseTask

Return a timestamp from a crontab instruction.

***Input***: None

***Returns***: A timestamp

***Example***: Return the unix timestamp for the on-chain SYSCLOCK

```json
{
  "cronParseTask": {
    "cronPattern": "* * * * * *",
    "clockOffset": 0,
    "clock": "SYSCLOCK"
  }
}
```

***Example***: Return the unix timestamp for next friday at 5pm UTC

```json
{
  "cronParseTask": {
    "cronPattern": "0 17 * * 5",
    "clockOffset": 0,
    "clock": 0
  }
}
```

| Field          | Type      | Description                                          |
| -------------- | --------- | ---------------------------------------------------- |
| `cron_pattern` | string    | The cron pattern to parse.                           |
| `clock_offset` | int32     | The timestamp offset to calculate the next run.      |
| `clock`        | ClockType | Use the TaskRunner's clock or the on-chain SYSCLOCK. |

***

### JsonParseTask

The adapter walks the path specified and returns the value found at that result. If returning JSON data from the HttpGet or HttpPost adapters, you must use this adapter to parse the response.

***Input***: String representation of a JSON object.

***Returns***: A numerical result.

***Example***: Parses the price field from a JSON object

```json
{
  "jsonParse": {
    "path": "$.price"
  }
}
```

| Field                | Type              | Description                                                                                                                |
| -------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `path`               | string            | JSONPath formatted path to the element. <https://t.ly/uLtw> <https://www.npmjs.com/package/jsonpath-plus>                  |
| `aggregation_method` | AggregationMethod | The technique that will be used to aggregate the results if walking the specified path returns multiple numerical results. |

***

### RegexExtractTask

Find and extract text using regular expressions from the previous task's output.

***Input***: String output from previous task

***Returns***: The matched string based on the regex pattern and group number

***Example***: Extract the first number from a string

```json
{
  "regexExtractTask": {
    "pattern": "\\d+",
    "groupNumber": 0
  }
}
```

***Example***: Extract text between quotes

```json
{
  "regexExtractTask": {
    "pattern": "\"([^\"]+)\"",
    "groupNumber": 1
  }
}
```

***Example***: Extract the first JSON object from a stream

```json
{
  "regexExtractTask": {
    "pattern": "\\{[^}]+\\}"
  }
}
```

| Field          | Type   | Description                                                                                                                      |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `pattern`      | string | The regular expression pattern to match against the input string. Uses the fancy-regex Rust crate syntax.                        |
| `group_number` | int32  | The capture group number to extract (0 returns full match, 1+ returns respective capture group). Defaults to 0 if not specified. |

***

### StringMapTask

Map a string input to a predefined output value using exact string matching.

***Input***: String from previous task output or specified value

***Returns***: The mapped value as a string if a match is found, or the default value if no match is found.

***Example***: Map "yes" to "1", "no" to "2", "maybe" to "3" (case-insensitive)

```json
{
  "stringMapTask": {
    "mappings": [
      {
        "key": "yes",
        "value": "1"
      },
      {
        "key": "no",
        "value": "2"
      },
      {
        "key": "maybe",
        "value": "3"
      }
    ],
    "defaultValue": "0",
    "caseSensitive": false
  }
}
```

***Example***: Map HTTP response status with case-sensitive matching

```json
{
  "tasks": [
    {
      "httpTask": {
        "url": "https://api.example.com/status"
      }
    },
    {
      "regexExtractTask": {
        "pattern": "status\":\\s*\"([^\"]+)\""
      }
    },
    {
      "stringMapTask": {
        "mappings": [
          {
            "key": "active",
            "value": "100"
          },
          {
            "key": "inactive",
            "value": "0"
          },
          {
            "key": "pending",
            "value": "50"
          }
        ],
        "defaultValue": "-1",
        "caseSensitive": true
      }
    }
  ]
}
```

| Field            | Type    | Description                                                                                                        |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `mappings`       | Mapping | The list of key-value mappings.                                                                                    |
| `default_value`  | string  | Optional default value to return if no mapping matches. If not provided and no match is found, the task will fail. |
| `case_sensitive` | bool    | Whether the string matching should be case-sensitive. Defaults to true.                                            |
| `input`          | string  | Optional input value to map. If not provided, will use the previous task output.                                   |

**Mapping fields**

| Field   | Type   | Description                             |
| ------- | ------ | --------------------------------------- |
| `key`   | string | The string key to match against.        |
| `value` | string | The value to return if the key matches. |

***

## Mathematical Operations

### AddTask

This task will add a numerical input by a scalar value from a job of subtasks, an aggregator, or a big.

***Input***: The current running numerical result output from a scalar value, an aggregator, a job of subtasks or a big.

***Returns***: A numerical result.

***Example***: Returns the numerical result by adding by a job of subtasks.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "addTask": {
        "job": {
          "tasks": [
            {
              "valueTask": {
                "value": 10
              }
            }
          ]
        }
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by an aggregator.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "addTask": {
        "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by a big.

```json
{
  "tasks": [
    {
      "cacheTask": {
        "cacheItems": [
          {
            "variableName": "TEN",
            "job": {
              "tasks": [
                {
                  "valueTask": {
                    "value": 10
                  }
                }
              ]
            }
          }
        ]
      }
    },
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "addTask": {
        "big": "${TEN}"
      }
    }
  ]
}
```

| Field               | Type      | Description                                                                      |
| ------------------- | --------- | -------------------------------------------------------------------------------- |
| `scalar`            | double    | Specifies a scalar to add by.                                                    |
| `aggregator_pubkey` | string    | Specifies an aggregator to add by.                                               |
| `job`               | OracleJob | A job whose result is computed before adding our numerical input by that result. |
| `big`               | string    | A stringified big.js. `Accepts variable expansion syntax.`                       |

***

### BoundTask

Bound the running result to an upper/lower bound. This is typically the last task in an OracleJob.

***Input***: The current running numerical result.

***Returns***: The running result bounded to an upper or lower bound if it exceeds a given threshold.

***Example***: Bound the running result to a value between 0.90 and 1.10

```json
{
  "boundTask": {
    "lowerBoundValue": "0.90",
    "onExceedsLowerBoundValue": "0.90",
    "upperBoundValue": "1.10",
    "onExceedsUpperBoundValue": "1.10"
  }
}
```

| Field                          | Type      | Description                                                                      |
| ------------------------------ | --------- | -------------------------------------------------------------------------------- |
| `lower_bound`                  | OracleJob | The OracleJob to execute for the lower bound value.                              |
| `lower_bound_value`            | string    | The value to use for the lower bound. Can be set to a `${CACHE_KEY}`.            |
| `upper_bound`                  | OracleJob | The OracleJob to execute for the upper bound value.                              |
| `upper_bound_value`            | string    | The value to use for the upper bound. Can be set to a `${CACHE_KEY}`.            |
| `on_exceeds_upper_bound`       | OracleJob | The OracleJob to execute if the upper bound is exceeded.                         |
| `on_exceeds_upper_bound_value` | string    | The value to use if the upper bound is exceeded. Can be set to a `${CACHE_KEY}`. |
| `on_exceeds_lower_bound`       | OracleJob | The OracleJob to execute if the lower bound is exceeded.                         |
| `on_exceeds_lower_bound_value` | string    | The value to use if the lower bound is exceeded. Can be set to a `${CACHE_KEY}`. |

***

### DivideTask

This task will divide a numerical input by a scalar value from a job of subtasks, an aggregator, or a big.

***Input***: The current running numerical result output from a scalar value, an aggregator, a job of subtasks or a big.

***Returns***: A numerical result.

***Example***: Returns the numerical result by dividing by a job of subtasks.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "divideTask": {
        "job": {
          "tasks": [
            {
              "valueTask": {
                "value": 10
              }
            }
          ]
        }
      }
    }
  ]
}
```

***Example***: Returns the numerical result by dividing by an aggregator.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "divideTask": {
        "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
      }
    }
  ]
}
```

***Example***: Returns the numerical result by dividing by a big.

```json
{
  "tasks": [
    {
      "cacheTask": {
        "cacheItems": [
          {
            "variableName": "TEN",
            "job": {
              "tasks": [
                {
                  "valueTask": {
                    "value": 10
                  }
                }
              ]
            }
          }
        ]
      }
    },
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "divideTask": {
        "big": "${TEN}"
      }
    }
  ]
}
```

| Field               | Type      | Description                                                                        |
| ------------------- | --------- | ---------------------------------------------------------------------------------- |
| `scalar`            | double    | Specifies a basic scalar denominator to divide by.                                 |
| `aggregator_pubkey` | string    | Specifies another aggregator resut to divide by.                                   |
| `job`               | OracleJob | A job whose result is computed before dividing our numerical input by that result. |
| `big`               | string    | A stringified big.js. `Accepts variable expansion syntax.`                         |

***

### MaxTask

Returns the maximum value of all the results returned by the provided subtasks and subjobs. Nested tasks or jobs must return a Number.

***Input***: None

***Returns***: A numerical result.

***Example***: Returns the maximum numerical result from 3 tasks.

```json
{
  "maxTask": {
    "tasks": [
      {
        "valueTask": {
          "value": 10
        }
      },
      {
        "valueTask": {
          "value": 20
        }
      },
      {
        "valueTask": {
          "value": 30
        }
      }
    ]
  }
}
```

***Example***: Returns the maximum numerical result from 3 jobs.

```json
{
  "maxTask": {
    "jobs": [
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.com/api/v3/ticker/price?symbol=SOLUSDT"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.us/api/v3/ticker/price?symbol=SOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://api-pub.bitfinex.com/v2/tickers?symbols=tSOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$[0][7]"
            }
          }
        ]
      }
    ]
  }
}
```

| Field   | Type      | Description                                                        |
| ------- | --------- | ------------------------------------------------------------------ |
| `tasks` | Task      | A list of subtasks to process and produce a list of result values. |
| `jobs`  | OracleJob | A list of subjobs to process and produce a list of result values.  |

***

### MeanTask

Returns the mean (average) of all the results returned by the provided subtasks and subjobs. Nested tasks or jobs must return a Number.

***Input***: None

***Returns***: A numerical result.

***Example***: Returns the mean numerical result of 3 tasks.

```json
{
  "meanTask": {
    "tasks": [
      {
        "valueTask": {
          "value": 10
        }
      },
      {
        "valueTask": {
          "value": 20
        }
      },
      {
        "valueTask": {
          "value": 30
        }
      }
    ]
  }
}
```

***Example***: Returns the mean numerical result of 3 jobs.

```json
{
  "meanTask": {
    "jobs": [
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.com/api/v3/ticker/price?symbol=SOLUSDT"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.us/api/v3/ticker/price?symbol=SOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://api-pub.bitfinex.com/v2/tickers?symbols=tSOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$[0][7]"
            }
          }
        ]
      }
    ]
  }
}
```

| Field   | Type      | Description                                                        |
| ------- | --------- | ------------------------------------------------------------------ |
| `tasks` | Task      | A list of subtasks to process and produce a list of result values. |
| `jobs`  | OracleJob | A list of subjobs to process and produce a list of result values.  |

***

### MedianTask

Returns the median (middle) of all the results returned by the provided subtasks and subjobs. Nested tasks must return a Number.

***Input***: None

***Returns***: A numerical result.

***Example***: Returns the median numerical result of 3 tasks.

```json
{
  "medianTask": {
    "tasks": [
      {
        "valueTask": {
          "value": 10
        }
      },
      {
        "valueTask": {
          "value": 20
        }
      },
      {
        "valueTask": {
          "value": 30
        }
      }
    ]
  }
}
```

***Example***: Returns the median numerical result of 3 jobs.

```json
{
  "medianTask": {
    "jobs": [
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.com/api/v3/ticker/price?symbol=SOLUSDT"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.us/api/v3/ticker/price?symbol=SOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://api-pub.bitfinex.com/v2/tickers?symbols=tSOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$[0][7]"
            }
          }
        ]
      }
    ]
  }
}
```

| Field                     | Type      | Description                                                                                         |
| ------------------------- | --------- | --------------------------------------------------------------------------------------------------- |
| `tasks`                   | Task      | A list of subtasks to process and produce a list of result values.                                  |
| `jobs`                    | OracleJob | A list of subjobs to process and produce a list of result values.                                   |
| `min_successful_required` | int32     | The minimum number of values before a successful median can be yielded.                             |
| `max_range_percent`       | string    | The maximum range between the minimum and maximum values before a successful median can be yielded. |

***

### MinTask

Returns the minimum value of all the results returned by the provided subtasks and subjobs. Nested tasks or jobs must return a Number.

***Input***: None

***Returns***: A numerical result.

***Example***: Returns the minimum numerical result from 3 tasks.

```json
{
  "minTask": {
    "tasks": [
      {
        "valueTask": {
          "value": 10
        }
      },
      {
        "valueTask": {
          "value": 20
        }
      },
      {
        "valueTask": {
          "value": 30
        }
      }
    ]
  }
}
```

***Example***: Returns the minimum numerical result from 3 jobs.

```json
{
  "minTask": {
    "jobs": [
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.com/api/v3/ticker/price?symbol=SOLUSDT"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://www.binance.us/api/v3/ticker/price?symbol=SOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$.price"
            }
          }
        ]
      },
      {
        "tasks": [
          {
            "httpTask": {
              "url": "https://api-pub.bitfinex.com/v2/tickers?symbols=tSOLUSD"
            }
          },
          {
            "jsonParseTask": {
              "path": "$[0][7]"
            }
          }
        ]
      }
    ]
  }
}
```

| Field   | Type      | Description                                                        |
| ------- | --------- | ------------------------------------------------------------------ |
| `tasks` | Task      | A list of subtasks to process and produce a list of result values. |
| `jobs`  | OracleJob | A list of subjobs to process and produce a list of result values.  |

***

### MultiplyTask

This task will multiply a numerical input by a scalar value from a job of subtasks, an aggregator, or a big.

***Input***: The current running numerical result output from a scalar value, an aggregator, a job of subtasks or a big.

***Returns***: A numerical result.

***Example***: Returns the numerical result by multiplying by a job of subtasks.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "multiplyTask": {
        "job": {
          "tasks": [
            {
              "valueTask": {
                "value": 10
              }
            }
          ]
        }
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by an aggregator.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "multiplyTask": {
        "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by a big.

```json
{
  "tasks": [
    {
      "cacheTask": {
        "cacheItems": [
          {
            "variableName": "TEN",
            "job": {
              "tasks": [
                {
                  "valueTask": {
                    "value": 10
                  }
                }
              ]
            }
          }
        ]
      }
    },
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "multiplyTask": {
        "big": "${TEN}"
      }
    }
  ]
}
```

| Field               | Type      | Description                                                                           |
| ------------------- | --------- | ------------------------------------------------------------------------------------- |
| `scalar`            | double    | Specifies a scalar to multiply by.                                                    |
| `aggregator_pubkey` | string    | Specifies an aggregator to multiply by.                                               |
| `job`               | OracleJob | A job whose result is computed before multiplying our numerical input by that result. |
| `big`               | string    | A stringified big.js. `Accepts variable expansion syntax.`                            |

***

### PowTask

Round the current running result to an exponential power.

***Input***: The current running numerical result.

***Returns***: The input raised to an exponential power.

***Example***: Raise 2 to the power of 3, 2^3

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 2
      }
    },
    {
      "powTask": {
        "scalar": 3
      }
    }
  ]
}
```

| Field               | Type   | Description                                                      |
| ------------------- | ------ | ---------------------------------------------------------------- |
| `scalar`            | double | Take the working value to the exponent of value.                 |
| `aggregator_pubkey` | string | Take the working value to the exponent of the aggregators value. |
| `big`               | string | A stringified big.js. `Accepts variable expansion syntax.`       |

***

### RoundTask

Round the current running result to a set number of decimal places.

***Input***: The current running numerical result.

***Returns***: The running result rounded to a set number of decimal places.

***Example***: Round down the running resul to 8 decimal places

```json
{
  "roundTask": {
    "method": "METHOD_ROUND_DOWN",
    "decimals": 8
  }
}
```

| Field      | Type   | Description                         |
| ---------- | ------ | ----------------------------------- |
| `method`   | Method | The rounding method to use.         |
| `decimals` | int32  | The number of decimals to round to. |

***

### SubtractTask

This task will subtract a numerical input by a scalar value from a job of subtasks, an aggregator, or a big.

***Input***: The current running numerical result output from a scalar value, an aggregator, a job of subtasks or a big.

***Returns***: A numerical result.

***Example***: Returns the numerical result by subtracting by a job of subtasks.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "subtractTask": {
        "job": {
          "tasks": [
            {
              "valueTask": {
                "value": 10
              }
            }
          ]
        }
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by an aggregator.

```json
{
  "tasks": [
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "subtractTask": {
        "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
      }
    }
  ]
}
```

***Example***: Returns the numerical result by multiplying by a big.

```json
{
  "tasks": [
    {
      "cacheTask": {
        "cacheItems": [
          {
            "variableName": "TEN",
            "job": {
              "tasks": [
                {
                  "valueTask": {
                    "value": 10
                  }
                }
              ]
            }
          }
        ]
      }
    },
    {
      "valueTask": {
        "value": 100
      }
    },
    {
      "subtractTask": {
        "big": "${TEN}"
      }
    }
  ]
}
```

| Field               | Type      | Description                                                                           |
| ------------------- | --------- | ------------------------------------------------------------------------------------- |
| `scalar`            | double    | Specifies a scalar to subtract by.                                                    |
| `aggregator_pubkey` | string    | Specifies an aggregator to subtract by.                                               |
| `job`               | OracleJob | A job whose result is computed before subtracting our numerical input by that result. |
| `big`               | string    | A stringified big.js. `Accepts variable expansion syntax.`                            |

***

## DeFi & DEX

### CurveFinanceTask

Fetch pricing information from Curve Finance pools.

***Input***: None

***Returns***: The current price/exchange rate from the specified Curve pool.

***Example***: Fetch the price from a Curve pool on Ethereum

```json
{
  "curveFinanceTask": {
    "chain": "CHAIN_ETHEREUM",
    "poolAddress": "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7",
    "outDecimals": 18
  }
}
```

***Example***: Fetch the price using a custom RPC provider

```json
{
  "curveFinanceTask": {
    "chain": "CHAIN_ETHEREUM",
    "provider": "https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY",
    "poolAddress": "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7",
    "outDecimals": 18
  }
}
```

| Field          | Type   | Description                                                                                                                         |
| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `chain`        | Chain  | Required. Specifies which blockchain to use when reading information from Curve Finance.                                            |
| `provider`     | string | Optional. The RPC endpoint to use for blockchain requests. If not specified, a default RPC will be used which may have rate limits. |
| `pool_address` | string | The on-chain address of the Curve Finance pool to fetch pricing data from.                                                          |
| `out_decimals` | uint32 | The number of decimal places to include in the returned price value.                                                                |

***

### HyloTask

Hylo Protocol task for converting 1 hyUSD to jitoSOL. hyUSD is a stablecoin with NAV pegged to $1.00 USD. Converts exactly 1 hyUSD token to jitoSOL.

| Field   | Type  | Description                                        |
| ------- | ----- | -------------------------------------------------- |
| `token` | Token | The Hylo token to convert from (defaults to hyUSD) |

***

### JupiterSwapTask

Fetch the simulated price for a swap on JupiterSwap.

***Input***: None

***Returns***: The swap price on Jupiter for a given input and output token mint address.

***Example***: Fetch the JupiterSwap price for exchanging 1 SOL into USDC.

```json
{
  "jupiterSwapTask": {
    "inTokenAddress": "So11111111111111111111111111111111111111112",
    "outTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
  }
}
```

***Example***: Fetch the JupiterSwap price for exchanging 1000 SOL into USDC.

```json
{
  "jupiterSwapTask": {
    "inTokenAddress": "So11111111111111111111111111111111111111112",
    "outTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "baseAmount": "1000"
  }
}
```

***Example***: Supply a request-scoped Jupiter API key without storing it in the job.

```json
{
  "jupiterSwapTask": {
    "inTokenAddress": "So11111111111111111111111111111111111111112",
    "outTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "baseAmountString": "1",
    "apiKey": "${JUPITER_API_KEY}"
  }
}
```

Pass the matching non-empty value when requesting the feed:

```typescript
const response = await gateway.fetchSignaturesConsensus({
  // ...
  variableOverrides: {
    JUPITER_API_KEY: process.env.JUPITER_API_KEY!,
  },
});
```

| Field                 | Type       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `in_token_address`    | string     | The input token address.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `out_token_address`   | string     | The output token address.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `allow_list`          | FilterList | A list of AMM markets to allow.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `deny_list`           | FilterList | A list of AMM markets to deny.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `base_amount`         | double     | The amount of `in_token_address` tokens to swap.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `quote_amount`        | double     | The amount of `out_token_address` tokens to swap.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `base_amount_string`  | string     | The amount of `in_token_address` tokens to swap.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `quote_amount_string` | string     | The amount of `out_token_address` tokens to swap.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `slippage`            | double     | The allowable slippage on the swap in decimal form (e.g. 0.5 is 0.5% slippage)                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `api_key`             | string     | Optional Jupiter API key. For a request-scoped key, set this field to a variable placeholder such as `${JUPITER_API_KEY}` and provide the matching non-empty value through `variableOverrides` when requesting the feed. An override is only applied when this field contains a matching placeholder. If the placeholder is unresolved, the task fails before contacting Jupiter. If this field is omitted or empty, the oracle's configured Jupiter key is used when available. Do not hardcode credentials in an oracle job. |

**FilterList fields**

| Field    | Type   | Description                                                            |
| -------- | ------ | ---------------------------------------------------------------------- |
| `labels` | string | A list of Jupiter AMM labels to allow or deny (e.g. 'Raydium', 'Orca') |

***

### KuruTask

Fetch a swap quote from Kuru API for best path routing on EVM chains.

***Input***: None

***Returns***: The expected output amount for swapping tokens via Kuru.

***Example***: Fetch a quote for swapping 1 WETH to USDC.

```json
{
  "kuruTask": {
    "tokenIn": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": "1000000000000000000"
  }
}
```

***Example***: Fetch a quote with custom slippage tolerance.

```json
{
  "kuruTask": {
    "tokenIn": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": "1000000000000000000",
    "autoSlippage": false,
    "slippageTolerance": 100
  }
}
```

| Field                | Type   | Description                                                                                            |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------ |
| `user_address`       | string | The Ethereum address of the user making the swap (default: zero address).                              |
| `token_in`           | string | The input token contract address (EVM address format).                                                 |
| `token_out`          | string | The output token contract address (EVM address format).                                                |
| `amount`             | string | The amount to swap in wei (e.g., "1000000000000000000" for 1 token with 18 decimals).                  |
| `auto_slippage`      | bool   | Whether to automatically calculate slippage tolerance (default: true).                                 |
| `slippage_tolerance` | uint32 | Slippage tolerance in basis points (1-10000, e.g., 50 = 0.5%). Only used when auto\_slippage is false. |
| `referrer_address`   | string | Optional referrer address for fee sharing.                                                             |
| `referrer_fee_bps`   | uint32 | Optional referrer fee in basis points (0-10000).                                                       |
| `input_decimals`     | uint32 | Number of decimals for the input token (default: 18).                                                  |
| `output_decimals`    | uint32 | Number of decimals for the output token (default: 18).                                                 |
| `api_key`            | string | Optional API key for authentication (X-API-Key header).                                                |
| `bearer_token`       | string | Optional bearer token for authentication (Authorization header).                                       |
| `api_endpoint`       | string | Optional API endpoint override (defaults to ws.staging.kuru.io/api/quote).                             |

***

### LpExchangeRateTask

Fetch the current swap price for a given liquidity pool

***Input***: None

***Returns***: The swap price for a given AMM pool.

***Example***: Fetch the exchange rate from the Orca SOL/USDC pool

```json
{
  "lpExchangeRateTask": {
    "orcaPoolAddress": "APDFRM3HMr8CAGXwKHiu2f5ePSpaiEJhaURwhsRrUUt9"
  }
}
```

***Example***: Fetch the exchange rate from the Raydium SOL/USDC pool

```json
{
  "lpExchangeRateTask": {
    "raydiumPoolAddress": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2"
  }
}
```

| Field                          | Type   | Description                                                                                                                 |
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------- |
| `in_token_address`             | string | Used alongside mercurial\_pool\_address to specify the input token for a swap.                                              |
| `out_token_address`            | string | Used alongside mercurial\_pool\_address to specify the output token for a swap.                                             |
| `mercurial_pool_address`       | string | Mercurial finance pool address. A full list can be found here: <https://github.com/mercurial-finance/stable-swap-n-pool-js> |
| `saber_pool_address`           | string | Saber pool address. A full list can be found here: <https://github.com/saber-hq/saber-registry-dist>                        |
| `orca_pool_token_mint_address` | string | **@deprecated** Use orcaPoolAddress                                                                                         |
| `raydium_pool_address`         | string | The Raydium liquidity pool ammId. A full list can be found here: <https://raydium.io/pools>                                 |
| `orca_pool_address`            | string | Pool address for an Orca LP pool or whirlpool. A full list of Orca LP pools can be found here: <https://www.orca.so/pools>  |
| `port_reserve_address`         | string | The Port reserve pubkey. A full list can be found here: <https://api-v1.port.finance/reserves>                              |
| `defituna_pool_address`        | string | DefiTuna Fusion AMM pool address. Program ID: tuna4uSQZncNeeiAMKbstuxA9CUkHH6HmC64wgmnogD                                   |

***

### LpTokenPriceTask

Fetch LP token price info from a number of supported exchanges.

See our blog post on [Fair LP Token Oracles](https://github.com/switchboard-xyz/gitbook-on-demand/tree/main/blog/2022/01/20/Fair-LP-Token-Oracles/README.md)

**NOTE**: This is not the swap price but the price of the underlying LP token.

***Input***: None

***Returns***: The price of an LP token for a given AMM pool.

***Example***: Fetch the Orca LP token price of the SOL/USDC pool

```json
{
  "lpTokenPriceTask": {
    "orcaPoolAddress": "APDFRM3HMr8CAGXwKHiu2f5ePSpaiEJhaURwhsRrUUt9"
  }
}
```

***Example***: Fetch the fair price Orca LP token price of the SOL/USDC pool

```json
{
  "lpTokenPriceTask": {
    "orcaPoolAddress": "APDFRM3HMr8CAGXwKHiu2f5ePSpaiEJhaURwhsRrUUt9",
    "useFairPrice": true,
    "priceFeedAddresses": [
      "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR",
      "BjUgj6YCnFBZ49wF54ddBVA9qu8TeqkFtkbqmZcee8uW"
    ]
  }
}
```

***Example***: Fetch the fair price Raydium LP token price of the SOL/USDC pool

```json
{
  "lpTokenPriceTask": {
    "raydiumPoolAddress": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2",
    "useFairPrice": true,
    "priceFeedAddresses": [
      "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR",
      "BjUgj6YCnFBZ49wF54ddBVA9qu8TeqkFtkbqmZcee8uW"
    ]
  }
}
```

| Field                    | Type      | Description                                                                                                                                                                                                                                      |
| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mercurial_pool_address` | string    | Mercurial finance pool address. A full list can be found here: <https://github.com/mercurial-finance/stable-swap-n-pool-js>                                                                                                                      |
| `saber_pool_address`     | string    | Saber pool address. A full list can be found here: <https://github.com/saber-hq/saber-registry-dist>                                                                                                                                             |
| `orca_pool_address`      | string    | Orca pool address. A full list can be found here: <https://www.orca.so/pools>                                                                                                                                                                    |
| `raydium_pool_address`   | string    | The Raydium liquidity pool ammId. A full list can be found here: <https://raydium.io/pools>                                                                                                                                                      |
| `price_feed_addresses`   | string    | A list of Switchboard aggregator accounts used to calculate the fair LP price. This ensures the price is based on the previous round to mitigate flash loan price manipulation.                                                                  |
| `price_feed_jobs`        | OracleJob | A list of OracleJobs to execute in order to yield the price feed jobs to use for the fair price formula.                                                                                                                                         |
| `use_fair_price`         | bool      | If enabled and price\_feed\_addresses provided, the oracle will calculate the fair LP price based on the liquidity pool reserves. See our blog post for more information: <https://switchboardxyz.medium.com/fair-lp-token-oracles-94a457c50239> |

***

### MaceTask

Fetch a swap quote from MACE (M.A.C.E.) aggregator for best path routing on EVM chains. MACE is a Multi-DEX EVM trade solver that uses simulated transactions for optimal routing.

***Input***: None

***Returns***: The expected output amount for swapping tokens via MACE aggregator.

***Example***: Fetch a quote for swapping 1 WETH to USDC on Ethereum.

```json
{
  "maceTask": {
    "tokenIn": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": "1000000000000000000"
  }
}
```

***Example***: Fetch a quote with custom slippage and gas price.

```json
{
  "maceTask": {
    "tokenIn": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": "1000000000000000000",
    "slippageToleranceBps": 100,
    "gasPriceWei": "50000000000"
  }
}
```

| Field                    | Type   | Description                                                                                                                             |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `from_address`           | string | The Ethereum address of the user making the swap (default: zero address).                                                               |
| `token_in`               | string | The input token identifier (EVM address, "native", or ERC1155 format). Examples: "native", "0x760AfE86e5de5fa0Ee542fc7B7B713e1c5425701" |
| `token_out`              | string | The output token identifier (EVM address, "native", or ERC1155 format).                                                                 |
| `amount`                 | string | The amount to swap in wei (e.g., "1000000000000000000" for 1 token with 18 decimals).                                                   |
| `slippage_tolerance_bps` | uint32 | Slippage tolerance in basis points (1-10000, e.g., 100 = 1%). Default: 10000 (100%).                                                    |
| `gas_price_wei`          | string | Gas price to simulate with in wei. Influences route selection - higher values weight gas usage more. Default: "1000000000" (1 Gwei).    |
| `max_routes`             | uint32 | Maximum number of routes to return (default: 1). Routes range from most valuable to most stable.                                        |
| `input_decimals`         | uint32 | Number of decimals for the input token (default: 18).                                                                                   |
| `output_decimals`        | uint32 | Number of decimals for the output token (default: 18).                                                                                  |
| `api_key`                | string | Optional API key for authentication.                                                                                                    |
| `api_endpoint`           | string | Optional API endpoint override (defaults to testnet.api.beta.mace.ag for testnet).                                                      |

***

### MeteoraSwapTask

Grab the swap price from a Meteora pool.

| Field  | Type   | Description              |
| ------ | ------ | ------------------------ |
| `pool` | string | The address of the pool. |
| `type` | Type   | The pool type.           |

***

### PancakeswapExchangeRateTask

Fetch the swap price from PancakeSwap.

| Field               | Type   | Description                                     |
| ------------------- | ------ | ----------------------------------------------- |
| `in_token_address`  | string | The input token address.                        |
| `out_token_address` | string | The output token address.                       |
| `in_token_amount`   | double | The amount of tokens to swap.                   |
| `slippage`          | double | The allowable slippage in percent for the swap. |
| `provider`          | string | The RPC provider to use for the swap.           |

***

### PumpAmmLpTokenPriceTask

Derive the fair LP token price for a given Pump AMM liquidity pool. ***Input***: Pool address, X token price job, Y token price job. ***Returns***: The fair LP token price for the given Pump AMM liquidity pool. ***Example***: Derive the fair LP token price for a given Pump AMM liquidity pool.

````json
   {
     "pumpAmmLpTokenPriceTask": {
       "pool_address": "Gf7sXMoP8iRw4iiXmJ1nq4vxcRycbGXy5RL8a8LnTd3v", // USDC/SOL
       "x_price_job": {
         "oracleTask": {
           "switchboardAddress": "..." // USDC/USD
         }
       },
       "y_price_job": {
         "oracleTask": {
           "switchboardAddress": "..." // SOL/USD
         }
       }
     }
   }

| Field | Type | Description |
|-------|------|-------------|
| `pool_address` | string | Required. The address of the liquidity pool in the Pump AMM. |
| `x_price_job` | OracleJob | Required. The job to execute to fetch the price of the pool x token |
| `y_price_job` | OracleJob | Required. The job to execute |

---

### PumpAmmTask

Execute a swap task in the Pump AMM based on the given parameters.

  _**Input**_: Pool address, input token amount, max allowed slippage, and swap direction.

  _**Returns**_: Executes the swap operation in the Pump AMM with the given parameters.

  _**Example**_: Swap 10 tokens from X to Y with a maximum slippage of 0.5%

  ```json
  {
    "pumpAmmTask": {
      "pool_address": "Gf7sXMoP8iRw4iiXmJ1nq4vxcRycbGXy5RL8a8LnTd3v",
      "in_amount": "10",
      "max_slippage": 0.5,
      "is_x_for_y": true
    }
  }

| Field | Type | Description |
|-------|------|-------------|
| `pool_address` | string | Required. The address of the liquidity pool in the Pump AMM. |
| `in_amount` | double | Optional. The input token amount for the swap. - This value should in full units of the input token. - Default value: `1` (Swap 1 full token). |
| `max_slippage` | double | Optional. The maximum allowed slippage for the swap, expressed as a percentage. - Example: `0.5` represents 0.5% slippage tolerance. - Default value: `3` (3% slippage tolerance). |
| `is_x_for_y` | bool | Optional. Indicates the swap direction: - `true`: Swapping token X for token Y. - `false`: Swapping token Y for token X. - Default value: `true`. |

---

### SerumSwapTask

Fetch the latest swap price on Serum's orderbook

| Field | Type | Description |
|-------|------|-------------|
| `serum_pool_address` | string | The serum pool to fetch swap price for |

---

### SushiswapExchangeRateTask

Fetch the swap price from SushiSwap.

| Field | Type | Description |
|-------|------|-------------|
| `in_token_address` | string | The input token address. |
| `out_token_address` | string | The output token address. |
| `in_token_amount` | double | The amount of tokens to swap. |
| `slippage` | double | The allowable slippage in percent for the swap. |
| `provider` | string | The RPC provider to use for the swap. |

---

### TitanTask

Fetch the simulated swap price from Titan API.

_**Input**_: None

_**Returns**_: The swap price on Titan for a given input and output token mint address.

_**Example**_: Fetch the Titan price for exchanging 1 SOL into USDC.

```json
{
  "titanTask": {
    "inTokenAddress": "So11111111111111111111111111111111111111112",
    "outTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
  }
}
````

***Example***: Fetch the Titan price for exchanging 1000 SOL into USDC with slippage.

```json
{
  "titanTask": {
    "inTokenAddress": "So11111111111111111111111111111111111111112",
    "outTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "1000",
    "slippageBps": 50
  }
}
```

| Field                | Type       | Description                                                                              |
| -------------------- | ---------- | ---------------------------------------------------------------------------------------- |
| `in_token_address`   | string     | The input token mint address (base58 encoded).                                           |
| `out_token_address`  | string     | The output token mint address (base58 encoded).                                          |
| `amount`             | string     | The amount of tokens to swap (raw atoms, not scaled by decimals).                        |
| `user_public_key`    | string     | Optional user public key for transaction generation (base58 encoded).                    |
| `swap_mode`          | SwapMode   | Whether the amount is in terms of input or output token. Defaults to ExactIn.            |
| `slippage_bps`       | uint32     | Allowed slippage in basis points (e.g., 50 = 0.5%).                                      |
| `dexes`              | FilterList | If set, constrain quotes to the given set of DEXes.                                      |
| `exclude_dexes`      | FilterList | If set, exclude the following DEXes when determining routes.                             |
| `only_direct_routes` | bool       | If set to true, only direct routes between the input and output mint will be considered. |
| `providers`          | string     | If set, limit quotes to the given set of provider IDs.                                   |
| `access_token`       | string     | Optional API access token for authenticated requests                                     |
| `api_endpoint`       | string     | Optional API endpoint override (defaults to partners.api.titan.exchange)                 |

**FilterList fields**

| Field    | Type   | Description                                                     |
| -------- | ------ | --------------------------------------------------------------- |
| `labels` | string | A list of DEX labels to allow or deny (e.g., 'Raydium', 'Orca') |

***

### UniswapExchangeRateTask

Fetch the swap price from UniSwap.

| Field               | Type    | Description                                     |
| ------------------- | ------- | ----------------------------------------------- |
| `in_token_address`  | string  | The input token address.                        |
| `out_token_address` | string  | The output token address.                       |
| `in_token_amount`   | double  | The amount of tokens to swap.                   |
| `slippage`          | double  | The allowable slippage in percent for the swap. |
| `provider`          | string  | The RPC provider to use for the swap.           |
| `version`           | Version | The version of the Uniswap exchange to use.     |

***

## LST & Staking

### LstHistoricalYieldTask

Query historical yield data for a given Liquid Staking Token (LST) and perform a statistical reduction operation over the dataset.

***Input***: LST mint address, reduction operation type, and number of epochs to sample.

***Returns***: The computed yield value based on the specified operation.

***Example***: Compute the median APY for an LST over the last 100 epochs

````json
{
  "lstHistoricalYieldTask": {
    "lstMint": "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",
    "operation": "OPERATION_MEDIAN",
    "epochs": 100
  }
}

| Field | Type | Description |
|-------|------|-------------|
| `lst_mint` | string | Required. The LST mint address for which historical yield data is queried. |
| `operation` | Operation | Required. The statistical operation to apply to the historical yield dataset. |
| `epochs` | int32 | Optional. The number of epochs to sample for the computation. - If `epochs = 0`, all available historical data will be used. - If `epochs > 0`, only the last `epochs` entries will be included. |

---

### MarinadeStateTask

Fetch the current mSOL/SOL exchange rate from the Marinade program state.

_**Input**_: None

_**Returns**_: The current value of 1 mSOL in SOL.

_**Example**_: Fetch the mSOL/SOL exchange rate

```json
{
  "marinadeStateTask": {}
}
````

***

### SanctumLstPriceTask

Grab the price of an Sanctum LST relative to SOL.

| Field              | Type   | Description                                                                          |
| ------------------ | ------ | ------------------------------------------------------------------------------------ |
| `lst_mint`         | string | The address of the LST mint. e.g. INF - 5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm |
| `skip_epoch_check` | bool   | Allow the check to see if the LST was cranked for the current epoch to be skipped.   |

***

### SolayerSusdTask

Fetch the current price of Solayer's sUSD stablecoin by reading its interest-bearing mint configuration.

***Input***: None

***Returns***: The current price of sUSD relative to USD (1.0 = $1.00)

***Example***: Fetch the current sUSD price

```json
{
  "solayerSusdTask": {}
}
```

***

### SplStakePoolTask

Fetch the JSON representation of an SPL Stake Pool account.

| Field    | Type   | Description                       |
| -------- | ------ | --------------------------------- |
| `pubkey` | string | The pubkey of the SPL Stake Pool. |

***

### SuiLstPriceTask

Get the exchange rate for Sui Liquid Staking Tokens (LSTs) relative to SUI.

All configuration is passed as parameters, allowing support for any LST without code changes.

***Input***: None

***Returns***: The exchange rate (e.g., 1.068 means 1 LST = 1.068 SUI)

***Example***: haSUI (simple - 1 shared object):

```json
{
  "suiLstPriceTask": {
    "packageId": "0xbde4ba4c2e274a60ce15c1cfff9e5c42e41654ac8b6d906a57efa4bd3c29f47d",
    "module": "staking",
    "function": "get_sui_by_stsui",
    "sharedObjects": [
      "0x47b224762220393057ebf4f70501b6e657c3e56684737568439a04f80849b2ca"
    ],
    "provideLstAmount": true
  }
}
```

***Example***: vSUI (2 shared objects - StakePool + Metadata):

```json
{
  "suiLstPriceTask": {
    "packageId": "0x68d22cf8bdbcd11ecba1e094922873e4080d4d11133e2443fddda0bfd11dae20",
    "module": "stake_pool",
    "function": "lst_amount_to_sui_amount",
    "sharedObjects": [
      "0x2d914e23d82fedef1b5f56a32d5c64bdcc3087ccfea2b4d6ea51a71f587840e5",
      "0x680cd26af32b2bde8d3361e804c53ec1d1cfe24c7f039eb7f549e8dfde389a60"
    ],
    "provideLstAmount": true
  }
}
```

| Field                | Type   | Description                                                                                                                                                                                                                                                                                             |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package_id`         | string | The package ID containing the exchange rate function.                                                                                                                                                                                                                                                   |
| `module`             | string | The module name containing the exchange rate function.                                                                                                                                                                                                                                                  |
| `function`           | string | The function name to call (e.g., "get\_sui\_by\_stsui", "from\_shares", "get\_exchange\_rate").                                                                                                                                                                                                         |
| `shared_objects`     | string | List of shared object IDs to pass as arguments (in order). These will be resolved to SharedObject arguments with their initial\_shared\_version.                                                                                                                                                        |
| `provide_lst_amount` | bool   | If true, appends the LST amount (1e9 = 1 token) as a Pure u64 argument after shared objects. Set to true for functions like "get\_sui\_by\_stsui(staking, amount)" or "from\_shares(pool, meta, amount)". Set to false for functions like "get\_exchange\_rate(staking)" that return the rate directly. |
| `rpc_url`            | string | The Sui RPC endpoint to use for fetching on-chain data. If not specified, uses the default mainnet RPC.                                                                                                                                                                                                 |

***

### VsuiPriceTask

Get the vSUI/SUI exchange rate on Sui mainnet. No inputs required - uses hardcoded vSUI pool addresses. @deprecated Use SuiLstPriceTask with lst\_type = LST\_VSUI instead.

| Field     | Type   | Description                                                                                             |
| --------- | ------ | ------------------------------------------------------------------------------------------------------- |
| `rpc_url` | string | The Sui RPC endpoint to use for fetching on-chain data. If not specified, uses the default mainnet RPC. |

***

## Oracle Integration

### EwmaTask

Compute an exponentially weighted moving average (EWMA) over an aggregator's history buffer.

***Input***: Aggregator address, lookback period (in seconds), and smoothing factor `lambda`.

***Returns***: The EWMA value over the specified period.

***Example***: Compute the 1h EWMA with lambda 0.94

```json
{
  "ewmaTask": {
    "aggregatorAddress": "AGGREGATOR_PUBKEY",
    "period": 3600,
    "lambda": 0.94
  }
}
```

| Field                | Type   | Description                           |
| -------------------- | ------ | ------------------------------------- |
| `aggregator_address` | string | The aggregator to query.              |
| `period`             | int32  | Lookback period in seconds.           |
| `lambda`             | double | Smoothing factor in the range (0, 1]. |

***

### OracleTask

Fetch the current price of a Solana oracle protocol.

***Input***: None

***Returns***: The current price of an on-chain oracle.

***Example***: The Switchboard SOL/USD oracle price.

```json
{
  "oracleTask": {
    "switchboardAddress": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
  }
}
```

***Example***: The Pyth SOL/USD oracle price.

```json
{
  "oracleTask": {
    "pythAddress": "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG"
  }
}
```

***Example***: The Pyth SOL/USD oracle price using a Hermes API key supplied at execution time.

```json
{
  "oracleTask": {
    "pythAddress": "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG",
    "pythConfigs": {
      "apiKey": "${PYTH_API_KEY}"
    }
  }
}
```

Supply the key in the execution request as `"variableOverrides": { "PYTH_API_KEY": "..." }`.

***Example***: The Pyth SOL/USD push oracle price.

```json
{
  "oracleTask": {
    "pythPushFeedId": "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d",
    "pythConfigs": {
      "pushFeedShardId": 0,
      "maxStaleSeconds": 75
    }
  }
}
```

***Example***: The Chainlink SOL/USD oracle price.

```json
{
  "oracleTask": {
    "chainlinkAddress": "CcPVS9bqyXbD9cLnTbhhHazLsrua8QMFUHTutPtjyDzq"
  }
}
```

| Field                              | Type        | Description                                                                                                                                                                                                                                                                                         |
| ---------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `switchboard_address`              | string      | Mainnet address of a Switchboard feed. Switchboard is decentralized and allows anyone to build their own feed.                                                                                                                                                                                      |
| `pyth_address`                     | string      | Mainnet address for a Pyth feed. A full list can be found here: <https://pyth.network/price-feeds/>                                                                                                                                                                                                 |
| `chainlink_address`                | string      | Mainnet address for a Chainlink feed. A full list can be found here: <https://docs.chain.link/docs/solana/data-feeds-solana>                                                                                                                                                                        |
| `pyth_push_feed_id`                | string      | Pyth price feed ID for an upgraded Solana push feed. The task derives and reads the on-chain feed account using this ID and pyth\_configs.push\_feed\_shard\_id; it does not use Hermes or a Hermes API key.                                                                                        |
| `pyth_allowed_confidence_interval` | double      | Value (as a percentage) that the lower bound confidence interval is of the actual value. Confidence intervals that are larger that this treshold are rejected. The confidence interval should be provided as a raw percentage value. For example, to represent 10%, enter the value as 10, not 0.1. |
| `pyth_configs`                     | PythConfigs | Optional settings for Pyth Hermes and on-chain push-feed tasks.                                                                                                                                                                                                                                     |

**PythConfigs fields**

| Field                              | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hermes_url`                       | string | Optional Hermes base URL used by pyth\_address tasks.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `pyth_allowed_confidence_interval` | double | Preferred Pyth confidence interval setting, expressed as a raw percentage. For example, use 10 to represent 10%, not 0.1.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `max_stale_seconds`                | int32  | Maximum accepted price age in seconds. Defaults to 15 seconds for pyth\_address and 75 seconds for pyth\_push\_feed\_id.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `push_feed_shard_id`               | uint32 | Pyth push-feed shard used only by pyth\_push\_feed\_id. Defaults to shard 0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `api_key`                          | string | Optional API key for authenticated Pyth Hermes requests made by pyth\_address tasks. Use a variable placeholder such as `${PYTH_API_KEY}` and supply a matching non-empty variableOverrides value at execution time; do not hardcode credentials. This field takes precedence when it is non-empty. If it is omitted or empty, variableOverrides.PYTH\_API\_KEY is accepted as a compatibility fallback. That fallback is request-wide: the same value is used by every pyth\_address task in the execution that does not configure its own api\_key. Pyth push-feed tasks read on-chain accounts and do not use this API key. |

***

### SurgeTwapTask

Compute TWAP from local candle database using AUTO-resolved source.

Uses the AUTO source cache to resolve the best (exchange, pair) for a canonical USD pair, then queries local candle storage and computes TWAP.

| Field           | Type         | Description                                                        |
| --------------- | ------------ | ------------------------------------------------------------------ |
| `symbol`        | string       | Canonical USD trading pair (e.g., "BTC/USD", "ETH/USD", "SOL/USD") |
| `time_interval` | TimeInterval | Time interval for TWAP calculation (default: ONE\_HOUR)            |

***

### SwitchboardSurgeTask

Fetch a *live* spot price straight out of the global **Surge** websocket cache – the same cache that powers our high-speed on-chain oracles. ***Input*** • `symbol` – the trading-pair symbol as it appears on the exchange • `source` – which exchange's stream to read from • `BINANCE` (weight 3) • `BYBIT` (weight 2) • `OKX` (weight 2) • `COINBASE` (weight 3, disabled) • `BITGET` (weight 2) • `PYTH` (weight 1) – Pyth oracle network • `TITAN` (weight 1) – Titan DEX aggregator on Solana • `WEIGHTED` (default) – use the *weighted median* of all fresh quotes with the weights shown above. • `AUTO` – automatically select the best source based on volume, spread, and data quality metrics. ***Returns*** The most recent price available from the chosen source. The task fails if the cached tick is older than **5 s**. ***Example***: Pull the Binance price for BTC / USDT

```json
{
  "switchboardSurgeTask": {
    "source": "BINANCE",
    "symbol": "BTC/FDUSD"
  }
}
```

***Example***: Use the weighted-median oracle for BTC / USDT

```json
{
  "switchboardSurgeTask": {
    "source": "WEIGHTED",   // or omit — WEIGHTED is the default
    "symbol": "BTC/USD"
  }
}
```

***Example***: Pull the Pyth oracle price for PYUSD / USD

```json
{
  "switchboardSurgeTask": {
    "source": "PYTH",
    "symbol": "PYUSD/USD"
  }
}
```

***Example***: Pull the Titan DEX aggregator price for SOL / USDC

```json
{
  "switchboardSurgeTask": {
    "source": "TITAN",
    "symbol": "SOL/USDC"
  }
}
```

***Notes*** • Symbols are auto-normalised (case-insensitive, punctuation removed). • If a venue’s price is stale (> 5 s) it is ignored in the WEIGHTED calculation. The task errors if **no** fresh price remains. • The weighted-median algorithm uses cumulative weights based on each exchange's data quality and volume. Currently active sources: Binance (3), Bybit (2), OKX (2), Bitget (2), Pyth (1), Titan (1).

***

### TwapTask

Takes a twap over a set period for a certain aggregator. Aggregators have an optional history buffer account storing the last N accepted results. The TwapTask will iterate over an aggregators history buffer and calculate the time weighted average of the samples within a given time period.

***Input***: None

***Returns***: The time weighted average of an aggregator over a given time period.

***Example***: The 1hr Twap of the SOL/USD Aggregator, requiring at least 60 samples.

```json
{
  "twapTask": {
    "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR",
    "period": 3600,
    "minSamples": 60,
    "weightByPropagationTime": true
  }
}
```

| Field                        | Type          | Description                                                          |
| ---------------------------- | ------------- | -------------------------------------------------------------------- |
| `aggregator_pubkey`          | string        | The target aggregator for the TWAP.                                  |
| `period`                     | int32         | Period, in seconds, the twap should account for                      |
| `weight_by_propagation_time` | bool          | Weight samples by their propagation time                             |
| `min_samples`                | uint32        | Minimum number of samples in the history to calculate a valid result |
| `ending_unix_timestamp`      | int32         | Ending unix timestamp to collect values up to                        |
| `ending_unix_timestamp_task` | CronParseTask | Execute the task to get the ending unix timestamp                    |

***

## Specialized Finance

### ExponentPTLinearPricingTask

Compute the current price of an Exponent principal token using a linear schedule from `start_price` to 1.0 at maturity. ***Input***: Vault address and starting price. ***Returns***: The current principal token price. ***Example***: Price a PT that linearly approaches 1.0

````json
   {
     "exponentPtLinearPricingTask": {
       "vault": "9YbaicMsXrtupkpD72pdWBfU6R7EJfSByw75sEpDM1uH",
       "startPrice": 0.8
     }
   }

| Field | Type | Description |
|-------|------|-------------|
| `vault` | string | The Exponent vault address. |
| `start_price` | double | The starting price at the vault start timestamp. |

---

### ExponentTask

Get the exchange rate between and Exponent vault pricipal token and
underlying token.
_**Input**_: Vault address
_**Returns**_: The exchange rate between the vault principal token and
underlying token.
_**Example**_: Get the exchange rate between the vault principal token and
underlying token.
```json
   {
     "exponentTask": {
       "vault": "9YbaicMsXrtupkpD72pdWBfU6R7EJfSByw75sEpDM1uH"
     }
   }

---

### KalshiApiTask

KalshiApiTask fetches a GET endpoint from the Kalshi API (with a token if supplied) and returns the JSON result

| Field | Type | Description |
|-------|------|-------------|
| `url` | string | A string containing the URL to direct this HTTP request to. |
| `api_key_id` | string | A string containing the API Key ID |
| `private_key` | string | A string containing the private key for authentication |
| `signature` | string | Optional signature string field |
| `timestamp` | string | Optional timestamp in milliseconds (used with signature) |

---

### LendingRateTask

Fetch the lending rates for various Solana protocols

| Field | Type | Description |
|-------|------|-------------|
| `protocol` | string | 01, apricot, francium, jet, larix, mango, port, solend, tulip |
| `asset_mint` | string | A token mint address supported by the chosen protocol |

---

### MapleFinanceTask

Fetch pricing information for Maple Finance assets.

_**Input**_: None

_**Returns**_: The requested price or value based on the specified method.

_**Example**_: Fetch the syrupUSDC fair price from Maple Finance

```json
{
  "mapleFinanceTask": {
    "method": "METHOD_SYRUP_USDC_FAIR_PRICE"
  }
}
````

| Field    | Type   | Description                               |
| -------- | ------ | ----------------------------------------- |
| `method` | Method | The specific method to use for this task. |

***

### OndoUsdyTask

OndoUsdyTask represents a task that computes the price of USDY relative to USD using a specified strategy.

| Field      | Type     | Description                                       |
| ---------- | -------- | ------------------------------------------------- |
| `strategy` | Strategy | The strategy used to determine the price of USDY. |

***

### PerpMarketTask

Fetch the current price of a perpetual market.

| Field                  | Type   | Description                                                                                                                                                      |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mango_market_address` | string | Market address for a mango perpetual market. A full list can be found here: <https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json>    |
| `drift_market_address` | string | Market address for a drift perpetual market. A full list can be found here: <https://github.com/drift-labs/protocol-v1/blob/master/sdk/src/constants/markets.ts> |
| `zeta_market_address`  | string | Market address for a zeta perpetual market.                                                                                                                      |
| `zo_market_address`    | string | Market address for a 01 protocol perpetual market.                                                                                                               |

***

### TurboEthRedemptionRateTask

Fetches tETH/WETH redemption rate

***

## Utilities

### Blake2b128Task

Compute the BLAKE2b-128 hash of the input data and convert it to a numeric Decimal value.

This task follows **cryptographic standard hash truncation** practices used in:

* **SHA-224**: SHA-256 truncated to leftmost 224 bits
* **BLAKE2s-128**: BLAKE2s-256 truncated to leftmost 128 bits
* **BLAKE2b-256**: BLAKE2b-512 truncated to leftmost 256 bits

***Input***: String data to hash (can be from a previous task's output)

***Returns***: A positive Decimal number with 18 decimal places (scale 18)

* Range: `0.000000000000000000` to `79228162514264.337593543950335` (2^96 - 1, scaled)
* Example: `17512223723.299011049621773283`

***

**Hash-to-Decimal Conversion Algorithm**

**Step-by-Step Process**

**1. Compute BLAKE2b-128 hash** (produces 16 bytes / 128 bits)

```
Input:  "Hello, World!"
Output: 3895c59e4aeb0903396b5be3fbec69fe
```

**2. Truncate to 96 bits (12 bytes)** - keep the **most significant** bits

```
KEPT (first 12 bytes):      3895c59e4aeb0903396b5be3
DISCARDED (last 4 bytes):   fbec69fe
```

This follows cryptographic standards where truncation keeps the leftmost/most significant bits.

**3. Pad to 16 bytes** for u128 representation

```
Add 4 zero bytes at the BEGINNING:

000000003895c59e4aeb0903396b5be3
└──┬──┘└──────────┬───────────┘
padding      first 12 bytes
(4 bytes)    (most significant)
```

**4. Interpret as u128 using big-endian** byte order

```
Hex:     0x000000003895c59e4aeb0903396b5be3
Decimal: 17512223723299011049621773283
```

Big-endian is the standard for cryptographic hash representations.

**5. Convert to Decimal** with scale 18 (18 decimal places)

```
Value:  17512223723299011049621773283
Scaled: 17512223723.299011049621773283 (divided by 10^18)
```

Scale 18 prevents precision loss when the protocol rescales values. Guaranteed to fit in Decimal's 96-bit mantissa (max: 2^96 - 1).

***

**Reproducibility**

To reproduce this conversion in **any programming language**:

**Python Example**

```python
import hashlib
from decimal import Decimal

# 1. Compute BLAKE2b-128 hash
data = b"Hello, World!"
hash_bytes = hashlib.blake2b(data, digest_size=16).digest()
# Result: b'\x38\x95\xc5\x9e\x4a\xeb\x09\x03\x39\x6b\x5b\xe3\xfb\xec\x69\xfe'

# 2. Keep first 12 bytes (most significant 96 bits)
truncated = hash_bytes[:12]

# 3. Pad with 4 zero bytes at the beginning
padded = b'\x00\x00\x00\x00' + truncated

# 4. Interpret as u128 big-endian
value = int.from_bytes(padded, byteorder='big')
# Result: 17512223723299011049621773283

# 5. Apply scale 18 (divide by 10^18)
result = Decimal(value) / Decimal(10**18)
# Result: Decimal('17512223723.299011049621773283')
```

**JavaScript Example**

```javascript
const crypto = require('crypto');

// 1. Compute BLAKE2b-128 hash
const hash = crypto.createHash('blake2b512')
  .update('Hello, World!')
  .digest()
  .slice(0, 16); // Take first 16 bytes for BLAKE2b-128

// 2. Keep first 12 bytes
const truncated = hash.slice(0, 12);

// 3. Pad with 4 zero bytes at the beginning
const padded = Buffer.concat([Buffer.alloc(4), truncated]);

// 4. Interpret as big-endian u128
let value = 0n;
for (let i = 0; i < 16; i++) {
  value = (value << 8n) | BigInt(padded[i]);
}
// Result: 17512223723299011049621773283n

// 5. Apply scale 18 (divide by 10^18)
const result = Number(value) / 1e18;
// Result: 17512223723.299011 (note: JS loses precision beyond ~15 digits)
```

**Rust Example**

```rust
use blake2::{Blake2b, Digest};
use blake2::digest::consts::U16;
use rust_decimal::Decimal;

type Blake2b128 = Blake2b<U16>;

// 1. Compute BLAKE2b-128 hash
let mut hasher = Blake2b128::new();
hasher.update(b"Hello, World!");
let hash = hasher.finalize();

// 2. Keep first 12 bytes and pad at beginning
let mut bytes = [0u8; 16];
bytes[4..16].copy_from_slice(&hash[0..12]);

// 3. Interpret as big-endian u128
let value = u128::from_be_bytes(bytes);
// Result: 17512223723299011049621773283

// 4. Apply scale 18 (convert with 18 decimal places)
let result = Decimal::from_i128_with_scale(value as i128, 18);
// Result: Decimal("17512223723.299011049621773283")
```

***

**Why This Approach?**

✅ **Cryptographic Standard**: Follows the same truncation method as SHA-224, BLAKE2s-128, etc. ✅ **Preserves Entropy**: Keeps the most significant/diverse bits of the hash ✅ **Big-Endian**: Standard convention for cryptographic hash representations ✅ **Fits Decimal Range**: 96 bits always fits within Decimal's mantissa (max 2^96-1) ✅ **Scale 18**: Prevents precision loss when protocol rescales values ✅ **Reproducible**: Simple algorithm implementable in any programming language ✅ **Deterministic**: Same input always produces same output

***

***Example***: Hash a static string

```json
{
  "blake2b128Task": {
    "value": "Hello, World!"
  }
}
```

***Example***: Hash the output from a previous task (e.g., HTTP response)

```json
{
  "tasks": [
    {
      "httpTask": {
        "url": "https://example.com/data"
      }
    },
    {
      "blake2b128Task": {}
    }
  ]
}
```

| Field   | Type   | Description                                                                          |
| ------- | ------ | ------------------------------------------------------------------------------------ |
| `value` | string | Optional value to hash. If not provided or empty, will use the previous task output. |

***

### CacheTask

Execute a job and store the result in a variable to reference later.

***Input***: None

***Returns***: The input

***Example***: CacheTask storing ${ONE} = 1

```json
{
  "cacheTask": {
    "cacheItems": [
      {
        "variableName": "ONE",
        "job": {
          "tasks": [
            {
              "valueTask": {
                "value": 1
              }
            }
          ]
        }
      }
    ]
  }
}
```

| Field         | Type      | Description                                                                 |
| ------------- | --------- | --------------------------------------------------------------------------- |
| `cache_items` | CacheItem | A list of cached variables to reference in the job with `${VARIABLE_NAME}`. |

**CacheItem fields**

| Field           | Type      | Description                                                                            |
| --------------- | --------- | -------------------------------------------------------------------------------------- |
| `variable_name` | string    | The name of the variable to store in cache to reference later with `${VARIABLE_NAME}`. |
| `job`           | OracleJob | The OracleJob to execute to yield the value to store in cache.                         |

***

### ComparisonTask

Compare two values and return one of two results.

***Input***: LHS/RHS values or jobs, plus on\_true/on\_false outputs.

***Returns***: The value produced by `on_true` or `on_false` (or `on_failure` if evaluation fails).

***Example***: Return 1 if lhs > rhs else 0

```json
{
  "comparisonTask": {
    "op": "OPERATION_GT",
    "lhsValue": "10",
    "rhsValue": "5",
    "onTrueValue": "1",
    "onFalseValue": "0"
  }
}
```

| Field              | Type      | Description                                                                            |
| ------------------ | --------- | -------------------------------------------------------------------------------------- |
| `op`               | Operation | The type of operator to use on the left (lhs) and right (rhs) operand.                 |
| `lhs`              | OracleJob | OracleJob where the executed result is equal to the left hand side operand.            |
| `lhs_value`        | string    | String or `${CACHE_KEY}` representing the left hand side operand.                      |
| `rhs`              | OracleJob | OracleJob where the executed result is equal to the right hand side operand.           |
| `rhs_value`        | string    | String or `${CACHE_KEY}` representing the right hand side operand.                     |
| `on_true`          | OracleJob | The OracleJob to execute if the condition evaluates to true.                           |
| `on_true_value`    | string    | The result to use if the condition evaluates to true. Can be set to a `${CACHE_KEY}`.  |
| `on_false`         | OracleJob | The OracleJob to execute if the condition evaluates to false.                          |
| `on_false_value`   | string    | The result to use if the condition evaluates to false. Can be set to a `${CACHE_KEY}`. |
| `on_failure`       | OracleJob | The OracleJob to execute if the condition fails to evaluate.                           |
| `on_failure_value` | string    | The result to use if the condition fails to evaluate. Can be set to a `${CACHE_KEY}`.  |

***

### ConditionalTask

This task will run the `attempt` on the subtasks in an effort to produce a valid numerical result. If `attempt`. fails to produce an acceptable result, `on_failure` subtasks will be run instead.

***Input***: The current running numerical result output from a task.

***Returns***: A numerical result, else run `on_failure` subtasks.

***Example***: Returns the numerical result from the conditionalTask's subtasks, else `on_failure` returns the numerical result from its subtasks.

```json
{
  "conditionalTask": {
    "attempt": [
      {
        "tasks": [
          {
            "jupiterSwapTask": {
              "inTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
              "outTokenAddress": "DUALa4FC2yREwZ59PHeu1un4wis36vHRv5hWVBmzykCJ"
            }
          }
        ]
      }
    ],
    "onFailure": [
      {
        "lpExchangeRateTask": {
          "orcaPoolAddress": "7yJ4gMRJhEoCR48aPE3EAWRmCoygakik81ZS1sajaTnE"
        }
      }
    ]
  }
}
```

| Field        | Type | Description                                                                                           |
| ------------ | ---- | ----------------------------------------------------------------------------------------------------- |
| `attempt`    | Task | A list of subtasks to process in an attempt to produce a valid numerical result.                      |
| `on_failure` | Task | A list of subtasks that will be run if `attempt` subtasks are unable to produce an acceptable result. |

***

### SecretsTask

Deprecated compatibility task for the legacy Switchboard secrets-server flow.

`SecretsTask` is no longer officially supported. The hosted secrets service has been taken down, and new integrations should use `variableOverrides` to inject API keys and other authentication credentials at request time.

See the Data Feed Variable Overrides guide for the supported pattern.

***Input***: None

***Returns***: The input

***Example***: Legacy `SecretsTask`

```json
{
  "secretsTask": {
    "authority": "Accb21tUCWocJea6Uk3DgrNZawgmKegDVeHw8cGMDPi5"
  }
}
```

| Field       | Type   | Description                                                                                                                                                   |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authority` | string | The authority of the secrets that are to be requested.                                                                                                        |
| `url`       | string | Legacy server URL override for historical or self-hosted deployments. The old hosted default at <https://api.secrets.switchboard.xyz> is no longer available. |

***

### SysclockOffsetTask

Return the difference between an oracle's clock and the current timestamp at `SYSVAR_CLOCK_PUBKEY`.

***

### UnixTimeTask

Get current time in seconds since Unix epoch.

| Field    | Type  | Description                                   |
| -------- | ----- | --------------------------------------------- |
| `offset` | int32 | The offset to subtract from the current time. |

***

### ValueTask

Returns a specified value.

***Input***: None

***Returns***: A numerical result.

***Example***: Returns the value 10

```json
{
  "valueTask": {
    "value": 10
  }
}
```

***Example***: Returns the currentRound result of an aggregator

```json
{
  "valueTask": {
    "aggregatorPubkey": "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"
  }
}
```

***Example***: Returns the value stored in a CacheTask variable

```json
{
  "valueTask": {
    "big": "${ONE}"
  }
}
```

| Field               | Type   | Description                                                |
| ------------------- | ------ | ---------------------------------------------------------- |
| `value`             | double | The value that will be returned from this task.            |
| `aggregator_pubkey` | string | Specifies an aggregatorr to pull the value of.             |
| `big`               | string | A stringified big.js. `Accepts variable expansion syntax.` |
| `hex`               | string | A stringified hex number (0x prefix is optional).          |

***

## Protocol-Specific

### AftermathTask

Fetch a spot swap quote from an Aftermath pool on Sui.

***Input***: Pool address, input amount, and coin types for the swap.

***Returns***: The estimated output amount for the given input.

***Example***: Quote a swap in an Aftermath pool

```json
{
  "aftermathTask": {
    "poolAddress": "0xPOOL_OBJECT_ID",
    "inAmount": 1,
    "inCoinType": "0x2::sui::SUI",
    "outCoinType": "0x...::coin::COIN"
  }
}
```

| Field           | Type   | Description                                  |
| --------------- | ------ | -------------------------------------------- |
| `pool_address`  | string | The Aftermath pool object ID.                |
| `in_amount`     | double | The input amount to quote.                   |
| `in_coin_type`  | string | The full Sui coin type for the input token.  |
| `out_coin_type` | string | The full Sui coin type for the output token. |

***

### BitFluxTask

Fetch the current swap price from a BitFlux pool.

***Input***: None

***Returns***: The swap price between the specified input and output tokens.

***Example***: Fetch the swap price using a custom RPC provider

```json
{
  "bitFluxTask": {
    "provider": "https://my-custom-rpc.example.com",
    "poolAddress": "0x0000000000000000000000000000000000000000",
    "inToken": "0x0000000000000000000000000000000000000000",
    "outToken": "0x0000000000000000000000000000000000000000"
  }
}
```

| Field          | Type   | Description                                                                                   |
| -------------- | ------ | --------------------------------------------------------------------------------------------- |
| `provider`     | string | Optional. The RPC endpoint to use for requests. If not specified, a default RPC will be used. |
| `pool_address` | string | The address of the BitFlux pool.                                                              |
| `in_token`     | string | The address of the input token.                                                               |
| `out_token`    | string | The address of the output token.                                                              |

***

### CorexTask

Fetch a swap quote from CoreX (Core chain) using Uniswap V3 pricing.

***Input***: Input token address, output token address, and slippage.

***Returns***: The quoted swap price for the requested pair.

***Example***: Quote STCORE -> WCORE

```json
{
  "corexTask": {
    "inToken": "0xIN_TOKEN",
    "outToken": "0xOUT_TOKEN",
    "slippage": 0.01
  }
}
```

| Field       | Type   | Description                                           |
| ----------- | ------ | ----------------------------------------------------- |
| `in_token`  | string | The input token address.                              |
| `out_token` | string | The output token address.                             |
| `slippage`  | double | The allowable slippage in percent for the swap quote. |

***

### EtherfuseTask

Fetch the current price for Etherfuse Stablebonds.

***Input***: The stablebond token to price.

***Returns***: The price of 1 bond in USDC.

***Example***: Fetch the CETES bond price

```json
{
  "etherfuseTask": {
    "token": "TOKEN_CETES"
  }
}
```

| Field   | Type  | Description                              |
| ------- | ----- | ---------------------------------------- |
| `token` | Token | The Etherfuse stablebond token to price. |

***

### FragmetricTask

Fetch the current price for Fragmetric liquid restaking tokens.

***Input***: None

***Returns***: The current price of the specified Fragmetric token relative to SOL (1.0 = 1 SOL)

***Example***: Fetch the fragSOL token price

```json
{
  "fragmetricTask": {
    "token": "TOKEN_FRAG_SOL"
  }
}
```

| Field   | Type  | Description                                 |
| ------- | ----- | ------------------------------------------- |
| `token` | Token | The Fragmetric token to fetch the price for |

***

### GlyphTask

Fetch the spot price from a Glyph (Algebra) pool on Core.

***Input***: Pool address and swap direction.

***Returns***: The pool price in the requested direction.

***Example***: Token0 -> Token1 price

```json
{
  "glyphTask": {
    "poolAddress": "0xPOOL_ADDRESS",
    "zeroForOne": true
  }
}
```

| Field          | Type   | Description                                                  |
| -------------- | ------ | ------------------------------------------------------------ |
| `pool_address` | string | The Algebra pool address on Core.                            |
| `zero_for_one` | bool   | True for token0 -> token1 price, false for token1 -> token0. |

***

### XStepPriceTask

Fetch the STEP/USD price either by running a MedianTask or reading an existing aggregator.

***Input***: Provide either `step_job` or `step_aggregator_pubkey`.

***Returns***: The STEP/USD price.

***Example***: Read from an existing STEP/USD aggregator

```json
{
  "xstepPriceTask": {
    "stepAggregatorPubkey": "STEP_USD_AGGREGATOR"
  }
}
```

| Field                    | Type       | Description                                                            |
| ------------------------ | ---------- | ---------------------------------------------------------------------- |
| `step_job`               | MedianTask | median task containing the job definitions to fetch the STEP/USD price |
| `step_aggregator_pubkey` | string     | existing aggregator pubkey for STEP/USD                                |

***

## Other

### HistoryFunctionTask

Compute a reduction (min/max) over an aggregator's history buffer.

***Input***: Aggregator address, method, and lookback period (in seconds).

***Returns***: The min or max value observed in the period.

***Example***: Fetch the max price over the last hour

```json
{
  "historyFunctionTask": {
    "aggregatorAddress": "AGGREGATOR_PUBKEY",
    "method": "METHOD_MAX",
    "period": 3600
  }
}
```

| Field                | Type   | Description                                            |
| -------------------- | ------ | ------------------------------------------------------ |
| `method`             | Method | The reduction method to apply over the history buffer. |
| `aggregator_address` | string | The aggregator to query.                               |
| `period`             | uint32 | Lookback period in seconds.                            |

***

### LlmTask

Interacts with a Large Language Model (LLM) to generate a text response based on a user-provided prompt.

***Input***: None

***Returns***: Text generated by the LLM based on the provided prompt and configuration.

***Example***: Using OpenAI's GPT-4 model to generate a joke.

````json
{
  "llmTask": {
    "providerConfig": {
      "openai": {
        "model": "gpt-4",
        "userPrompt": "Tell me a joke.",
        "temperature": 0.7,
        "secretNameApiKey": "${OPENAI_API_KEY}"
      }
    }
  }
}

---

### MangoPerpMarketTask

Fetch the current price for a Mango perpetual market

| Field | Type | Description |
|-------|------|-------------|
| `perp_market_address` | string | Mainnet address for a mango perpetual market. A full list can be found here: https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json |

---

### VwapTask

Compute a volume-weighted average price (VWAP) using price and volume aggregators.

_**Input**_: Price aggregator address, volume aggregator address, and lookback period (in seconds).

_**Returns**_: The VWAP over the specified period.

_**Example**_: Compute the 1h VWAP for a price/volume pair

```json
{
  "vwapTask": {
    "priceAggregatorAddress": "PRICE_AGGREGATOR",
    "volumeAggregatorAddress": "VOLUME_AGGREGATOR",
    "period": 3600
  }
}
````

| Field                       | Type   | Description                                  |
| --------------------------- | ------ | -------------------------------------------- |
| `price_aggregator_address`  | string | The aggregator that provides price samples.  |
| `volume_aggregator_address` | string | The aggregator that provides volume samples. |
| `period`                    | uint32 | Lookback period in seconds.                  |

***

## Next Steps

* [Build with TypeScript](/custom-feeds/build-and-deploy-feed/build-with-typescript) - Create feeds programmatically
* [Build with UI](/custom-feeds/build-and-deploy-feed/build-with-ui) - Use the visual feed builder
* [Advanced Feed Configuration](/custom-feeds/advanced-feed-configuration) - Learn about variable overrides and more


# Switchboard Protocol

The Switchboard Protocol is a decentralised oracle network that enables blockchain applications to access real-world data securely and reliably. At its core, the protocol coordinates a network of independent oracle operators who fetch, verify, and deliver data on-chain.

## How It Works

1. **Data Requests**: Applications request data through Switchboard's on-chain contracts
2. **Oracle Execution**: Oracle nodes running in Trusted Execution Environments (TEEs) fetch and process the requested data
3. **Verification**: The protocol verifies oracle signatures and ensures data integrity before making it available on-chain
4. **Delivery**: Verified data is delivered to your smart contracts for use

## Key Components

* **Oracle Operators**: Independent node operators who run Switchboard oracles and earn rewards for providing accurate data
* **Staking & Slashing**: Economic security through restaking mechanisms that incentivise honest behaviour and penalise malicious actors
* **TEE Protection**: Hardware-level security ensures oracle code cannot be tampered with or inspected, even by the operators themselves

## Protocol Security

The protocol's security model combines cryptographic verification with economic incentives:

* Oracles must run verified code within TEEs, preventing manipulation
* Operators stake assets that can be slashed for misbehaviour
* Multiple oracles can be required to reach consensus on data values
* On-chain verification ensures only properly signed data is accepted

## Get Involved

* [Run your own oracle](/how-it-works/switchboard-protocol/running-a-switchboard-oracle) and earn rewards
* [Provide stake](/how-it-works/switchboard-protocol/providing-stake-to-switchboard) to secure the network
* [Learn about restaking](/how-it-works/switchboard-protocol/re-staking) and the Switchboard NCN


# (Re)staking

The Switchboard protocol is improving the security and efficiency of its data feeds through integration with Jito Node Consensus Networks (NCNs). Jito NCNs are decentralised networks that utilise staked assets to secure and validate on-chain activity within the Solana ecosystem. These NCNs leverage the economic security provided by staked assets to power their operations, creating the potential for stakers to earn rewards. Jito Restaking provides a flexible, multi-asset staking protocol on Solana where users can stake various SPL tokens, receive liquid Vault Receipt Tokens (VRTs) representing their stake, and earn rewards from multiple sources. By integrating Jito's NCNs, Switchboard is significantly strengthening the reliability and performance of its protocol, ultimately delivering enhanced benefits to its users.

Specifically, Switchboard chose to secure its protocol with Jito NCNs because of what they offer. That is:

* **Enhanced Data Security:** An extra layer of decentralised security, making Switchboard's data feeds more resistant to manipulation and attacks.
* **Improved Protocol Reliability:** By leveraging a wide range of staked assets within Jito's ecosystem, the Switchboard protocol gains access to a more robust and dependable network for validating its data.
* **User Rewards:** By participating in the Jito Restaking ecosystem through Switchboard, users may have the opportunity to earn additional protocol rewards from their staked assets from multiple networks.

Keen to understand restaking, Node Consensus Networks, or Vault Receipt Tokens in greater detail? Explore our dedicated sections:\
[What is restaking.](/how-it-works/switchboard-protocol/re-staking/what-is-re-staking)\
[What are Node Consensus Networks (NCNs).](/how-it-works/switchboard-protocol/re-staking/what-are-node-consensus-networks-ncns)\
[What are Vault Receipt Tokens (VRTs).](/how-it-works/switchboard-protocol/re-staking/what-are-vault-receipt-tokens-vrts)

Interested in helping secure the Switchboard Protocol? Visit ‘[The Node Partner Program](/how-it-works/switchboard-protocol/re-staking/the-node-partner-program)’


# What is (re)staking?

Blockchains have evolved to solve problems with scaling, security, and speed. Restaking is the newest step in this evolution. Instead of building completely new security systems for each blockchain or application, restaking lets developers “borrow” security from existing, established networks. This allows them to focus on building great applications instead of spending all their time and resources on securing the underlying network.

Imagine it like this: **staking** is like putting down a deposit to guarantee good behaviour on a blockchain. **Restaking**, then, is like reusing that same deposit to secure multiple things at once.

#### **Why is this important?**

* **Faster Innovation:** Developers can launch new applications more quickly and easily.
* **Stronger Security:** New projects instantly benefit from the security of well-established networks.
* **More Efficient Use of Assets:** Users can earn more rewards by staking their tokens and contributing to the security of multiple projects.

Before restaking, new blockchains or applications had to create their own security from scratch. Restaking lets them leverage the “economic security” of existing networks, offering stronger protection from the start. At its core, restaking simply applies the principles of traditional staking to more applications and services.

The biggest restaking project currently is outside of Solana, called [EigenLayer](https://www.eigenlayer.xyz/). However, the [Jito (Re)staking platform](https://www.jito.network/restaking/) is pioneering this concept on Solana, allowing users to secure new on-chain products and services using almost any SPL token.


# What are Node Consensus Networks (NCNs)?

Node Consensus Networks (NCNs) represent a foundational architecture within blockchain technology, utilising distributed processes to specifically validate and confirm information. These networks operate through collections of independent nodes collaborating to scrutinise actions, transactions, and various data types. Essentially, any system aiming to establish a decentralised network bolstered by community validation can be classified as an NCN – This even includes the Switchboard Protocol, which is intrinsically an oracle network that seamlessly connects decentralised applications to real-world data.

A significant catalyst for the rise of NCNs is their ability to address the “cold start problem,” a common hurdle for new decentralised projects. This problem encompasses the difficulties in bootstrapping network security and validation mechanisms from scratch. Traditionally, new networks have had to develop bespoke validation solutions which require substantial resources, time, and robust economic backing to establish a dependable validator network.

Think of NCNs as modular 'building blocks' constructed for trust. NCNs can perform specific roles to confirm the accuracy of price feeds from oracles or even securing cross-chain communications. By specialising and focusing, this ensures higher levels of reliability for any protocol that uses a NCN.

#### **Jito NCN’s and the Switchboard Protocol**

Security is critical for oracle networks due to the high value of the applications that depend on their data. Switchboard is enhancing its security by integrating with [Jito (Re)staking](https://www.jito.network/restaking/), a system built for Solana's Node Consensus Networks (NCNs). Jito allows NCNs to define their own staking rules and penalties for poor performance, creating a more robust and adaptable security model. It also tokenises staked assets into Vault Receipt Tokens (VRTs), making them more flexible and usable.

Switchboard is using Jito's system to launch its own NCN, which will increase the security and reliability of its data feeds. Jito provides a marketplace where node operators (those who manage the NCNs) and the NCNs themselves can connect and collaborate to build a stronger, more interconnected security network. Node operators announce their services on-chain, and Switchboard can then choose to use them.

With Jito, Switchboard can customise its network security – for example, setting specific staking requirements and penalties for misbehaviour. This, along with ongoing monitoring of on-chain and off-chain data, allows Switchboard to precisely manage its security. By combining its data feeds with Jito's restaking capabilities, Switchboard is working to become a leading example of secure and effective restaking on Solana.


# What are Vault Receipt Tokens (VRTs)?

Vault Receipt Tokens (VRTs) are synthetic tokens representing your staked assets in a network. They are akin to receipts you get when you stake your cryptocurrency that you can use elsewhere on-chain. Instead of just locking up your tokens, VRTs give you more flexibility. They allow the network to manage risks better, and some systems even let you stake different types of tokens, giving you more choices. Switchboard uses VRTs within its restaking system, which is modelled after the Jito Node Consensus Network (NCN). To better understand it, let's clarify their role in how Switchboard is set up.

#### Switchboard and VRTs:

To maximise the security of the Switchboard protocol, its staking mechanisms are built on the Jito NCN restaking system. This means Switchboard operates its own NCN to secure key actions performed by oracles within the protocol:

* **Oracle Queues:** In Switchboard, each oracle queue (a group of oracles) is linked to its own Jito NCN.
* **Oracles as Nodes:** Individual oracles within the queue act as nodes (operators) in the NCN.

To ensure these oracle actions are secure the Switchboard’s NCN has associated token vaults. These token vaults are where stakers stake their tokens, for example, SWTCH. When you stake, you receive a Vault Receipt Token (VRT) representing your staked tokens in Switchboard, such as svSWTCH.

### The Role of svSWTCH:

The svSWTCH token has two main roles within Switchboard:

* **Governance:** It's the governance token, allowing stakers to participate in decisions about the network.
* **Economic Incentive:** It ensures oracles operate reliably. Oracle operators need to have a minimum amount of svSWTCH delegated in their operating wallet to participate in the network. The prioritisation and workload distribution among oracles within the network will be determined by a combination of their performance metrics and total stake.


# The Node Partner Program

Switchboard's Node Consensus Network (NCN) benefits from Jito's robust NCN infrastructure, ensuring strong consensus through numerous independent operators and guaranteeing protocol integrity. Further enhancing security, Switchboard relies on a decentralised network of oracles for vital feed data. Node operators are therefore incentivised to maintain the continuous availability, accuracy, and reliability of this data, strengthening overall network performance.

The Switchboard Node Partner Program directly rewards this crucial contribution, empowering skilled operators to actively enhance the security and reliability of our decentralised oracle network. Program members gain access to exclusive benefits that promote growth and foster contributions to the Switchboard ecosystem:

* **Enhanced Revenue Streams:** Earn Jito NCN staking rewards, Switchboard protocol transaction fees, and additional incentives specifically designed for maintaining optimal uptime and data accuracy within the Switchboard network.
* **Dedicated Technical Support:** Receive direct, prioritised support from the core Switchboard contributors, including access to detailed best practice guides and rapid troubleshooting assistance.
* **Active Governance Role:** Directly shape the future of the Switchboard protocol by actively participating in network proposals, protocol improvement discussions, and community governance initiatives.
* **Exclusive Ecosystem Opportunities:** Unlock collaborative partnerships with leading DeFi protocols, innovative enterprises, and cutting-edge blockchain projects that are actively utilising Switchboard oracle services. You will also be promoted on the Switchboard website and documentation.

### How to join the Node Partner Program

If you meet the [Program Requirements](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/prerequisites), we encourage you to submit an [application](https://forms.gle/9EQS2Mo3EFBi8fGq8), providing details about your operational experience.

Applications are reviewed on a rolling basis. Successful applicants will be invited to onboarding sessions.


# The Switchboard NCN

## How does Switchboard use (re)Staking to secure the Switchboard network?

Switchboard oracles are a scarce and valuable resource. To protect the network from abuse (e.g., spam, denial-of-service attempts), and to maximize oracle performance, the network uses a staking-based mechanism powered by the Jito NCN framework.

### Incentivizing Oracle Performance with Stake

Although Switchboard oracles operate within secure Trusted Execution Environments (TEEs), they are still subject to real-world limitations like CPU load and network throughput. To incentivize optimal uptime, responsiveness, and resource management, Switchboard integrates with Jito’s staking vaults.

Stakers can delegate their stake to oracles. High-performing oracles will receive more stake over time, creating a reward feedback loop. Oracles with the most stake are prioritized in routing for new price requests, ensuring that the most performant nodes serve critical traffic.

### Managing Usage Limits with Stake

To prevent misuse of the network (e.g., users flooding price requests), Switchboard enforces a default rate limit of 20 requests per second (RPS) per user.

Users can raise this limit by staking $SWTCH tokens. When submitting a request, users can sign it with a wallet that holds stake, proving their economic commitment to the network. The more stake held, the higher the RPS limit granted to that user. This ties resource consumption directly to network value.

### Summary

By integrating staking into both **supply (oracles)** and **demand (users)**, Switchboard ensures a secure, performant, and economically aligned oracle network.

<figure><img src="/files/V5mCmEJpiH9FltK4HQ9w" alt=""><figcaption></figcaption></figure>


# Running a Switchboard Oracle

All [data feeds](/custom-feeds/build-and-deploy-feed) within the Switchboard protocol are sourced from a network of oracles that validate and submit the raw data required to update a data feed. Interested in becoming part of this network by running your own oracle? You're in the right place.

This guide provides step-by-step instructions for setting up and managing your Oracle on Switchboard. It covers everything from initial configuration to ongoing maintenance.

[Get started](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/prerequisites)


# Prerequisites

This guide is aimed at Operators that want to host their own Switchboard Oracle.

Below is a short list of your requirements. If you need more explanations about each component, please read the next pages to dive deeper into each aspect.

At a high level, you'll need a:

* Bare metal server equipped with an AMD EPYC CPU with AMD SEV SNP support enabled
* Copy of the Switchboard [infra-external](https://github.com/switchboard-xyz/infra-external) repo cloned on the target machine that will host the Oracle

Please review the following resources for more specific requirements:

[Linux, Containers and Self-hosting Knowledge](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/prerequisites/knowledge-about-linux-containers-and-self-hosting)

[Hardware Requirements](broken://pages/3ubmDpBWeei15PEdIXdI)

[Software Requirements](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/prerequisites/software-requirements)

[Network Requirements](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/prerequisites/network-requirements)\
\
[Tested Hosting Providers](/how-it-works/switchboard-protocol/running-a-switchboard-oracle/hardware-tested-providers-and-setup)


# Knowledge about Linux, containers and Self-Hosting

You know Linux right.. right?!?

## About self-hosting and ...

Throughout this manual we assume a good grasp and knowledge of basic networking concepts, Linux system administration and cloud hosting, especially via containers and a basic understanding of Kubernetes.

During the installation process multiple of the following tools will be used:

* Linux (Ubuntu 24.04)
* ctr (containerd)
* git
* ssh
* Solana CLI tool
* Switchboard CLI tool

## ... definitely some Kubernetes? :cloud:

Our deployment platform of choice is Kubernetes on bare metal via \`k3s\`, so you'll also use the following tools:

* Kubernetes (via k3s or Azure/AKS)
* helm
* kubectl
* (optional) k9s

## ... definitely some TEE (AMD SEV SNP)! :unlock:

In order to enable **Trusted Computing** via **AMD SEV** SNP (more on this in the next section) a minimal knowledge about navigating a server/computer motherboard BIOS will also be needed to configure your system to enable the needed features.


# Hardware Requirements and AMD SEV SNP

What is a TEE and why do Switchboard Oracles need it?

Switchboard Oracles code uses a security feature called a [TEE (Trusted Execution Environment)](/how-it-works/technical-architecture/trusted-execution-environments-tees) to ensure that the code and data in transit is safe and secure, even from the Oracle Operators themselves.

To achieve this solution, a server that supports TEE via [AMD SEV SNP](https://en.wikipedia.org/wiki/Secure_Encrypted_Virtualization).

### AMD SEV SNP on AMD EPYC CPUs

In order for AMD SEV SNP to be enabled, you'll have to get a CPU and motherboard that supports it and ensure AMD SEV SNP is enabled in BIOS.\
You'll need an AMD EPYC processors that is part of family 7xx3, 7xx4, 9xx3 or 9xx4 series (or newer) with AMD SEV SNP support.

We specifically successfully tested with 7413 and 7313 CPUs.

Check the following link for a complete list [AMD SEV CPUs list in PDF](https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/tuning-guides/58207-using-sev-with-amd-epyc-processors.pdf).

To sum it up, we use the AMD SEV SNP set of technologies as a TEE platform to encrypt virtual machines memory and isolate them to protect against unauthorized access, even from the hypervisor. However, it's fundamental to keep your BIOS and firmware updated for optimal security and performance. For validated providers and specific setup instructions, refer to later sections.

While not technically mandatory, if possible ensure to **disable hyperthreading (SMT)** as it is a potential security issue in a number of cases when working with TEEs.

We identified a set of trusted providers that we know works well with AMD SEV SNP and our own code, you can find a list later in the manual.

### How to enable AMD SEV SNP in MOST BIOS

Connect to your system BIOS and then be sure to change the following settings:

#### AMD CBS → CPU Common Options OR Advanced → CPU Configuration

* SVM Mode: Enabled
* SMEE: Enabled

#### AMD CBS → CPU Common Options

* SEV-ES ASID Count: 509 ASIDs
* SEV-ES ASID Space Limit Control: Manual
* SEV-ES ASID Space Limit: 32 (or more)
* SEV Control: Enabled
* SNP Memory (RMP Table) Coverage: Enabled

#### AMD CBS → CPU Common Options

Performance OR CCD/Core/Thread Enablement

* SMT (Multithreading): Disabled

#### AMD CBS → NBIO Common Options

* SEV-SNP Support : Enabled (***NOT Auto***)


# Software Requirements

All the software you may need

In order to maximize reliability and minimize the maintenance effort from an Operator perspective, we're targeting a specific set of technologies that will ease the deployment and routine operations.

Our main software stack revolves around ***Kubernetes***.

Our stack is set to run on bare-metal and our manual will assume that you're running ***Ubuntu 24.04**.*

To host your Oracle on bare-metal using Kubernetes, we provide an easy way to create a one node [k3s](https://k3s.io/) cluster, by just following our step-by-step instructions below, which can be later expanded into a multi-node cluster for maximizing reliability and scalability.

When working with your Kubernetes cluster, these instructions will use a set of standard common tools like `kubectl` and `helm` to manage installation of required application and the Oracle code itself.


# Network Requirements

Networking, IPv4, IPv6, possibly DNS and more fun stuff!

### Intro

Our network setup has minimal requirements:

* 1 public static IPv4 pointing at your server
* port 80 and 443 open and available on the IPv4
* all the usual network requirements needed for Kubernetes
* a DNS record pointing at your IPv4 address (more on this later)

That's it.\
\
IPv6 is supported, just not included in this guide. We plan to include it in this manual in future.

### Custom DNS or use our automagic one: xip.switchboard-oracles.xyz

In our setup, we use DNS to provide a TLS certificate to encrypt communication.

While you can use your custom DNS record, that points to you IPv4 address, in order to avoid unnecessary overload, we introduced the use of SSLIP-based services to achieve the same result.

The way it works is simple: you just craft a DNS name based on your IPv4 and add `xip.switchboard-oracles.xyz` ... as an example if you need to resolve `127.0.0.1` just use:

```
127.0.0.1.xip.switchboard-oracles.xyz
```

this DNS record will always resolve to the IPv4 `127.0.0.1` while also providing a valid DNS result.

This setup works perfectly for our setup and doesn't require you to buy and maintain a set of DNS records just for the Oracle setup.

### Bring you own DNS name

If you decide to go with the solution of using your own custom DNS, before moving forward be sure to create a DNS record that points to your server public IPv4 ( and IPv6 if you have one ) address and verify that it is propagated correctly.

### Firewall and traffic

As mentioned above, at the very minimum your server will need to answer incoming requests on port 80 and 443 for the Oracle to be working correctly.

For outgoing traffic, just allow all connectivity as many network ports are involved in the communication especially in the higher range (>= 30K).


# Hardware: tested providers and setup

Switchboard Oracles are designed to provide secure and reliable data within various hardware and provider environments.

The successful operation of these systems hinges on compatibility and performance across multiple platforms.

This section details the specific hardware and technologies that have been rigorously tested to meet these standards.\
**AMD SEV SNP** forms the backbone of the secure computing environments leveraged by Switchboard Oracles.

The following subsections will list the hardware providers and setup that we validated and tested to be working with Switchboard Oracles.

More will be added as we get to validate them first hand.


# OVH

### Links

Website:

* [https://www.ovh.com/](https://www.ovhcloud.com/en/bare-metal/) (for European customers)
* [https://us.ovhcloud.com](https://us.ovhcloud.com/bare-metal/) (for US customers)

Product types supporting AMD SEV SNP:

* Bare Metal - any offering with AMD EPYC 7313 and 7413

### Process to enable AMD SEV SNP

To enable AMD SEV SNP, simply log into your web control panel and you should see a toggle option that allows to setup the IPMI/KVM connection and log into the BIOS.

From here, be sure to enable/change all the appropriate settings as explained in AMD SEV SNP section.


# Platform: Kubernetes + AMD SEV SNP


# Bare Metal with Kubernetes (K3s)

The power of Kubernetes on bare metal and VM

If your team has knowledge about Kubernetes, you should be already aware of the many advantages and power that it provides (scalability, flexibility, great tooling, etc..) so we won't go too deep into those aspects here.

Running Kubernetes on your own hardware provides its own challenges but we found that with just a few hints and settings it's easy to run our Oracle code with no huge effort, provided you have basic Kubernetes management knowledge.

During our tests, we found that the easiest and most flexible solution to run Kubernetes on bare metal is K3S (<https://k3s.io>) and that is what we're using in our instructions but everything should work as well in any other Kubernetes distribution, provided you're able to use `helm` and `kubectl` to control it and the underlying hardware supports AMD SEV SNP.

WARNING: At the moment our instructions are written for a k3s cluster of only one node, but we plan to soon extend it to be able to fully support a multi node cluster as we have Oracles running correctly in this configuration which takes full advantage of Kubernetes HA and scalability offering.


# The Git Repo: Clone Our Code

GitHub repo setup

When thinking about the installation we wanted to create a process that was as easy and streamlined as possible.

This is why we create a public repository called `infra-external` that contains all the scripts needed to setup our Oracles from scratch, simply by following scripts step-by-step in order.

In the next sections you'll learn everything about it.


# Repo Structure

Our infra-external repo intro and structure

First step is then to clone our `infra-external` github repo locally and `cd` into it:

```bash
git clone https://github.com/switchboard-xyz/infra-external
cd infra-external
```

This repository structure is divided into three main areas:

```bash
infra-external/
│
├── README.md
│
├── cfg/
│   │
│   ├── 00-common-vars.cfg
│   ├── 00-devnet-vars.cfg
│   └── 00-mainnet-vars.cfg
│
├── data/
│   │
│   ├── devnet_payer.json
│   └── mainnet_payer.json
│
└── oracle/
    │
    └── bare-metal/
        └── kubernetes/
            └── ... scripts ...

```

These areas serve as follows:

* `cfg/`: Contains configuration files for two environments, `devnet` and `mainnet` plus a shared one called `common` that contains shared variables that apply to both.\
  You may want to backup this directory at least once or every time you change its content.
* `data/`: Holds essential data files like `devnet_payer.json` and `mainnet_payer.json` needed for setting up the Oracles. May be populated by other directories after setup.\
  You may want to backup this directory at least once or every time you change its content.
* `oracle/`: Includes scripts for setting up Oracles.\
  This directory can be ignored during backups and will change as we update our process.
* `README.md`: it's the README file for the repo, in Markdown format.

Continue to the next section where we'll take care of the configuring files.


# Configuration: Tweaking Configurations

One process to rule them all

In the previous step we cloned locally the `infra-external` repo, but we're now focusing specifically on this section of the repo:

```bash
infra-external/
│
└── cfg/
    │
    ├── 00-common-vars.cfg
    ├── 00-devnet-vars.cfg
    └── 00-mainnet-vars.cfg

```

You will only have to edit the `00-common-vars.cfg` file and the one targeting the Solana cluster you want your Oracle to work on.

`devnet` is a good place to start and get acquainted with how our Oracle code works but then you can easily reproduce the same setup on `mainnet` at a later moment.

Let's focus on one file at a time.


# cfg/00-common-vars.cfg

common vars

We'll now go through the file content, one chuck and variable at a time:

```bash
# customize the data below according to your setup
EMAIL="YOUR@EMAIL.IS.NEEDED.HERE" # change this to be your E-MAIL
```

This variable should simply contain a reference email that will be used for certificates and future reference.

```bash
IPv4="0.0.0.0" # add the external IPv4 address of your cluster
IPv6="0000::0000" # add the external IPv6 address of your cluster (optional)
```

This are the PUBLIC STATIC IPs associated with your Oracle.\
\
Right now we're only using IPv4 but we plan to include soon support for IPv6 too, so leave IP6 at "0000::0000: for now.

```bash
CLUSTER_DOMAIN="${IPv4}.xip.switchboard-oracles.xyz"
```

This is a DNS record that points to the IP specified above.\
If you don't have a DNS record, you can leave it as written and it will automatically resolve to your IP address.

```bash
# this URL should always be a MAINNET non rate-limited RPC (even when using devnet)
TASK_RUNNER_SOLANA_RPC="https://api.mainnet-beta.solana.com"
```

Regardless of installing a `devnet` or `mainnet` Oracle you should add here your own RPC pool for `mainnet` that is NOT rate limited or you may find that some Oracle operations will have a hard time going through.

You're free to use the one we specify there, but it's quite possible that your Oracle will be frequently heavy rate limited in the requests it sends to that RPC.

Toward the end of the file, you'll see a comment similar to:

```
##########
!!! - DO NOT CHANGE ANYTHING BELOW THIS POINT - !!!
##########
```

Unless you know what you're doing or you're in direct contact with a member of the Switchboard team, NEVER change anything below this line.


# cfg/00-devnet-vars.cfg and cfg/00-mainnet-vars.cfg

devnet and mainnet variables

### Intro

The two following files are basically identical, besides some small details that makes them dedicated to specific SOLANA clusters (or chains), that is `devnet` and `mainnet`(sometimes also referred to as `mainnet-beta` for historical reasons).

Let's go through the content, chunk by chunk.

### Network definition

```bash
# ADD YOUR DEVNET ORACLE/GUARDIAN DATA

# NETWORK can be "devnet" or "mainnet"
NETWORK="devnet"
```

This just refers to the SOLANA cluster you're settings this up for. Could be `devnet` or `mainnet`.

### Oracle pubkeys

```bash
# ORACLE DATA
PULL_ORACLE=""
PULL_QUEUE=""

# GUARDIAN DATA
GUARDIAN_ORACLE=""
GUARDIAN_QUEUE=""
```

These variables are the public keys of the accounts you will use to host your Oracle and will be created in a following step, later in the installation process.

Leave them empty for now and you will come back later to this section when the setup script that creates them will tell you to do so.

### RPC URLs

```
# RPC endpoints - Add your NON-rate-limited RPC endpoints
RPC_URL="https://api.${NETWORK}.solana.com"
WSS_URL="wss://api.${NETWORK}.solana.com"
```

These variables are referring to SOLANA RPC URLs and once again we invite you to find one that is not rate limited from a trusted provider like <https://triton.one/> or <https://www.helius.dev/> or other similar solutions.

These two endpoints must be dedicated to the network you're using, ie: devnet or mainnet.

### Infisical settings

```bash
# CHANGE ONLY IF YOU USE INFISICAL
#INFISICAL_SECRET_KEY="SOLANA_KEY"
#INFISICAL_SECRET_PATH="/"
#INFISICAL_SECRET_SLUG="dev" # usually "dev" or "prod"
#INFISICAL_TOKEN_NS="infisical" # the K8S namespace where the Infisical TOKEN lives
```

This section is entirely dedicated to Infisical and is related to secrets management and can be used with Kubernetes based setups. It's not mandatory but we use in some of our installations successfully.

### Namespace and Ingresses

```bash
##########
### !!! - DO NOT CHANGE ANYTHING BELOW THIS POINT - !!!
##########

GUARDIAN_ENABLED=false

NAMESPACE="switchboard-oracle-${NETWORK}"

# INGRESS gateway
ORACLE_INGRESS="https://${CLUSTER_DOMAIN}/devnet"
GATEWAY_INGRESS="https://${CLUSTER_DOMAIN}/devnet"
GUARDIAN_INGRESS="https://${CLUSTER_DOMAIN}/devnet"

DOCKER_IMAGE_TAG="${DEVNET_DOCKER_IMAGE_TAG}"
```

This section is mostly meant to stay untouched unless you know what you're changing.\
\
The variable `GUARDIAN_ENABLED` is an internal variable, if you don't know what its use is, you probably don't need it :sunglasses:

If you changed the `CLUSTER_DOMAIN` variable in the `00-common-vars.cfg` file, then you MAY also need to adjust the `*_INGRESS` variables accordingly, remember to leave the `/devnet` and `/mainnet` string at the end to be able to host multiple Oracles.


# Installation: Setup Via Scripts

Set a solid foundation

In this part of the setup we decided to create dedicated sections, each per different "platform" that we envisioned you could be using.

We are now referring to this part of the repository:

```bash
infra-external/
│
└── oracle/
    │
    └── bare-metal/
        └── kubernetes/
            └── ... scripts ...
```

Many of the steps are similar between different platforms, others are specific to only one of them, but in general you should be able to follow the scripts that are named in a way that makes it easy to follow them in numerical order, ie:

```bash
00-kernel-install.sh # absolutely FIRST step
# [...]
30-k3s-install.sh # then this needs to be run before ...
31-k3s-sail-setup.sh # ... this script but ...
# [...]
70-k8s-apps-cert-manager.sh # ... this happens even 
                            # later in the setup!
# [...]
90-k8s-oracle-install.sh # this is usually 
                         # the last step you'll run
```

So you just have to `cd` into the directory dedicated to your chosen platform and start following the scripts one by one, in order.

Read the output and watch carefully for specific instructions from the script themselves, though usually they will be VERY visible and highlighted like:

```
===================================================
=                !!! IMPORTANT !!!                =
=  COPY/SAVE THE OUTPUT ABOVE, BEFORE PROCEEDING  =
=  THEN TYPE 'exit' TO LEAVE THIS TMP CONTAINER.  =
===================================================
```

Should be self-explanatory enough :grimacing: so you should just be able to follow scripts in numerical order.


# Bare Metal with Kubernetes (K3s) + AMD SEV SNP

Your server, your cloud, your data...

### Initial setup steps

First of all, let's move to the proper directory:

```bash
cd install/bare-metal/kubernetes
├── 00-kernel-install.sh
├── 01-helm-install.sh
├── 02-snphost-install.sh
├── 30-k3s-install.sh
├── 31-k3s-sail-setup.sh
├── 40-oracle-ctr-sol.sh
├── 41-oracle-create-sol-account.sh
├── 50-oracle-ctr-sb.sh
├── 51-oracle-prepare-request.sh
├── 52-oracle-ncn-enroll.sh
├── 70-k8s-apps-cert-manager.sh
├── 71-k8s-apps-ingress-nginx.sh
├── 72-k8s-apps-watchtower.sh
├── 73-k8s-apps-vmagent.sh
├── 74-k8s-apps-logs.sh
├── 79-k8s-apps-infisical.sh
├── 80-test-cert-setup.sh
├── 81-test-cert-cleanup.sh
├── 90-k8s-oracle-install.sh
└── 91-k8s-ctr-cleanup.sh
```

From here, we can start running all the scripts, step by step, using the first two chars in the filename as a numerical order, starting from the smallest and going in ascending order.

### Step by step installation

```shell
./00-kernel-install.sh
```

This step will download and install a custom version of the Linux kernel, patched by AMD engineers to support SNP correctly.

Remember to reboot and verify that your system is then running kernel version `6.8.0-rc5-next-20240221-snp-host-cc2568386`.

Reboot done? good! If you haven't enable AMD SEV SNP in your BIOS now is a good time to do so.\
\
Now you can proceed with:

```shell
./01-helm-install.sh
```

This step will install a few utility tools like helm and k9s to interact with Kubernetes in amore efficient way.

```shell
./02-snphost-install.sh
```

This will install a small utility called `snphost` that can be used effectively by running:

```
snphost ok
```

anywhere in your system to run all the necessary AMD SEV SNP checks. You will get a list of checks that should all report `PASS` .

If that's not the case, you probably forgot to change/enable some of the needed settings in BIOS.

To proceed, let's start by installing Kubernetes with `k3s` using the following step:

```bash
./30-k3s-install.sh
```

This will just download and install `k3s` and start it.

After Kubernetes settles (you can check by connecting via `k9s` or `kubectl`) you can proceed by running next step:

```
31-k3s-sail-setup.sh
```

which will download our custom components and set `k3s` up to use them.

### **Creating a payer.json Solana Account**

In this phase of the setup you're going to enter a temporary environment and create the Solana Account used by your Oracle. If you don't save the output when suggested to, once you'll leave this temporary container it will be really hard (if not impossible) to retrieve the content and thus the account you created. So please take time to read carefully instructions as you go through each step.

Let's start with:

```shell
./40-oracle-ctr-sol.sh
```

This step will drop you in a temporary container that will have all the necessary tools to run the following step:

```shell
# only choose the one that applies to your setup
./41-oracle-create-sol-account.sh # uses devnet by default
./41-oracle-create-sol-account.sh devnet  # equivalent to above
./41-oracle-create-sol-account.sh mainnet # run this for mainnet
```

This step will create a new account on the Solana network that will be used by your Oracle and save it in the `data` directory, in the respective `devnet` and `mainnet` files.\
By default this script will crate a `devnet` account, so you want to create one for `mainnet` you have to call by adding `mainnet` at the end as shown above.\
Once done with the steps above, you can leave the container by typing `exit` and will be dropped back to the `docker` installation directory.

### Create a request to register your Oracle and Guardian to Switchboard queue

Now that you have a Solana account that can be used by your Oracle, you can send a request to be allowed to cooperate to the Switchboard network by contributing to tasks on a specific queue.

To do so, we have another special container that will make your life easy. To enter it just type:

```shell
./50-oracle-ctr-sb.sh
```

This will bring you in a temporary container that has our [Switchboard CLI tool](https://www.npmjs.com/package/@switchboard-xyz/cli/) available and is ready to send your request to be allow to contribute to the Switchboard network.

To send your request simply run:

```shell
# only choose the one that applies to your setup
./51-oracle-prepare-request.sh # uses devnet by default
./51-oracle-prepare-request.sh devnet  # equivalent to above
./51-oracle-prepare-request.sh mainnet # run this for mainnet
```

You will be prompted if you intend to also run a `Guardian`. Answer `no` unless you know what it is :sunglasses:.

Save the output of the command above and follow the link provided to send your request. Our operators will receive your request and provide you permission to be included in the queue as soon as possible.

Once done with the steps above, you can leave the container by typing `exit` and will be dropped back to the `docker` installation directory.

Save values from the output in the file dedicated to `devnet` or `mainnet` inside the `cfg` directory, based on your current setup.

#### \[RECOMMENDED] Enrolling Your Oracle in a Node Consensus Network (NCN)

After your Oracle has been granted permissions to participate in the Switchboard network, you can optionally enroll it in a Node Consensus Network (NCN) to participate in the restaking ecosystem.\
\
\&#xNAN;***This allows your Oracle to earn additional rewards while contributing to network security.***

To enroll your Oracle in an NCN, run the following command from the same temporary container you used for the previous steps:

```bash
# Make sure you're in the temporary container first by running:
./50-oracle-ctr-sb.sh

# Then run the NCN enrollment script (inside the container)
# only choose the one that applies to your setup
./52-oracle-ncn-enroll.sh # uses devnet by default
./52-oracle-ncn-enroll.sh devnet  # equivalent to above
./52-oracle-ncn-enroll.sh mainnet # run this for mainnet
```

This script will:

1. Check if you already have an NCN operator account or create a new one if needed
2. Link your Switchboard Oracle to your NCN operator
3. Initialize and prepare your operator for vault interactions
4. Guide you through the enrollment process with clear instructions

**What to Expect During Enrollment**

The script will display important information about your enrollment, including:

* Solana cluster (devnet or mainnet)
* NCN address
* Vault address
* NCN operator address
* Oracle operator address

**Important:** You must save this information when prompted. The script will display several notices with boxes around them - these contain critical information you'll need.

**Two-Phase Enrollment Process**

The NCN enrollment is a two-phase process:

1. **Initial Setup**: The script will execute the first set of transactions to link your Oracle to the NCN operator and initialize vault permissions.
2. **Completion Phase**: After Switchboard confirms your enrollment on their side, you'll need to return to the temporary container and run the final command displayed by the script to complete the process.

**Different Authority Scenarios**

If your Oracle keypair authority is different from your NCN operator admin, the script will detect this and provide you with specific instructions for completing the enrollment using separate keypairs.

After completing the enrollment process, your Oracle will be fully integrated with the NCN ecosystem, allowing you to participate in restaking and earn additional rewards.

Remember to exit the temporary container when you're done by typing `exit`.

### Install Kubernetes (with k3s) and all needed apps

For the following steps, you should be able to run them in order with no particular change. Just give each step 30-60 seconds to settle before proceeding to the next one:

```bash
./70-k8s-apps-cert-manager.sh
./71-k8s-apps-ingress-nginx.sh
```

The first the TLS certificate manager needed to create the HTTPs certificate that runs the reverse proxy in front of your gateway component.

Next you should install our Ingress toolset based on `nginx`:

```sh
./71-k8s-apps-ingress-nginx.sh bare-metal # deploys nginx
```

This will install nginx ingress and enable it.

### \[RECOMMENDED] Enable watchtower auto-update mechanism

To make maintenance and regular updates easier for our partners we propose a mechanism based on `watchtower`.

This software will monitor our repos automatically for you and pull and deploy newer versions of our Oracle automatically without any intervention on your side.

If you want to enable this feature, please run:

```
./72-k8s-apps-watchtower.sh
```

You can always disable it by removing it via `helm`.

If you don't use watchtower, please note that old Oracles that are not up-to-date will be excluded from running tasks in our queues.

### \[OPTIONAL] Enable metrics reporting and monitoring

While the following step is optional, we recommend running it as this will send statistics about your Oracle to our systems so that we can keep an eye on anomalies or outliers behaviors and warn you promptly if we detect any and keep our network safe:

```bash
# only choose the one that applies to your setup - optional step
./73-k8s-apps-vmagent.sh # uses devnet by default
./73-k8s-apps-vmagent.sh devnet  # equivalent to above
./73-k8s-apps-vmagent.sh mainnet # run this for mainnet
```

### \[OPTIONAL] Enable logs reporting for debugging purpose during support reqs

This step will enable sending all logs from your deployment to our central loggging aggregation system.

To do so we'll have to provide you a `username`and `password` to use in the command below.

This is a set of ephemeral credentials that will only work for the time need for the support request and will be deactivated afterwards.

After getting credentials from us please run:

```bash
./74-k8s-apps-logs.sh "my_username" "my_password"
```

remember to run the following command to clean up the logs forward installation after the support phase is completed:

```
helm uninstall -n sb-log-forwarding sb-log-forwarding 
```

and verify that the installation is gone.

### \[OPTIONAL] Secrets management via Infisical

Next is another optional step:

```bash
# only choose the one that applies to your setup - optional step
./79-k8s-apps-infisical.sh # uses devnet by default
./79-k8s-apps-infisical.sh devnet  # equivalent to above
./79-k8s-apps-infisical.sh mainnet # run this for mainnet
```

This will install all the needed artifacts and code for our integration with [Infisical](https://infisical.com/).\
This step is optional and needs to be completed by the data present in your `cfg` file with all the variables starting with `INFISICAL_`.

### \[OPTIONAL] TLS certificate creation test

Another optional step:

```bash
# only choose the one that applies to your setup - optional step
./80-test-cert-setup.sh # uses devnet by default
./80-test-cert-setup.sh devnet  # equivalent to above
./80-test-cert-setup.sh mainnet # run this for mainnet
```

This script will create an Ingress that will test your Kubernetes installation, DNS setup and the entire flow.

To verify that it's working, run the script above, give it 3-5 minutes and then visit the DNS record you decided to use for your system.

When done, please run:

```bash
# only choose the one that applies to your setup - optional step
./81-test-cert-cleanup.sh # uses devnet by default
./81-test-cert-cleanup.sh devnet  # equivalent to above
./81-test-cert-cleanup.sh mainnet # run this for mainnet
```

to clean up the artifacts that the test created.

### Finally start your Oracle!

If everything went well, it's now just a matter of running:

```bash
# only choose the one that applies to your setup
./90-k8s-oracle-install.sh # uses devnet by default
./90-k8s-oracle-install.sh devnet  # equivalent to above
./90-k8s-oracle-install.sh mainnet # run this for mainnet
```

So that the last step will install our Oracle code and run it in your Kubernetes cluster.

From this point onward, you can use the usual Kubernetes tools that you use to work with your cluster.

### Troubleshooting

#### Oracle not starting after reboot

Sometimes after a serve reboot, your Oracle containers may refuse to start and give back an error saying something like:

```
Error: failed to create containerd container: create instance 105: object with key "105" already exists: unknown
```

In this case, just run the step:

```
./91-k8s-ctr-cleanup.sh
```

and delete the Kubernetes PODs so that they will be recreated correctly.


# Enable Staking to your Oracle

Enable svSWTCH delegation to your oracle through Jito NCN integration

Setting up your oracle to start earning SWTCH rewards is easy. In less than 5 minutes, Jito vaults can start delegating stake to your oracle to start earning!

## Overview

Oracle operators in the Switchboard network **must have svSWTCH delegated to them** to participate. This "skin in the game" model ensures oracle incentives are aligned with network security and data reliability.

**Benefits of enabling staking:**

* 🏆 **Earn SWTCH rewards** from network fees and subsidies
* 📈 **Competitive advantage** - more delegation = more work opportunities
* 🔒 **Network participation** - required to validate and submit oracle data
* ⚡ **Performance rewards** - high uptime and accuracy earn bonus rewards

For complete details on the economic model, see [Governance & Tokenomics](/governance-and-tokenomics/governance-and-tokenomics).

To start earning, you must first create your NCN operator or provide a pre-existing operator. This operator account is what manages the permissions to allow stake to be delegated to your oracle from a Jito vault.

Prerequisites:

To get started with re-staking, you must have already installed the Jito restaking CLI:

```bash
curl -fsSL -o jito-restaking-0.0.4.tar.gz https://github.com/jito-foundation/restaking/archive/refs/tags/v0.0.4.tar.gz
tar -xvzf jito-restaking-0.0.4.tar.gz
rm jito-restaking-0.0.4.tar.gz
cd restaking-0.0.4/cli/
cargo build --release
mkdir -p ~/.local/bin
cp ../target/release/jito-restaking-cli ~/.local/bin/
```

To create an NCN operator account, run

```bash
jito-restaking-cli restaking operator initialize ${OPERATOR_FEE_BPS?} --rpc-url ${URL?} --keypair ${KP?}
```

Note that the operator fee is not a setting that has any effect in the NCN protocol. This configuration acts as an indicator of what amount of rewards will go directly to your oracle instead of being sent to the vault. Do note that a higher fee de-incentivizes the vault to provide you stake.

Once your operator is set up, you must link it to the Switchboard NCN:

```bash
export NCN=BGTtt2wdTdhLyFQwSGbNriLZiCxXKBbm29bDvYZ4jD6G
jito-restaking-cli restaking ncn initialize-ncn-operator-state ${NCN?} ${OPERATOR?} --keypair ${KP?} --rpc-url ${URL?}
jito-restaking-cli restaking operator operator-warmup-ncn ${OPERATOR?} ${NCN?} --rpc-url ${URL?} --keypair ${KP?}
```

At this point, your operator must be approved by Switchboard to continue, you may reach out via the application form or via the `#sb-operators`discord channel for Switchboard to run

```bash
jito-restaking-cli restaking ncn ncn-warmup-operator ${NCN?} ${OPERATOR?} --rpc-url ${URL?} --keypair ${KP?}
```

At this point your operator will be registered with the Switchboard NCN!

Now, it's time to onboard to our supported **Vaults**

Switchboard currently supports stake delegation via the [Fragmetric vault](https://fragmetric.xyz/)

The Fragmetric vault may be found at address `HR1ANmDHjaEhknvsTaK48M5xZtbBiwNdXM5NTiWhAb4S`

To onboard your operator to the Fragmetric vault, run

```bash
jito-restaking-cli restaking operator inititalize-operator-vault-ticket ${OPERATOR?} ${VAULT?} --rpc-url ${URL?} --keypair ${KP?}
jito-restaking-cli restaking operator warmup-operator-vault-ticket ${OPERATOR?} ${VAULT?} --rpc-url ${URL?} --keypair ${KP?}
```

And you are all set! Fragmetric may now delegates stake to your node at the vault's discretion.

May the odds forever be in your favor...

<figure><img src="/files/fvoJDKNv1Ouu7fnzkRQK" alt=""><figcaption></figcaption></figure>


# Providing stake to Switchboard

How to stake and earn SWTCH rewards through Switchboard's Jito NCN integration

Providing stake to Switchboard is simple and rewarding! By staking supported assets, you help secure the oracle network while earning SWTCH governance tokens.

## Overview

Switchboard uses [Jito's Node Consensus Network (NCN)](https://www.jito.network/restaking/ncns/) for its restaking infrastructure. This allows you to stake various LSTs (Liquid Staking Tokens) to secure the Switchboard oracle network and earn SWTCH rewards in return.

**Key Benefits:**

* 🏆 **Earn SWTCH**: Receive governance tokens for network participation
* 🔒 **Network Security**: Help secure over $5B in DeFi value
* 🌐 **Multi-Asset Support**: Stake SOL or various LSTs
* ⚡ **Regular Rewards**: Distributed every Solana epoch (3 days)

To start securing the Switchboard network, you may contribute to the Fragmetric vault for stake to be distributed to oracle operators.

To start staking visit the Fragmetric vault page on the Jito Vault website:

[https://www.jito.network/restaking/vaults/HR1ANmDHjaEhknvsTaK48M5xZtbBiwNdXM5NTiWhAb4S](https://www.jito.network/restaking/vaults/HR1ANmDHjaEhknvsTaK48M5xZtbBiwNdXM5NTiWhAb4S/)

From there you can see the:

* Vault Address
* VRT Mint
* Vault Authority
* Vault website: [https://app.fragmetric.xyz/restake/fragsol](https://app.fragmetric.xyz/restake/fragsol/)

Navigate to the Fragmetric application and stake SOL or any of the fragSOL supported LSTs to start delegating to Switchboard oracles and start earning SWTCH

See example at <https://screen.studio/share/MzI1bXNL>

## What can I stake?

With Jito (re)-staking built in, you can stake any assets your vault supports.

For the Fragmetric Vault, these assets are currently:

* SOL
* JitoSOL
* BNSOL
* mSOL

## How do I earn $SWTCH?

SWTCH is rewarded directly to the vault to be claimable by stakers.

These awards will be distributed every Solana epoch (3 days) and will be viewable on the Fragmetric vault rewards page: <https://app.fragmetric.xyz/rewards/overview/>

<figure><img src="/files/Lev7APWvX0s7jrsiOmIR" alt=""><figcaption></figcaption></figure>

## How Much SWTCH Do I Earn?

The amount to distribute is governed by the Switchboard DAO through [svSWTCH governance](/governance-and-tokenomics/governance-and-tokenomics), with the initial year set to distribute 8% of supply to stakers.

**Reward Calculation:**

* Based on your proportional share of total staked assets
* Distributed from protocol fees and SWTCH subsidies
* Higher oracle performance = higher rewards for delegators

For complete details on tokenomics and reward distribution, see the [SWTCH Token Overview](/governance-and-tokenomics/swtch-token-overview) and [Governance & Tokenomics](/governance-and-tokenomics/governance-and-tokenomics) guides.

## How can I unstake?

The unstaking process is controlled by each vault added to the Switchboard NCN.

For the Fragmetric vault, visit [https://app.fragmetric.xyz/unstake](https://app.fragmetric.xyz/unstake/) to initiate the unstake. The unstaking process, like the Solana unstaking process, will take until next epoch to take effect.


# Technical Architecture

This section delves into the architecture and technologies used within Switchboard, detailing the interactions between components and the security measures safeguarding the network's integrity.

Switchboard operates as a decentralised oracle network, delivering reliable and transparent data feeds to blockchain applications. Several core components underpin Switchboard's functionality and security, these include:

* [**Trusted Execution Environments (TEEs)**](/how-it-works/technical-architecture/trusted-execution-environments-tees): Providing secure enclaves for code execution and data integrity.
* [**Oracle Queues**](/how-it-works/technical-architecture/oracle-queues): Managing and securing data feeds through structured environments.
* [**Node Architecture**](/how-it-works/technical-architecture/node-architecture): Distributing tasks across a network of specialised nodes, with Guardians ensuring security through ongoing review and validation of new Oracles and Guardians.


# Trusted Execution Environments (TEEs)

Switchboard enhances its security model with Trusted Execution Environments. Instead of relying solely on the assumption that the majority of oracles are honest ("honest-majority"), we use TEEs for added protection. Switchboard currently uses AMD SEV-SNP (Secure Encrypted Virtualization - Secure Nested Paging) for hardware-backed attestation.

Think of TEEs as secure enclaves where code can run in isolation, protected from the rest of the system. This means:

* **Code Verification:** Switchboard can cryptographically verify that each oracle node acting as a publisher is running *only* the approved and verified code. No rogue modifications allowed.
* **Data Integrity:** This verification process ensures the integrity of the data being provided, as the code responsible for fetching and signing data hasn't been tampered with.

In essence, TEEs provide a hardware-backed guarantee of code integrity, offering a robust defence against malicious actors and further bolstering the reliability of Switchboard's data feeds on-chain.

***

## TEE Applications and Considerations

TEEs are generally overlooked, but they are used by many of the most popular applications that are synonymous with security and safety.

* [Signal app](https://signal.org/blog/private-contact-discovery/) uses TEEs to safeguard its users' messages, guaranteeing they remain secure and private.
* [Azure Cloud](https://techcommunity.microsoft.com/t5/azure-confidential-computing/announcing-microsoft-moves-25-billion-in-credit-card/ba-p/3981180) (Microsoft) leverages TEEs, to ensure top-tier credit card data management and protection, so both Azure and its corporate clients can maintain optimum [PCI compliance](https://www.pcisecuritystandards.org/).
* [1Password](https://blog.1password.com/using-intels-sgx-to-keep-secrets-even-safer/) employs TEEs (across a host of its platforms), adding extra layers of security to a user's passwords.
* [Flashbots](https://writings.flashbots.net/block-building-inside-sgx) relies on TEEs as an integral tool in verifiable block operations, so trust and integrity will be maintained within blockchain operations.

Because TEEs are not perfect and can have undocumented security flaws, Switchboard needs to have a system in place to quickly shut down or upgrade any oracle. To stay on top of this, Switchboard makes all oracles prove they're still trustworthy by re-verifying their certificates and also uses economic incentives to help ensure integrity.


# Oracle Queues

An Oracle Queue (often just called a "Queue") is a core component of Switchboard, designed to manage and secure data feeds by creating a structured and secure environment that facilitates efficient management, isolation, and reliable data delivery.

Think of a Queue as:

* **A Dedicated Subnetwork of the larger Switchboard Protocol:** A whitelisted environment within the Switchboard protocol, controlling which software can be executed and which oracles are authorised to respond in its network.
* **An Oracle Registry:** A list of on-chain oracle accounts, each linked to a physical machine that fetches and publishes data.
* **A Security Boundary**: Oracles within a Queue must run verified code. This ensures that only trusted nodes contribute to the data feeds.
* **A Multi-Chain Entity**: Defined on Solana and synchronised across all Switchboard deployments on different blockchains.

Queues have an important key characteristic:

* Each data feed *must* belong to **one**, and only **one**, Queue.


# Node Architecture

The Switchboard network distributes data processing across various node types, each with specific responsibilities. Understanding these node types is essential for grasping how data requests are handled and secured within the Switchboard architecture. The following table details each node type and its key functions:

| Node Type                    | Role/Function                                                                                                         | Key Features/Responsibilities                                                                                    |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Guardian**                 | Gatekeeper of Data Integrity                                                                                          | Verifies Oracle code integrity, Bridges blockchains and TEE, Initiates TEE verification, Strict approval process |
| **Oracle**                   | Decentralised Access — Acts as a web API for public access                                                            | Segregated internal components for security and efficiency                                                       |
| **Oracle Router — Frontend** | Traffic Controller-Mitigation of DoS threats — Protects the internal environment from Denial of Service (DoS) attacks | Front-end traffic control                                                                                        |
| **Oracle Router — Gateway**  | Task Distributor — Assigned tasks efficiently across workers                                                          | Calculates the best way to assign different tasks with different parameters                                      |
| **Oracle Worker**            | Task Executioner — Runs code for data retrieval and signing                                                           | Executes tasks assigned to worker                                                                                |

## Guardian and Oracle Onboarding

Guardians play a crucial role in the Switchboard network by verifying that oracles and other guardians are running the correct software images. This verification process involves checking their Trusted Execution Environment (TEE) attestations. Once approved, guardians can proceed through the guardian attestation process and act as validators for the network.

**Step 1: Initial Onboarding as Root of TEE Attestation.**

Guardians are first and foremost onboarded into the network as the root validators of TEE attestations. This inaugural step is necessary to establish their pivotal role as the secure bridge between TEE attestation practices and the blockchain itself.

Here is a visual representation of the entire process:

<figure><img src="/files/T52vruggMCpNA0Vo3NZv" alt="Guardian Onboarding Process"><figcaption></figcaption></figure>

Following successful verification, a minimum of one-third of all guardians are required to attest to the TEE attestations of each oracle. This ensures robust validation and security across the Switchboard network.

## Oracle Onboarding

Before any attestation can occur, all oracle nodes must first successfully navigate a pre-approval process. Only then can they formally seek guardian approval. Once an oracle has been both approved and verified that the correct software image is running, said entity gains the ability to join the Oracle Queue.

**Step 2: Guardian Attestation and Addition to Oracle Queue**

The Guardian attests to the oracle's TEE attestation and, upon successful verification, adds the oracle to the Oracle Queue.

<figure><img src="/files/TPzBNUGPZBr68EqGo0wh" alt="Guardian Attestation Process"><figcaption></figcaption></figure>

**Important Keypair Verification Note:** Similar to oracles, guardians must also undergo a keypair verification process, ensuring that all secp256k1 keypairs are considered valid for a period of seven days, after which they must undergo a re-verification.

Following the successful completion of the onboarding procedures for both guardians and oracles, users can then commence the process of requesting price signatures to be used on-chain.

## The Lifespan of a Data Feed Request

Once onboarded, users have the flexibility to define their custom data feeds and solicit updates from oracle nodes within the network. This process ensures that the data returned to the user includes essential data feed outputs, and any signatures required to validate data updates on-chain.

**Step 3: User Request and On-Chain Posting**

The user requests data from a specified feed through the gateway. In response, the user receives a signature-set. The user then posts this signature-set on-chain to update the data.

Users can request up-to-date data from a specified feed through the gateway. Following a response, the user receives a signature set, which is then posted on-chain to update the data.

<figure><img src="/files/xU1DIDVlxbz7V28iB9Is" alt="Data Feed Request Flow"><figcaption></figcaption></figure>


# Crossbar

## Crossbar: Switchboard's Utility Server

Crossbar is a high-performance utility server implemented in Rust, designed to simplify interactions with the Switchboard network. It provides essential functionalities for simulating and resolving feeds across various blockchains. Crossbar comes with a set of useful utility functions for resolving feeds on all chains with active Switchboard deployments, IPFS utilities for storing and fetching jobs, and built-in simulation capabilities for constantly fetching feed updates for liquidators and other bots.

> **Note**: The Rust version includes built-in simulation and no longer requires a separate Task Runner Simulator service.

> Running your own instance of Crossbar is highly recommended for user interfaces and bots that require frequent price simulations.

Refer to [Run Crossbar with Docker Compose ](/tooling/crossbar/run-crossbar-with-docker-compose)for instructions on setting up your own Crossbar instance.

### Key Features

Crossbar aims to streamline the Switchboard experience, offering the following core functionalities:

* **Fetch Feeds by Feed Hash:** Retrieve a feed's job definitions and queue in JSON format using its unique feed hash (content identifier).
* **Store Jobs:** Store feed definitions using your configured IPFS node (requires Piñata credentials or a Kubo node).
* **Simulate Feeds by Feed Hash:** Simulate multiple feeds simultaneously using their feed hashes, enabling off-chain tracking of custom price feeds for bot automation.

> Crossbar exposes both simulation and signed-update paths. Simulation can succeed even when signed updates fail oracle-side validation. For validation units such as raw v2 `maxJobRangePct` and gateway `max_variance`, see [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units).

For current Solana/SVM feed-hash integrations, use the SDK managed update path (`queue.fetchManagedUpdateIxs(...)`) and canonical quote-program accounts. The classic `PullFeed.fetchUpdateIx(...)` and `/updates/solana/...` flows are legacy PullFeed compatibility paths and require queue/gateway support for that account scheme.

### Blockchain-Specific Features

Crossbar provides tailored features for specific blockchains:

**Solana and Aptos/Sui:**

* **Fetch Encoded Update Instructions:** Retrieve update instructions from live oracles. For Solana/SVM feed-hash integrations, prefer managed quote-program updates through the SDK; Crossbar's classic Solana update routes are for legacy PullFeed accounts.
* **Fetch Simulated Results for Feeds:** Fetch current prices for feeds. This is a useful feature for tracking custom price feeds off-chain, for triggering an action that the bots can use.

**Ethereum Virtual Machine (EVM):**

* **Fetch Encoded Updates:** Obtain an encoded update for a feed to submit on-chain via a contract explorer (like Etherscan), eliminating the need to include feed definitions directly in your frontend.
* **Settle Randomness:** Fetch a settlement message for resolving randomness requests when using Switchboard's EVM Randomness features.

### Rust Implementation Benefits

The Rust implementation provides several advantages:

* **High Performance:** Built with actix-web for maximum throughput
* **Built-in Simulation:** No separate Task Runner Simulator required
* **WebSocket Support:** Real-time data streaming capabilities
* **Memory Efficiency:** Optimized for high concurrent connections
* **Simplified Deployment:** Single binary with minimal dependencies

### Environment Variables

All environment variables are optional and have sensible defaults:

**Core Configuration:**

* `PORT` (default: 8080) - HTTP server port
* `WS_PORT` (default: 8081) - WebSocket server port
* `DISABLE_API` (default: false) - Disable HTTP API entirely

**Performance:**

* `BROADCAST_WORKER_THREADS` (default: 32) - Tokio worker threads
* `SIMULATION_CACHE_TTL_SECONDS` (default: 3) - Cache TTL
* `DISABLE_CACHE` (default: false) - Disable caching

**Blockchain RPCs (Recommended):**

* `SOLANA_MAINNET_RPC` - Solana mainnet RPC
* `SOLANA_DEVNET_RPC` - Solana devnet RPC

**IPFS (Optional):**

* `IPFS_GATEWAY_URL` (default: <https://ipfs.io>) - IPFS gateway
* `PINATA_JWT_KEY` - Pinata storage key
* `KUBO_URL` - Local IPFS node

For a complete list of environment variables, see the [Docker Compose guide](/tooling/crossbar/run-crossbar-with-docker-compose#environment-variables-reference).

#### Public Instance of Crossbar

While a public instance is available for quick testing, running your own Crossbar instance is highly recommended. Switchboard oracles are heavily rate-limited by IP address, so using a dedicated instance prevents disruptions.

* **Public Instance:** <https://crossbar.switchboard.xyz>

#### Public Rate Limits (as of March 3, 2026)

The public Crossbar endpoint has multiple limit layers. The key limits to plan around are:

* **Network-level oracle request budget:** default `20 RPS` per user wallet for Switchboard requests; higher limits are available with `svSWTCH` stake. See [The Switchboard NCN](/how-it-works/switchboard-protocol/re-staking/the-switchboard-ncn).
* **Public edge throttling:** `https://crossbar.switchboard.xyz` enforces additional IP-based throttling and can return `429 Too Many Requests` under burst traffic (especially for `/updates/*` routes).
* **Surge connection caps:** managed Surge subscriptions have explicit connection limits by plan (`Plug: 1`, `Pro: 10`, `Enterprise: 15`). See [Surge pricing and limits](/docs-by-chain/solana-svm/surge).

Practical guidance:

* Treat public Crossbar as **best-effort** for development and low-volume usage.
* Back off exponentially with jitter on `429` responses.
* For production bots/frontends, self-host Crossbar to remove public edge contention.

**Examples:**

* **Job Definition Fetch:** <https://crossbar.switchboard.xyz/fetch/2718f49aa8fb6b71452ef149fa654a06d3996113034c27e2dca5c71b4a2866e7>
* **EVM Oracle Fetch (Core Mainnet):** <https://crossbar.switchboard.xyz/updates/evm/1116/0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c>

### Advanced

* [Crossbar API Endpoints](/tooling/crossbar/api-endpoints) — complete endpoint reference by route group.
* [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol) — HTTP + WebSocket protocol for custom Surge clients.


# Run Crossbar with Docker Compose

### Overview

Crossbar can be run pretty easily with Docker. The instructions below will walk you through running Crossbar in Docker containers using Docker Compose.

#### Crossbar Rust Implementation

The current version of Crossbar is implemented in Rust and includes built-in simulation capabilities, eliminating the need for a separate Task Runner Simulator service. This provides better performance and simplified deployment.

#### Prerequisites

Before running crossbar, ensure you have the following

* **Docker** and **Docker Compose** installed on your machine.
* A custom **Solana RPC** for improved performance (optional - strongly recommended)
* **Pinata** or another **IPFS** node for job storage (optional)

#### **Step 1: Set Up Your Project Directory**

1. **Create a Project Directory**: Create a directory for your project. This directory will contain all the necessary files for your Docker container deployment.

`mkdir my-crossbar-project cd my-crossbar-project`

#### **Step 2: Create the `docker-compose.yml` File**

1. **Create a `docker-compose.yml` File**: In your project directory, create a file named `docker-compose.yml`. This file will define the Docker services and environment variables.

> **Platform Compatibility Note:** The Crossbar Docker image is built for `linux/amd64` (x86/Intel architecture). If you're running on Apple Silicon (M1, M2, M3, M4 Macs) or other ARM-based systems, add the `platform: linux/amd64` line under the service definition. Docker Desktop will use Rosetta 2 or QEMU to emulate the x86 architecture. For best performance on Apple Silicon, ensure "Use Rosetta for x86/amd64 emulation" is enabled in Docker Desktop settings.

```yaml
version: '3.8'

services:
  crossbar:
    image: switchboardlabs/rust-crossbar:stable
    # Uncomment the following line if running on Apple Silicon (M1/M2/M3/M4) or other ARM systems:
    # platform: linux/amd64
    ports:
      - "8080:8080"  # HTTP API
      - "8081:8081"  # WebSocket
    environment:
      # === Core Configuration ===
      PORT: ${PORT:-8080}
      WS_PORT: ${WS_PORT:-8081}
      
      # === Blockchain RPCs (Optional - Recommended) ===
      SOLANA_MAINNET_RPC: ${SOLANA_MAINNET_RPC:-https://api.mainnet-beta.solana.com}
      SOLANA_DEVNET_RPC: ${SOLANA_DEVNET_RPC:-https://api.devnet.solana.com}

      # === IPFS Configuration (Optional) ===
      IPFS_GATEWAY_URL: ${IPFS_GATEWAY_URL:-https://ipfs.io}

      # === Performance (Optional) ===
      BROADCAST_WORKER_THREADS: ${BROADCAST_WORKER_THREADS:-32}
      RUST_LOG: ${RUST_LOG:-info}
```

#### **Step 3: Create the `.env` File**

1. **Create a `.env` File**: In the same directory, create a `.env` file to store your environment variables. This file is read by `docker compose` and will override the default values in the compose file if specified.

```bash
# .env file
# All environment variables are optional and have sensible defaults

# Recommended for better performance
SOLANA_MAINNET_RPC=https://api.mainnet-beta.solana.com
SOLANA_DEVNET_RPC=https://api.devnet.solana.com

# Optional IPFS configuration
# PINATA_JWT_KEY="your-pinata-jwt-key"
# PINATA_GATEWAY_KEY="your-pinata-gateway-key"
# IPFS_GATEWAY_URL="https://ipfs.io"
```

#### **Step 4: Build and Run the Docker Container**

1. **Build and Run the Docker Container**: Navigate to your project directory and run the following command to start your Docker container:

`docker-compose up -d`

This command will start the container in detached mode. The `-d` flag stands for "detached," meaning the container runs in the background.

#### **Step 5: Verify the Deployment**

1. **Verify the Deployment**: Once the container is running, you can verify that the service is up and running by accessing it at `http://localhost:8080`. You can also check the status of the container by running:

`docker-compose ps`

This command will show the status of the running services.

#### **Step 6: Stopping and Restarting the Docker Container**

1. **Stop the Docker Container**: To stop the container, run:

`docker-compose down`

This command will stop and remove the containers defined in your `docker-compose.yml` file.

1. **Restart the Docker Container**: To restart the container, run:

`docker-compose up -d`

#### Additional Tips

* **Logs**: To view the logs of the running container, use the following command:

`docker-compose logs -f`

* **Updating Environment Variables**: If you need to update the environment variables, edit the `.env` file and restart the container:

`docker-compose down docker-compose up -d`

## Environment Variables Reference

### Core Server Configuration

| Variable      | Type     | Default | Description                       |
| ------------- | -------- | ------- | --------------------------------- |
| `PORT`        | Optional | `8080`  | HTTP server port                  |
| `WS_PORT`     | Optional | `8081`  | WebSocket server port             |
| `DISABLE_API` | Optional | `false` | Set to "true" to disable HTTP API |

### Blockchain RPC Configuration

| Variable             | Type     | Default                               | Description                 |
| -------------------- | -------- | ------------------------------------- | --------------------------- |
| `SOLANA_MAINNET_RPC` | Optional | `https://api.mainnet-beta.solana.com` | Solana mainnet RPC endpoint |
| `SOLANA_DEVNET_RPC`  | Optional | `https://api.devnet.solana.com`       | Solana devnet RPC endpoint  |

### IPFS Configuration

| Variable             | Type     | Default           | Description                    |
| -------------------- | -------- | ----------------- | ------------------------------ |
| `IPFS_GATEWAY_URL`   | Optional | `https://ipfs.io` | IPFS gateway for fetching data |
| `PINATA_JWT_KEY`     | Optional | -                 | Pinata JWT key for storage     |
| `PINATA_GATEWAY_KEY` | Optional | -                 | Pinata gateway key             |
| `KUBO_URL`           | Optional | -                 | Local Kubo IPFS node URL       |

### Database Configuration (Optional)

| Variable       | Type     | Default | Description                  |
| -------------- | -------- | ------- | ---------------------------- |
| `DATABASE_URL` | Optional | -       | PostgreSQL connection string |
| `PGUSER`       | Optional | -       | PostgreSQL username          |
| `PGPASSWORD`   | Optional | -       | PostgreSQL password          |
| `PGDATABASE`   | Optional | -       | PostgreSQL database name     |
| `PGHOST`       | Optional | -       | PostgreSQL host              |
| `PGPORT`       | Optional | -       | PostgreSQL port              |

### Performance & Caching

| Variable                       | Type     | Default | Description                    |
| ------------------------------ | -------- | ------- | ------------------------------ |
| `BROADCAST_WORKER_THREADS`     | Optional | `32`    | Number of Tokio worker threads |
| `SIMULATION_CACHE_TTL_SECONDS` | Optional | `3`     | Cache TTL for simulations      |
| `DISABLE_CACHE`                | Optional | `false` | Disable caching entirely       |
| `RATE_LIMIT`                   | Optional | -       | Rate limiting configuration    |

### Development & Debugging

| Variable             | Type     | Default | Description                                 |
| -------------------- | -------- | ------- | ------------------------------------------- |
| `RUST_LOG`           | Optional | `info`  | Log level (error, warn, info, debug, trace) |
| `TOKIO_CONSOLE`      | Optional | `false` | Enable tokio console debugging              |
| `TOKIO_CONSOLE_PORT` | Optional | `6669`  | Port for tokio console                      |
| `IS_LOCALHOST`       | Optional | `false` | Development mode flag                       |

### Testing it out:

Try the deployment out by navigating to (can take a few seconds the first run): <http://localhost:8080/updates/evm/1116/0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c>

The equivalent result should look something like the output from the public node: <https://crossbar.switchboard.xyz/updates/evm/1116/0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c>


# Crossbar API Endpoints

This page documents the HTTP and WebSocket endpoints exposed by Crossbar.

Base URL examples:

* Public: `https://crossbar.switchboard.xyz`
* Local: `http://localhost:8080`

For machine-readable schema and live testing:

* Swagger UI: `GET /docs`
* OpenAPI JSON: `GET /api-docs/openapi.json`

## Core Endpoints

| Method | Path                        | Purpose                            |
| ------ | --------------------------- | ---------------------------------- |
| `GET`  | `/health`                   | Health check                       |
| `GET`  | `/version`                  | Service version info               |
| `GET`  | `/test`                     | Basic test endpoint                |
| `GET`  | `/protos/job_schemas.proto` | Oracle job protobuf schema         |
| `POST` | `/store`                    | Store v1 feed definition           |
| `GET`  | `/fetch/{hash}`             | Fetch v1 feed definition           |
| `POST` | `/v2/store`                 | Store v2 feed definition           |
| `GET`  | `/v2/fetch/{feed_id}`       | Fetch v2 feed definition           |
| `GET`  | `/v2/update/{feedHashes}`   | Build v2 multi-feed update payload |
| `ANY`  | `/rpc/{network}`            | RPC passthrough/proxy endpoint     |
| `GET`  | `/debug/cid/{hash}`         | CID conversion/debug               |
| `GET`  | `/debug/bnb`                | Binance debug endpoint             |

Parameter units are surface-specific. Raw v2 `OracleFeed.maxJobRangePct` and raw gateway `max_variance` values are percentages scaled by `1e9`; `1_000_000_000` means `1%`. See [Feed Parameter Units](/custom-feeds/advanced-feed-configuration/feed-parameter-units).

Use `/v2/fetch/{feed_id}` for v2 feed IDs created by Feed Builder or Crossbar v2 storage. The older `/fetch/{hash}` route is for v1 feed definitions and legacy compatibility.

## EVM Route Selection

Use different Crossbar routes depending on whether you are integrating a current Feed Builder/custom feed or an older aggregator-based EVM integration.

| Use case                                | Use these routes                                                                     | Identifier                                  | Notes                                                                                                             |
| --------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Feed Builder/custom feed on EVM         | `/v2/fetch/{feed_id}`, `/v2/simulate/{feedHashes}`, `/v2/update/{feedHashes}`        | deterministic `bytes32` feed ID / feed hash | Recommended flow for Monad and current custom-feed integrations. Use `chain=evm`, \`network=mainnet               |
| Legacy aggregator-based EVM integration | `/simulate/evm/{network}/{aggregator_ids}`, `/updates/evm/{chainId}/{aggregatorIds}` | legacy aggregator ID                        | Compatibility flow for older EVM integrations. Do not use this as the primary path for Feed Builder custom feeds. |

## Solana/SVM Route Selection

For new Solana/SVM feed-hash integrations, prefer the SDK managed update path: `queue.fetchManagedUpdateIxs(...)`. It fetches Ed25519 oracle signatures and builds the quote-program `verified_update` instruction that writes the canonical `OracleQuote` account.

The classic `PullFeed.fetchUpdateIx(...)` and Crossbar `/updates/solana/...` flows target legacy PullFeed accounts and `pullFeedSubmitResponseConsensus`. Use them only for existing classic PullFeed integrations where the selected queue/gateway environment supports that path.

## Simulation Endpoints

### Generic

| Method | Path                     | Purpose                              |
| ------ | ------------------------ | ------------------------------------ |
| `POST` | `/simulate`              | Simulate jobs from request body      |
| `POST` | `/simulate/jobs`         | Alias for `/simulate`                |
| `GET`  | `/simulate/{feedHashes}` | Simulate comma-separated feed hashes |

### Chain-specific

| Method | Path                                        | Purpose                       |
| ------ | ------------------------------------------- | ----------------------------- |
| `POST` | `/simulate/solana`                          | Simulate Solana feeds         |
| `GET`  | `/simulate/solana/{network}/{feedpubkeys}`  | Simulate Solana feed pubkeys  |
| `POST` | `/simulate/eclipse`                         | Simulate Eclipse feeds        |
| `GET`  | `/simulate/eclipse/{network}/{feedpubkeys}` | Simulate Eclipse feed pubkeys |
| `POST` | `/simulate/evm`                             | Simulate EVM aggregators      |
| `GET`  | `/simulate/evm/{network}/{aggregator_ids}`  | Simulate EVM aggregator IDs   |
| `POST` | `/simulate/aptos`                           | Simulate Aptos feeds          |
| `GET`  | `/simulate/aptos/{network}/{feedhashes}`    | Simulate Aptos feed hashes    |
| `POST` | `/simulate/sui`                             | Simulate Sui feeds            |
| `POST` | `/simulate/sui/feeds`                       | Alias for Sui simulation      |
| `GET`  | `/simulate/sui/{network}/{feedids}`         | Simulate Sui feed IDs         |
| `POST` | `/simulate/iota`                            | Simulate Iota feeds           |
| `GET`  | `/simulate/iota/{network}/{feedids}`        | Simulate Iota feed IDs        |

### V2 simulation

| Method | Path                        | Purpose                                    |
| ------ | --------------------------- | ------------------------------------------ |
| `POST` | `/v2/simulate`              | Simulate v2 feed input payload             |
| `GET`  | `/v2/simulate/{feedHashes}` | Simulate v2 feed hashes                    |
| `POST` | `/v2/simulate/proto`        | Simulate base64-encoded `OracleFeed` proto |

### Backward-compatible API prefix

Crossbar also exposes simulation routes under `/api/simulate` for compatibility.\
Example: `POST /api/simulate`, `GET /api/simulate/{feedHashes}`.

## Update Endpoints

| Method | Path                                                   | Purpose                                          |
| ------ | ------------------------------------------------------ | ------------------------------------------------ |
| `GET`  | `/updates/solana/{network}/{feedPubkeys}`              | Solana update instructions and oracle responses  |
| `GET`  | `/updates/eclipse/{network}/{feedPubkeys}`             | Eclipse update instructions and oracle responses |
| `GET`  | `/updates/evm/{chainId}/{aggregatorIds}`               | EVM encoded updates                              |
| `GET`  | `/updates/evm/fetch_update_data/{chain_id}/{feed_ids}` | EVM update-data variant                          |
| `GET`  | `/updates/aptos/{network}/{aggregatorAddresses}`       | Aptos aggregator updates                         |
| `GET`  | `/updates/sui/{network}/{aggregatorAddresses}`         | Sui aggregator updates                           |
| `GET`  | `/updates/iota/{network}/{aggregatorAddresses}`        | Iota aggregator updates                          |

## Gateway Endpoints

| Method | Path                                   | Purpose                                    |
| ------ | -------------------------------------- | ------------------------------------------ |
| `GET`  | \`/gateways?network=mainnet            | devnet                                     |
| `POST` | `/gateways/fetch_signatures`           | Fetch signatures (single feed/job request) |
| `POST` | `/gateways/fetch_signatures_consensus` | Fetch consensus signatures                 |

## Oracle and Guardian Endpoints

| Method | Path                        | Purpose                 |
| ------ | --------------------------- | ----------------------- |
| `GET`  | `/oracles`                  | List Solana oracles     |
| `GET`  | `/oracles/sui`              | List Sui oracles        |
| `GET`  | `/oracles/aptos`            | List Aptos oracles      |
| `POST` | `/oracles/fetch_signatures` | Fetch oracle signatures |
| `GET`  | `/guardians`                | List guardians          |

## Randomness Endpoints

| Method | Path              | Purpose                             |
| ------ | ----------------- | ----------------------------------- |
| `POST` | `/randomness/evm` | Fetch EVM randomness result payload |

## Stream (Surge) Endpoints

| Method | Path                      | Purpose                           |
| ------ | ------------------------- | --------------------------------- |
| `GET`  | `/stream/ws`              | WebSocket stream endpoint         |
| `GET`  | `/stream/surge_feeds`     | List available Surge feeds        |
| `POST` | `/stream/request_session` | Create Surge stream session token |
| `GET`  | `/stream/socket_metrics`  | Stream/socket metrics             |
| `GET`  | `/stream/debug_binance`   | Binance stream debug info         |

Legacy Surge endpoints are also exposed when Surge is enabled:

| Method | Path                | Purpose                         |
| ------ | ------------------- | ------------------------------- |
| `GET`  | `/v1/surge/stream`  | Legacy Surge WebSocket endpoint |
| `POST` | `/v1/surge/session` | Legacy Surge session endpoint   |

## Flamegraph Endpoints

| Method | Path                | Purpose                |
| ------ | ------------------- | ---------------------- |
| `GET`  | `/flamegraph`       | Flamegraph status/info |
| `POST` | `/flamegraph/start` | Start profiling        |
| `POST` | `/flamegraph/stop`  | Stop profiling         |
| `GET`  | `/flamegraph/ui`    | Flamegraph UI          |

## Detailed Request/Response Reference

Use this section for implementation-level request and response shapes. The tables above remain the quick index.

### Simulation

#### `POST /simulate/jobs`

Purpose: simulate raw `OracleJob[]` directly (without fetching feed definitions from IPFS).

Request body:

```json
{
  "jobs": [
    {
      "tasks": [
        { "valueTask": { "value": 42 } }
      ]
    }
  ],
  "includeReceipts": true,
  "variableOverrides": {
    "API_KEY": "..."
  },
  "network": "mainnet"
}
```

Response (`200`):

```json
{
  "feedHash": "direct",
  "results": ["42"],
  "receipts": ["..."],
  "error": null
}
```

Notes:

* `jobs` also accepts base64-encoded protobuf entries.
* `variableOverrides` is optional.
* `network` defaults to `mainnet` when omitted.

#### `POST /simulate/solana`

Purpose: simulate one or more Solana feed pubkeys by loading on-chain feed state, resolving feed hash, loading jobs from IPFS, and running jobs.

Request body:

```json
{
  "feeds": [
    "6dJY6fNn7q7eYxw8fPqfF7XULg1Wm3v3GJm6M6hQ8B9X"
  ],
  "network": "mainnet-beta",
  "includeReceipts": false
}
```

Response (`200`): array of per-feed simulation results.

```json
[
  {
    "feed": "6dJY6fNn7q7eYxw8fPqfF7XULg1Wm3v3GJm6M6hQ8B9X",
    "feedHash": "617c43b30c588de5e620fa4c7b932e103301b9a160e2c24be69dbe0357e45797",
    "results": ["1.2345", "1.2351", "1.2339"],
    "receipts": null,
    "result": "1.2345",
    "stdev": "0.00049",
    "variance": "0.00024",
    "error": null
  }
]
```

Notes:

* `network` accepts values such as `mainnet-beta`, `devnet`, `testnet`.
* `mainnet` is normalized to `mainnet-beta`.

#### `GET /simulate/{feedHashes}`

Purpose: simulate one or more feed hashes directly from IPFS definitions.

Path:

* `feedHashes`: comma-separated feed hashes

Query:

* `includeReceipts` (`bool`, optional)

Response (`200`): array

```json
[
  {
    "feedHash": "617c43b30c588de5e620fa4c7b932e103301b9a160e2c24be69dbe0357e45797",
    "results": ["1.2345", "1.2351"],
    "receipts": null,
    "error": null
  }
]
```

#### `POST /v2/simulate`

Purpose: simulate v2 feed hashes with optional network and variable overrides.

Request body:

```json
{
  "feedHashes": [
    "617c43b30c588de5e620fa4c7b932e103301b9a160e2c24be69dbe0357e45797"
  ],
  "includeReceipts": false,
  "variableOverrides": {
    "API_KEY": "..."
  },
  "network": "mainnet"
}
```

Response (`200`):

```json
{
  "feeds": [
    {
      "feedHash": "617c43b30c588de5e620fa4c7b932e103301b9a160e2c24be69dbe0357e45797",
      "feedName": "MINO/USD",
      "results": ["1.2345"],
      "receipts": null,
      "variableOverrides": {
        "API_KEY": "..."
      },
      "network": "mainnet"
    }
  ],
  "totalFeeds": 1,
  "successfulFeeds": 1,
  "failedFeeds": 0
}
```

### Updates

#### `GET /v2/update/{feedHashes}`

Purpose: build a chain-specific consensus payload for one or more v2 feed hashes.

Path:

* `feedHashes`: comma-separated deterministic feed IDs / feed hashes

Query:

* `chain` (`string`, required for chain-specific payloads; use `evm` for EVM)
* `network` (`string`, optional; `mainnet` or `testnet`)
* `use_timestamp` (`bool`, optional)
* `num_oracles` (`u32`, optional)
* `gateway` (`string`, optional)

Response (`200`): object

```json
{
  "medianResponses": [
    {
      "value": "123450000000000000000",
      "feedHash": "0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c",
      "numOracles": 3
    }
  ],
  "oracleResponses": [],
  "timestamp": 1730000000,
  "slot": 0,
  "recentHash": "0xabc123...",
  "encoded": "0x8f6f2b7c..."
}
```

V2 update response schema:

* `medianResponses`: one consensus value per requested feed hash
* `oracleResponses`: per-oracle response detail
* `timestamp`: signed consensus timestamp
* `slot`: slot or sequence metadata from the gateway
* `recentHash`: recent hash used for the signed payload
* `encoded`: chain-specific encoded update payload

For EVM, wrap `encoded` into a one-element `bytes[]` when calling `getFee` or `updateFeeds`.

Monad example:

```bash
curl "http://localhost:8080/v2/update/0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812?chain=evm&network=testnet&use_timestamp=true"
```

#### `GET /updates/solana/{network}/{feedPubkeys}`

Purpose: generate Solana pull update instructions and oracle response metadata.

Path:

* `network`: `mainnet`, `mainnet-beta`, `devnet`, `testnet`
* `feedPubkeys`: comma-separated feed pubkeys

Query:

* `numSignatures` (`u32`, optional)
* `payer` (`string`, required)

Response (`200`): array of update objects

```json
[
  {
    "success": true,
    "pullIxns": [
      "0673bd46f2e47e04f12bd92fb731968ecd9d9757c274da87476f465c040c6573050000000000000089e0fecf1c1b3a11e77b9d1048192288ae1d8ada0e19ff95d737c4e5afb583f70001d284bd424eb258f1f502c95ff334245b64af7df6b14435d1ea46d10fb3ac68b6000086807068432f186a147cf0b13a30067d386204ea9d6c8b04743ac2ef010b075200007752c55e8b0a7079ad51975736764e45f56d61ebd15aa96d55f7ca86d5b5e387010100000000000000000000000000000000000000000000000000000000000000000000310000000000000001020304050607081d3c620d5670b1b26d0d3c7c27cb75a5187d99b8ccf8850d5b79d573b81bff7c030000009600000000"
    ],
    "responses": [
      {
        "oracle": "9pPCSotuPGUDgdYUtngMCemNi3KHdWFvwLhqx57KkbXb",
        "result": 1.1067679409326794,
        "errors": ""
      }
    ],
    "lookupTables": [
      "A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w"
    ]
  }
]
```

`pullIxns` wire format:

* Each array entry is a hex-encoded `bincode` serialization of `solana_sdk::instruction::Instruction`.
* Raw HTTP responses return these serialized strings directly.
* SDK helpers such as `CrossbarClient.fetchSolanaUpdates` may decode them into native instruction objects before returning to your application.
* `lookupTables` are still returned separately for address lookup table usage in versioned transactions.

Rust decode example:

```rust
use solana_sdk::instruction::Instruction;

fn decode_instruction(ix_hex: &str) -> anyhow::Result<Instruction> {
    let bytes = hex::decode(ix_hex)?;
    Ok(bincode::deserialize(&bytes)?)
}
```

#### `GET /updates/eclipse/{network}/{feedPubkeys}`

Response schema is the same as `/updates/solana/{network}/{feedPubkeys}`:

* `success`: `bool`
* `pullIxns`: array of hex-encoded serialized `Instruction` values
* `responses`: array of oracle responses
* `lookupTables`: array of address lookup table pubkeys (base58)

#### `GET /updates/evm/{chainId}/{aggregatorIds}`

Purpose: fetch EVM-compatible encoded updates and supporting oracle response data.

Path:

* `chainId`: EVM chain ID (example `1`, `42161`, `1116`)
* `aggregatorIds`: comma-separated feed IDs

Query:

* `numSignatures` (`u32`, optional)
* `gateway` (`string`, optional)

Response (`200`): object

```json
{
  "results": [
    {
      "result": "123450000000000000000"
    }
  ],
  "failures": [],
  "encoded": [
    "0x8f6f2b7c..."
  ]
}
```

EVM response schema:

* `results`: array of oracle response objects (includes normalized `result` and additional gateway-returned fields).
* `failures`: array of errors for feeds/oracles that failed during fetch/update building.
* `encoded`: array of `0x`-prefixed ABI-encoded update payloads.

#### `GET /updates/aptos/{network}/{aggregatorAddresses}`

Response (`200`):

```json
{
  "responses": [
    {
      "responses": [],
      "failures": [],
      "encoded": [
        "0x..."
      ]
    }
  ],
  "failures": [],
  "encoded": [
    "0x..."
  ]
}
```

Aptos response schema:

* `responses`: per-aggregator update objects returned by Aptos SDK.
* `failures`: top-level route errors.
* `encoded`: flattened list of encoded Aptos update payloads.

#### `GET /updates/sui/{network}/{aggregatorAddresses}`

Response (`200`):

```json
{
  "responses": [
    {
      "results": [],
      "failures": []
    }
  ],
  "failures": []
}
```

Sui response schema:

* `responses`: per-aggregator update objects returned by Sui SDK (`fetchUpdateInfo` output).
* `failures`: top-level route errors.

#### `GET /updates/iota/{network}/{aggregatorAddresses}`

Response (`200`) matches the Sui endpoint shape:

```json
{
  "responses": [
    {
      "results": [],
      "failures": []
    }
  ],
  "failures": []
}
```

### Gateways

#### `GET /gateways`

Purpose: discover active gateway URLs from cached oracle state.

Query:

* `network`: `mainnet` (default), `devnet`, or `testnet`

Response (`200`):

```json
[
  "https://gateway-1.example.com",
  "https://gateway-2.example.com"
]
```

#### `POST /gateways/fetch_signatures`

Purpose: fetch signatures for a single feed/jobs request. Supports both legacy and new request shapes.

`maxVariance` in these raw gateway request bodies is already scaled by `1e9`; `50_000_000` means `0.05%`.

Legacy body (feed-hash based):

```json
{
  "feedHash": "617c43b30c588de5e620fa4c7b932e103301b9a160e2c24be69dbe0357e45797",
  "numSignatures": 3,
  "maxVariance": 50000000,
  "minResponses": 1,
  "useTimestamp": true
}
```

New body (jobs-based):

```json
{
  "apiVersion": "1",
  "jobsB64Encoded": ["..."],
  "numOracles": 3,
  "maxVariance": 50000000,
  "minResponses": 1,
  "useTimestamp": true
}
```

Response (`200`): gateway signature response object (signatures + timestamp + variance).

#### `POST /gateways/fetch_signatures_consensus`

Purpose: fetch consensus signatures using feed request objects.

Request body:

```json
{
  "apiVersion": "1",
  "feedRequests": [],
  "numOracles": 3,
  "useTimestamp": true
}
```

Response (`200`):

```json
{
  "signatures": [],
  "timestamp": 1730000000,
  "variance": 0,
  "consensusReached": true
}
```

### Stream (Surge)

#### `GET /stream/surge_feeds`

Purpose: list currently available Surge feed symbols.

Query:

* `symbol` (optional)
* `exchange` (optional)

Response (`200`): feed list object from Surge core. Response (`503`): no feeds available.

#### `POST /stream/request_session`

Purpose: validate API key and create session token + WebSocket URL.

Auth input:

* Header `x-api-key: <key>` preferred
* Also supports `Authorization: Bearer <key>`
* Also supports query param `api_key=<key>`

Request body:

```json
{
  "client_ip": "203.0.113.10"
}
```

Response (`200`):

```json
{
  "session_token": "...",
  "simulator_ws_url": "wss://.../v1/surge/stream"
}
```

Error responses:

* `400` missing API key
* `401` invalid API key

#### `GET /stream/ws`

Purpose: WebSocket stream endpoint for Surge updates.\
For full handshake, auth headers, subscribe payload, and ping/pong flow, see [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol).

### Minimal curl examples

Simulate Solana feed:

```bash
curl -X POST http://localhost:8080/simulate/solana \
  -H "Content-Type: application/json" \
  -d '{
    "feeds": ["6dJY6fNn7q7eYxw8fPqfF7XULg1Wm3v3GJm6M6hQ8B9X"],
    "network": "mainnet-beta",
    "includeReceipts": false
  }'
```

Fetch EVM updates:

```bash
curl "http://localhost:8080/updates/evm/1116/0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c"
```

Fetch a Monad custom-feed payload with the v2 route:

```bash
curl "http://localhost:8080/v2/update/0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812?chain=evm&network=testnet&use_timestamp=true"
```

Discover gateways:

```bash
curl "http://localhost:8080/gateways?network=mainnet"
```

## Public Rate Limits

For `https://crossbar.switchboard.xyz`, rate limiting is layered and route-dependent:

| Surface                                                       | Public limit behavior                                                                       | Source                                                                                   |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Network-level Switchboard oracle requests                     | Default `20 requests/second` per user wallet; can be increased with stake                   | [The Switchboard NCN](/how-it-works/switchboard-protocol/re-staking/the-switchboard-ncn) |
| Crossbar public REST (`/updates/*`, gateway/signature routes) | Additional IP-based edge throttling; `429 Too Many Requests` can appear under burst traffic | Public endpoint behavior, observed on March 3, 2026                                      |
| Surge WebSocket access                                        | Plan-based max connections (`Plug: 1`, `Pro: 10`, `Enterprise: 15`)                         | [Surge pricing and limits](/docs-by-chain/solana-svm/surge)                              |

Operational guidance:

* Keep update polling conservative on public Crossbar and avoid burst fan-out.
* On `429`, apply exponential backoff with jitter and retry.
* For steady high-throughput workloads, self-host Crossbar.

## Notes

* Some stream and gateway flows require signature/auth headers. See [Surge Gateway Protocol](/tooling/crossbar/gateway-protocol).
* Endpoint behavior can vary by network and environment configuration.
* Public Crossbar is rate limited by IP. Production systems should self-host.
* For complete machine schema and current field contracts, use `GET /api-docs/openapi.json`.


# Surge Gateway Protocol

Surge provides low-latency price streaming via the Crossbar gateway. This page documents the HTTP + WebSocket protocol for clients that cannot use the SDK or need custom integrations.

## When to use this protocol

* You are implementing a non-JS client (Rust/Go/Python/etc.).
* You are running custom infrastructure and need direct gateway control.
* You are debugging auth/session issues or connection failures.
* You are building a load-testing or monitoring client.

## Prerequisites

Before calling `request_stream`, the Solana pubkey you authenticate with must have an **active on-chain Surge subscription**.

* Subscriptions are managed on Solana and paid in `SWTCH` tokens (except free-tier cases where payment can be zero, but subscription initialization is still required).
* If there is no active subscription for your pubkey, `POST /gateway/api/v1/request_stream` will fail even when signatures, blockhash, and timestamps are valid.
* Subscription setup guide: [Surge Subscription Guide](/ai-agents-llms/surge-subscription-guide).
* Explorer subscription UI: [explorer.switchboardlabs.xyz/subscriptions](https://explorer.switchboardlabs.xyz/subscriptions).

## Protocol Overview

1. Discover a gateway endpoint.
2. Create signature headers.
3. Request a streaming session.
4. Open the WebSocket with auth headers.
5. Send a Subscribe message.
6. Receive bundled price updates.
7. Respond to keepalive pings.

***

## 1. Gateway discovery

Mainnet:

```
GET https://crossbar.switchboard.xyz/gateways?network=mainnet
```

Devnet:

```
GET https://crossbar.switchboard.xyz/gateways?network=devnet
```

The response returns one or more gateway base URLs. Choose one and use it for the session request.

***

## 2. Signature headers

For every HTTP and WebSocket request, you must include signature headers derived from a recent Solana blockhash and current timestamp.

**Message to sign**

```
SHA256("{blockhash}:{timestamp}")
```

Sign the hash with **Ed25519** using your Solana keypair.

**Required headers**

* `X-Switchboard-Signature` — Ed25519 signature of the hash
* `X-Switchboard-Pubkey` — Solana public key
* `X-Switchboard-Blockhash` — recent Solana blockhash
* `X-Switchboard-Timestamp` — current timestamp

Notes:

* Use a **fresh blockhash and timestamp** for each request.
* Keep client time in sync (clock skew can cause auth failures).

***

## 3. Session request

```
POST {gateway}/gateway/api/v1/request_stream
```

Include the signature headers. The response contains:

* `session_token`
* `oracle_ws_url`

***

## 4. WebSocket connection

Open a WebSocket connection to `oracle_ws_url` with:

* `Authorization: Bearer {pubkey}:{session_token}`
* The same signature headers (`X-Switchboard-*`)

***

## 5. Subscribe message

Send a `Subscribe` message after connecting:

```json
{
  "type": "Subscribe",
  "feed_bundles": [
    {
      "feeds": [
        {
          "symbol": { "base": "BTC", "quote": "USD" },
          "source": "AUTO"
        }
      ]
    }
  ],
  "signature_scheme": "Ed25519",
  "pubkey": "<your-solana-pubkey>",
  "signature": "<ed25519-signature>",
  "blockhash": "<recent-solana-blockhash>",
  "timestamp": "<current-timestamp>"
}
```

***

## 6. Price updates

The gateway sends **BundledFeedUpdate** messages. Each update includes `feed_values[]` entries. The `value` field is an **18-decimal big-integer string** (no decimal point). Convert it to a decimal value by dividing by `1e18` (e.g., `\"67335320000000000000000\"` → `67335.32`).

If you're using the SDK, helpers like `getFormattedPrices()` already apply this scaling for you. Only raw consumers need to handle the `1e18` divisor.

***

## 7. Keepalive

The gateway may send a `SignedPing`. Respond with a `SignedPong` that includes a **fresh signature** (new blockhash + timestamp):

```json
{
  "type": "SignedPong",
  "signature_scheme": "Ed25519",
  "pubkey": "<your-solana-pubkey>",
  "signature": "<ed25519-signature>",
  "blockhash": "<recent-solana-blockhash>",
  "timestamp": "<current-timestamp>"
}
```

***

## Error handling and reconnects

Common causes of disconnects or auth errors:

* Invalid signature
* Expired timestamp
* Stale blockhash
* Invalid or expired session token

On failure, request a new session and reconnect with fresh signatures.


# CLI

Switchboard offers a command line interface (CLI) for interacting and printing on-chain info for all chains.

[Please see the README and documentation in the Switchboard CLI here.](https://www.npmjs.com/package/@switchboard-xyz/cli)


# SDKs

Switchboard SDK versions are pinned and validated in one canonical place:

* [SDK Version Matrix](/tooling/sdk-version-matrix)
* Machine-readable lock file: [`tooling/sdk-versions.lock.json`](https://github.com/switchboard-xyz/gitbook-on-demand/tree/main/tooling/sdk-versions.lock.json)

Use the matrix for all docs and code snippets to avoid version drift.

## SDK Links

| Language   | Type                           | Resource                              | Link                                                           |
| ---------- | ------------------------------ | ------------------------------------- | -------------------------------------------------------------- |
| Rust       | SDK                            | `switchboard-on-demand`               | <https://crates.io/crates/switchboard-on-demand>               |
| Rust       | Docs                           | `switchboard-on-demand`               | <https://switchboard-on-demand-rust-docs.web.app/>             |
| TypeScript | SDK (SVM)                      | `@switchboard-xyz/on-demand`          | <https://switchboard-docs.web.app/>                            |
| TypeScript | SDK (EVM)                      | `@switchboard-xyz/on-demand-solidity` | <https://switchboard-evm-sdk.web.app>                          |
| TypeScript | SDK (Sui)                      | `@switchboard-xyz/sui-sdk`            | <https://switchboard-sui-sdk.web.app>                          |
| TypeScript | SDK (Aptos/Movement)           | `@switchboard-xyz/aptos-sdk`          | <https://switchboard-aptos-sdk.web.app>                        |
| TypeScript | SDK (Iota)                     | `@switchboard-xyz/iota-sdk`           | <https://www.npmjs.com/package/@switchboard-xyz/iota-sdk>      |
| TypeScript | Common                         | `@switchboard-xyz/common`             | <https://switchboardxyz-common.netlify.app/>                   |
| TypeScript | Classic PullFeed compatibility | `@switchboard-xyz/common-legacy`      | <https://www.npmjs.com/package/@switchboard-xyz/common-legacy> |

Current on-demand JavaScript integrations should use Common `5.8.5` and on-demand `3.10.6`. on-demand installs common-legacy `1.1.1` transitively; install common-legacy directly only when importing `LegacyCrossbarClient`.


# SDK Version Matrix

This page is the version reference for Switchboard docs, examples, and verifier tooling.

* Baseline date: **July 30, 2026**
* Source of truth: [`tooling/sdk-versions.lock.json`](https://github.com/switchboard-xyz/gitbook-on-demand/tree/main/tooling/sdk-versions.lock.json)
* The lock file now tracks two different views:
  * `sdk_versions` / `companion_versions`: the **canonical docs pin set** used by current docs and compatibility checks
  * `observed_example_versions`: the **exact versions currently pinned in the checked-in `sb-on-demand-examples` manifests**

## Observed Versions In Current Examples

| Package / Crate                       | Observed Version(s) | Current Example References                                                                                                                                                                                         | Notes                                                      |
| ------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `@switchboard-xyz/on-demand`          | `^3.10.6`           | `common`, `solana/feeds/*`, `solana/prediction-market`, `solana/randomness/coin-flip`, `solana/surge`, `solana/x402`, `solana/legacy/feeds`, `sui/feeds/basic`, `sui/surge/basic`                                  | Current TypeScript examples are aligned.                   |
| `@switchboard-xyz/common`             | `^5.8.5`            | `common`, `common/twitter-follower-count`, `common/variable-overrides`, `evm/*`, `solana/feeds/*`, `solana/prediction-market`, `solana/randomness/coin-flip`, `solana/surge`, `solana/x402`, `solana/legacy/feeds` | Current TypeScript examples are aligned.                   |
| `@switchboard-xyz/common-legacy`      | `^1.1.1`            | `solana/legacy/feeds`                                                                                                                                                                                              | Compatibility transport for classic PullFeed integrations. |
| `@switchboard-xyz/on-demand-solidity` | `^1.1.0`            | `evm/price-feeds`, `evm/randomness/*`                                                                                                                                                                              | Current EVM examples are aligned.                          |
| `@switchboard-xyz/sui-sdk`            | `^0.1.16`           | `sui/feeds/basic`, `sui/surge/basic`                                                                                                                                                                               | Aligned across current Sui examples.                       |
| `switchboard-on-demand`               | `0.13.0`            | `common/rust-feed-creation`, `solana/feeds/*`, `solana/prediction-market`, `solana/randomness/coin-flip`                                                                                                           | Current Rust examples are aligned.                         |
| `switchboard-protos`                  | `0.2.6`             | `solana/prediction-market`                                                                                                                                                                                         | Only used by the prediction-market program.                |
| `pinocchio`                           | `0.11.2`            | `solana/feeds/advanced`                                                                                                                                                                                            | Companion dependency for the low-CU Pinocchio example.     |

## Canonical Docs Pin Set

These are the current docs pins. Example-backed pins match the observed versions above; docs-only pins are listed as the versions already used by their pages.

| Package / Crate                       | Canonical Pin | Notes                                                                                             |
| ------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------- |
| `@switchboard-xyz/on-demand`          | `3.10.6`      | Matches the current TypeScript example set.                                                       |
| `@switchboard-xyz/common`             | `5.8.5`       | Canonical OracleJob and OracleFeed serialization used by current TypeScript examples.             |
| `@switchboard-xyz/common-legacy`      | `1.1.1`       | Installed transitively by on-demand; install directly only when importing `LegacyCrossbarClient`. |
| `@switchboard-xyz/on-demand-solidity` | `1.1.0`       | Current Solidity interface pin.                                                                   |
| `@switchboard-xyz/sui-sdk`            | `0.1.16`      | Matches current Sui examples.                                                                     |
| `@switchboard-xyz/aptos-sdk`          | `0.1.5`       | Used by the Aptos and Movement docs.                                                              |
| `@switchboard-xyz/iota-sdk`           | `0.0.3`       | Current docs pin.                                                                                 |
| `switchboard-on-demand`               | `0.13.0`      | Matches current Rust examples.                                                                    |
| `switchboard-protos`                  | `0.2.6`       | Matches the current prediction-market example.                                                    |

## Companion Dependencies

| Dependency           | Canonical Pin | Notes                                                     |
| -------------------- | ------------- | --------------------------------------------------------- |
| `@solana/web3.js`    | `1.98.0`      | Matches current Solana examples.                          |
| `@mysten/sui`        | `1.38.0`      | Compatible with current Sui docs/examples import surface. |
| `@aptos-labs/ts-sdk` | `6.1.0`       | Used by the Aptos and Movement docs.                      |
| `@iota/iota-sdk`     | `1.11.0`      | Iota smoke projects.                                      |
| `ethers`             | `6.13.1`      | Matches the current EVM price-feeds example manifest.     |
| `@coral-xyz/anchor`  | `0.31.1`      | Matches the current Solana example manifests.             |
| `pinocchio`          | `0.11.2`      | Matches the advanced Solana price-feed example.           |

## Toolchain Baseline

| Tool              | Version                                                       |
| ----------------- | ------------------------------------------------------------- |
| Node.js           | `23.11.0` (verified, `>=24` recommended for `@iota/iota-sdk`) |
| Bun               | `1.3.6`                                                       |
| Rust              | `1.89.0-nightly`                                              |
| Anchor CLI        | `0.31.1`                                                      |
| Solana CLI        | `2.3.11`                                                      |
| Foundry (`forge`) | `1.5.0`                                                       |
| Aptos CLI         | `8.1.0`                                                       |
| Sui CLI           | `1.73.1`                                                      |

## Known Notes

* Current TypeScript example manifests are aligned on `@switchboard-xyz/on-demand@^3.10.6` and `@switchboard-xyz/common@^5.8.5`. The classic PullFeed example also uses `@switchboard-xyz/common-legacy@^1.1.1`.
* When upgrading from Common `5.8.4`, common-legacy `1.1.0`, or on-demand `3.10.5`, update all three compatible pins together and refresh the application lockfile with its normal package-manager install command.
* on-demand installs common-legacy transitively. Add common-legacy as a direct dependency only when your code imports `LegacyCrossbarClient`.
* `@mysten/sui` latest `2.x` still breaks the import surface used in current docs/examples, so `1.38.0` remains pinned.
* Current Solana Rust examples use `switchboard-on-demand = "0.13.0"`. The advanced Pinocchio price-feed example uses `pinocchio = "0.11.2"` and the `AccountView` API.
* `sui/feeds/basic` defaults its checked-in `Move.toml` to testnet. Use the explicit `build:testnet`, `build:mainnet`, `deploy:testnet`, and `deploy:mainnet` scripts when documenting or verifying flows.


# Overview

Guidance and patterns for building AI agents and LLM-powered workflows with Switchboard.

## AI-Focused Access Points

Use these endpoints to ingest the docs efficiently or to wire tools directly into structured content.

* **Per-Page Markdown** — Append `.md` to any page URL to retrieve Markdown instead of HTML.
* **`/llms.txt` Index** — A site-level index of all pages as Markdown URLs with titles.
* **`/llms-full.txt`** — The entire site content in a single file. Useful for smaller docsets or offline "packs."
* **MCP Server** — Use the MCP endpoint at `docs.switchboard.xyz/~gitbook/mcp` to discover and retrieve docs as structured resources rather than scraping.

## Switchboard Agent Skill

The [Switchboard Agent Skill](/ai-agents-llms/switchboard-agent-skill) defines an autonomous operator for designing, simulating, deploying, updating, and reading Switchboard feeds and randomness across chains. Use it to configure an agent's behavior: copy the skill content into your agent skill registry or system prompt.

### Install the Skill in Your AI Tool

Preferred setup is an Agent Skills folder (a directory containing `SKILL.md`). Create a folder named `switchboard-agent/` and a `SKILL.md` file inside it, then paste the [raw markdown for the skill](https://docs.switchboard.xyz/ai-agents-llms/skills/switchboard-agent-skill.md) into it.

#### Claude Code (Skill folder)

Project-scoped (recommended for repos):

* `.claude/skills/switchboard-agent/SKILL.md`

Personal (all projects):

* `~/.claude/skills/switchboard-agent/SKILL.md`

```bash
mkdir -p .claude/skills/switchboard-agent
# paste into .claude/skills/switchboard-agent/SKILL.md
```

Invoke: `/switchboard-agent <your request>`

#### OpenAI Codex (Skill folder)

Repo-scoped (recommended):

* `.agents/skills/switchboard-agent/SKILL.md`

User-scoped (all repos):

* `~/.agents/skills/switchboard-agent/SKILL.md`

```bash
mkdir -p .agents/skills/switchboard-agent
# paste into .agents/skills/switchboard-agent/SKILL.md
```

Optional (recommended): add a single short line to `AGENTS.md`:

```
Use the switchboard-agent skill for Switchboard feeds/randomness tasks.
```

Invoke: Run `/skills`, or type `$` and select `switchboard-agent`.

#### OpenClaw

Install the Switchboard Data Operator skill from [clawhub.ai](https://clawhub.ai/oakencore/switchboard-data-operator).


# SAIL

SAIL is Switchboard's attestation layer for hardware-backed oracle and runtime verification. It uses Trusted Execution Environments (TEEs), currently AMD SEV-SNP, to provide evidence that critical Switchboard runtime code is running in an isolated environment before that runtime is trusted by the network.

For AI agents and autonomous systems, this matters because the data and services they depend on need a clear trust boundary. SAIL helps answer a narrower, more concrete question: did this Switchboard runtime produce an attestation report showing it is running the expected code in the expected hardware-backed environment?

SAIL does not, by itself, prove that an arbitrary AI model made the right decision, that an agent followed every business rule, or that a smart contract is correct. It provides hardware-backed evidence about the runtime that produced or signed data, which applications can combine with normal on-chain checks, quote verification, and application-level authorization.

## What SAIL Provides

**Hardware-backed runtime evidence** - Switchboard runtimes can produce AMD SEV-SNP attestation evidence that guardians and other verifiers can inspect before trusting the runtime.

**Verified oracle execution** - Switchboard uses attestation to confirm that oracle infrastructure is running approved code before it participates in sensitive network workflows.

**TEE-derived runtime identity** - SAIL exposes helpers for deriving enclave-bound keys, so a runtime can sign or identify itself from material tied to the TEE environment.

**Runtime randomness helpers** - SAIL includes randomness utilities for code running inside these environments. Treat these as low-level runtime helpers, not a replacement for chain-specific Switchboard randomness products.

## Where SAIL Shows Up Today

SAIL is part of the infrastructure behind Switchboard's verified oracle network.

* The [TEE architecture page](/how-it-works/technical-architecture/trusted-execution-environments-tees) explains why Switchboard uses TEEs and AMD SEV-SNP.
* The Surge docs describe price streaming through a SAIL-verified oracle network for [Solana/SVM](/docs-by-chain/solana-svm/surge), [EVM](/docs-by-chain/evm/surge), and [Sui](/docs-by-chain/sui/surge).
* The current advanced SDK surface is [`@switchboard-xyz/sail-sdk`](https://www.npmjs.com/package/@switchboard-xyz/sail-sdk). It is intended for low-level attestation and runtime work, not as a beginner application tutorial.

There is not currently a runnable SAIL example in `sb-on-demand-examples`. Start with the linked architecture and product docs unless you are working directly on TEE runtime integration.

## Current Developer Surface

The SAIL SDK exposes low-level primitives used by Switchboard runtime infrastructure:

* AMD SEV-SNP attestation helpers for generating attestation reports/evidence.
* JSON attestation helpers for newer Confidential Containers (CoCo) environments.
* Verification helpers for attestation evidence, including an optional verifier feature.
* Enclave-derived key helpers for Ed25519 and secp256k1 runtime identities.
* Runtime randomness helpers for TEE-bound code.

These APIs are advanced infrastructure tools. Most application developers should consume Switchboard through the chain-specific feed, randomness, Surge, or Crossbar docs instead of integrating SAIL directly.


# Switchboard Agent Skill

## Purpose

Provide a compact “front door” for all Switchboard work:

* Enforce security/permissions and secret-handling rules consistently
* Normalize terms and identifiers (feed IDs, queues, update payloads)
* Route requests to the correct specialized Switchboard skill(s)
* Produce consistent, integration-ready outputs

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/on-demand@3.10.6`
* `@switchboard-xyz/common@5.8.5`
* `@switchboard-xyz/on-demand-solidity@1.1.0`
* `@switchboard-xyz/sui-sdk@0.1.16`
* `@switchboard-xyz/aptos-sdk@0.1.5`
* `@switchboard-xyz/iota-sdk@0.0.3`
* `switchboard-on-demand = "0.13.0"`
* `switchboard-protos = "0.2.6"`

## Scope

This skill covers:

* Capturing and enforcing an `OperatorPolicy`
* Interpreting intent (feeds vs Surge vs randomness vs Crossbar vs X402)
* Selecting and sequencing specialized skills
* Standard output format for plans and execution steps

Out of scope (handled by specialized skills):

* Chain-specific transaction composition details
* Chain-specific on-chain verifier/consumer code
* Crossbar deployment configuration details

## Subskills

* [Switchboard Solana/SVM Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-solana-svm-feeds)
* [Switchboard EVM Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-evm-feeds)
* [Switchboard Sui Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-sui-feeds)
* [Switchboard Aptos Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-aptos-feeds)
* [Switchboard Iota Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-iota-feeds)
* [Switchboard Movement Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-movement-feeds)
* [Switchboard Feed Design Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-feed-design)
* [Switchboard Crossbar Ops Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-crossbar-ops)
* [Switchboard Surge Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-surge)
* [Switchboard Randomness Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-randomness)
* [Switchboard X402 Micropayments Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-x402)

## Hard Rules: Security & Permissions Contract

### MUST establish `OperatorPolicy` before any of the following

You MUST have an explicit `OperatorPolicy` before you:

* sign transactions (any chain)
* move funds / pay fees
* deploy contracts/programs/packages
* write to on-chain state
* store/persist secrets (private keys, JWTs, API keys)

If missing, ask one compact question set and store answers as `OperatorPolicy`.

### OperatorPolicy (required)

Capture these fields (ask if missing):

1. **Target chain(s)**: Solana/SVM, EVM (chain IDs), Sui, Aptos, Iota, Movement
2. **Network per chain**: mainnet/testnet/devnet (and any custom cluster name)
3. **Autonomy mode**
   * `read_only` (no keys)
   * `plan_only` (no signing; provide exact steps)
   * `execute_with_approval` (propose each tx and wait for approval)
   * `full_autonomy` (execute within constraints)
4. **Spend limits** (required for any execute mode) Ask whether the user would like to set:
   * max per-tx spend (native token + fees)
   * max daily spend
   * max total spend for the task
5. **Allow/Deny lists** Ask whether the user would like to set:
   * allowlist/denylist of program IDs (Solana/SVM), contract addresses (EVM), package IDs (Move/Sui)
   * allowlist/denylist of RPC endpoints
6. **Key custody & handling**
   * where keys come from (file path, keystore, env var, remote signer)
   * whether keys may be persisted (default: NO)
   * whether mainnet signing is allowed (explicit YES required)
7. **Data validation defaults** (overrideable per request)
   * `minResponses` / `minSampleSize`
   * `maxVariance` / `maxDeviationBps`
   * `maxStaleness` / `maxAgeSeconds` / chain-equivalent

### Data Validation Default Presets

Use these as baseline defaults when capturing `OperatorPolicy`, then override per request if needed:

| Preset             | minResponses / minSampleSize | maxDeviationBps | maxVariance (Aptos/Movement/Iota) | maxAgeSeconds | Solana maxStaleness (approx slots) | Sui maxAgeMs | Use case                           |
| ------------------ | ---------------------------: | --------------: | --------------------------------: | ------------: | ---------------------------------: | -----------: | ---------------------------------- |
| Devnet             |                            1 |            1000 |                        1000000000 |           300 |                                750 |       300000 | prototyping / non-critical         |
| Standard (default) |                            2 |             500 |                        1000000000 |            60 |                                150 |        60000 | general production                 |
| High-risk          |                            3 |             200 |                        1000000000 |            30 |                                 75 |        30000 | liquidation / settlement / payouts |

Preset selection rules:

* `devnet` network -> default to `Devnet`.
* mainnet/testnet -> default to `Standard`.
* safety-critical financial logic -> escalate to `High-risk`.

### OperatorPolicy devnet defaults

For quick devnet experimentation, prefill `OperatorPolicy` with these defaults and then ask only for missing high-impact constraints (for example allow/deny lists or custom RPCs):

```yaml
OperatorPolicy (devnet defaults):
  network: devnet
  autonomy: execute_with_approval
  spend_limits: 1 SOL/tx, 10 SOL/day
  key_custody: file path ($HOME/.config/solana/id.json)
  persist_keys: no
  mainnet_signing: no
  minResponses: 1
  minSampleSize: 1
  maxDeviationBps: 1000
  maxVariance: 1000000000
  maxAgeSeconds: 300
  maxStalenessSlots: 750
  maxAgeMs: 300000
```

Notes:

* These are starter defaults, not a bypass of required policy capture.
* For non-Solana chains, keep the same safety posture and translate native token/key custody fields to chain-appropriate values.
* Solana slot values are approximate mappings for documentation convenience (\~400ms/slot).
* `maxVariance` is chain-native and kept at `1e9` baseline unless explicitly overridden; for these chain parameters, `1e9` means `1%`.
* Raw v2 `OracleFeed.maxJobRangePct` uses the same `1e9` percent scale, but `MedianTask.max_range_percent` is a human percent string. See `custom-feeds/advanced-feed-configuration/feed-parameter-units.md` for the surface-specific units.

### Secret handling (mandatory)

* NEVER print secrets, private keys, seed phrases, API tokens, Pinata JWTs, or full `.env` contents.
* If referencing a secret, use placeholder names (e.g., `$PINATA_JWT_KEY`, `$API_KEY`).
* Prefer encrypted keystores / secret managers.
* Never recommend `export PRIVATE_KEY=...` in shell history.

## Core Concepts and Terms

### Pull-based oracle model

* Data is fetched off-chain and then submitted on-chain for verification and use.
* For safety-critical logic, update and read should be atomic (same tx / same entry call), where the chain supports it.

### Feed identifiers (normalized)

Use these names consistently:

* **`feedId`**: 32-byte identifier (commonly `0x` + 64 hex chars).
* **`feedDefinition`**: job pipelines (`OracleJob[]`) describing how to compute values.
* **`queueId`**: oracle subnet/queue identifier (chain-specific type).
* **`updatePayload`**: chain-specific proof/data used for on-chain verification.

Note: Some SDKs/docs say “feed hash” for the same 32-byte `feedId`. Treat the 32-byte identifier as `feedId` unless explicitly dealing with content-addressed feed definition storage.

### Variable overrides (security invariant)

* Variable overrides (`${VAR}`) are for secrets only (API keys, auth tokens, payment headers).
* Do not use overrides for URLs, JSON paths, IDs, multipliers, selectors, or anything that changes data selection logic.

## Routing Logic

### Step 1: classify the request

Determine intent:

* Feeds
* Custom feed design
* Crossbar
* Surge streaming
* Randomness
* X402 micropayments

Determine chain:

* Solana/SVM
* EVM
* Sui
* Aptos
* Iota
* Movement

### Step 2: route to specialized skills

Chain routing:

* Solana/SVM feeds → `switchboard-solana-svm-feeds`
* EVM feeds → `switchboard-evm-feeds`
* Sui feeds → `switchboard-sui-feeds`
* Aptos feeds → `switchboard-aptos-feeds`
* Iota feeds → `switchboard-iota-feeds`
* Movement feeds → `switchboard-movement-feeds`

Feature routing:

* Feed design → `switchboard-feed-design`
* Simulation/store/self-host → `switchboard-crossbar-ops`
* Streaming (Surge) → `switchboard-surge`
* Randomness → `switchboard-randomness`
* X402 micropayments → `switchboard-x402`

### Step 3: common multi-skill sequences

* “I need a new feed”
  * `switchboard-feed-design` → `switchboard-crossbar-ops` → chain feed skill
* “Integrate an existing feed”
  * chain feed skill → optional `switchboard-crossbar-ops` (simulate)
* “Use Surge prices on-chain”
  * `switchboard-surge` → chain feed skill (settlement path)
* “Use randomness”
  * `switchboard-randomness` → (optional) chain skill for integration

## Minimal Example

Route one request to the right subskill and emit the standard output sections:

```json
{
  "request": "Use BTC/USD on Solana with atomic update+use",
  "classifiedIntent": "feeds",
  "chain": "solana-svm",
  "selectedSkills": ["switchboard-solana-svm-feeds"],
  "outputSections": [
    "Summary",
    "Assumptions",
    "OperatorPolicy",
    "Plan",
    "Execution Steps",
    "Rollback / Recovery",
    "Risks & Mitigations",
    "Next Actions"
  ]
}
```

## Getting Started: First Solana Feed in 5 Minutes (Devnet)

Objective: create a minimal BTC/USD feed definition, store it in Crossbar, and then update+read it on Solana in one transaction.

Flow map: `switchboard-feed-design -> switchboard-crossbar-ops -> switchboard-solana-svm-feeds`

### Step 1: Define a minimal single-source BTC/USD job

Use one source for fastest first success. This is a devnet quickstart baseline.

### Step 2: Store the job in Crossbar and capture feed identifier

### Step 3: Simulate once to validate result shape

```bash
set -euo pipefail

CROSSBAR="https://crossbar.switchboard.xyz"

cat > /tmp/btc-usd-job.json <<'JSON'
{
  "jobs": [
    {
      "tasks": [
        {
          "httpTask": {
            "url": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
            "method": "METHOD_GET"
          }
        },
        {
          "jsonParseTask": {
            "path": "$.price"
          }
        },
        {
          "multiplyTask": {
            "big": "100000000"
          }
        }
      ]
    }
  ]
}
JSON

# Store the definition
curl -sS -X POST "$CROSSBAR/store" \
  -H "content-type: application/json" \
  -d @/tmp/btc-usd-job.json \
  | tee /tmp/store-response.json | jq .

# Extract identifier across common response shapes (feedId/feedHash)
FEED_ID="$(jq -r '.feedId // .feedHash // .result.feedId // .result.feedHash // .data.feedId // .data.feedHash // empty' /tmp/store-response.json)"
if [ -z "$FEED_ID" ]; then
  echo "Could not find feed identifier in /store response" >&2
  exit 1
fi
echo "Feed identifier: $FEED_ID"

# Validate once with simulation
curl -sS "$CROSSBAR/simulate/$FEED_ID" | jq .
```

### Step 4: Run minimal Solana update+read transaction

For new Solana/SVM feed-hash integrations, use managed quote-program updates and canonical quote accounts. Do not suggest `PullFeed.fetchUpdateIx(...)` unless the user is maintaining an existing classic PullFeed account and has confirmed queue/gateway support for that legacy path.

```ts
import * as sb from "@switchboard-xyz/on-demand";
import { OracleQuote } from "@switchboard-xyz/on-demand";

async function main() {
  // Assumes Solana CLI/devnet + wallet env are already configured.
  const { connection, keypair, queue, crossbar, program } = await sb.AnchorUtils.loadEnv();
  const feedId = process.env.FEED_ID!;
  if (!feedId) throw new Error("Set FEED_ID from the /store response");

  // Canonical quote PDA for this queue + feed.
  const [quoteAccount] = OracleQuote.getCanonicalPubkey(queue.pubkey, [feedId]);

  const updateIxs = await queue.fetchManagedUpdateIxs(crossbar, [feedId], {
    numSignatures: 1,
    payer: keypair.publicKey,
    variableOverrides: {},
  });

  // Placeholder consumer read instruction (for example, readOracleData in your Anchor program).
  const consumerIx = await program.methods
    .readOracleData()
    .accounts({ quoteAccount })
    .instruction();

  // Atomic ordering rule: Switchboard update instructions first, then consumer instruction.
  const tx = await sb.asV0Tx({
    connection,
    ixs: [...updateIxs, consumerIx],
    signers: [keypair],
  });

  const sim = await connection.simulateTransaction(tx);
  if (sim.value.err) throw new Error(JSON.stringify(sim.value.err));
  const sig = await connection.sendTransaction(tx);
  console.log("tx:", sig);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
```

### Step 5: Confirm output and next hardening step

Success criteria:

* `/simulate/{id}` returns numeric results.
* The transaction simulates and sends successfully.
* Program logs show the feed was read from the canonical quote account.

Next hardening step:

* Replace the single-source job with a multi-source median template and tighten validation defaults (`minResponses`, staleness, and deviation limits) per your `OperatorPolicy`.

> **Caution:** Crossbar/docs may use `feedHash` and some SDK flows say `feedId` for the same 32-byte identifier. In this quickstart, use the identifier returned by `/store` as the `feedId` input to update calls. This is a devnet-first flow; for production, use multi-source jobs and stricter policy constraints.

Quick links:

* [Switchboard Feed Design Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-feed-design)
* [Switchboard Crossbar Ops Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-crossbar-ops)
* [Switchboard Solana/SVM Feeds Skill](/ai-agents-llms/switchboard-agent-skill/switchboard-solana-svm-feeds)
* [Basic Price Feed Tutorial](/docs-by-chain/solana-svm/price-feeds/basic-price-feed)

## Standard Output Format

When producing artifacts, use these headings:

1. Summary
2. Assumptions
3. OperatorPolicy
4. Plan
5. Execution Steps (only if allowed)
6. Rollback / Recovery
7. Risks & Mitigations
8. Next Actions

## References

* <https://docs.switchboard.xyz/>
* <https://docs.switchboard.xyz/tooling/crossbar>
* <https://docs.switchboard.xyz/custom-feeds/task-types>
* <https://docs.switchboard.xyz/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides>


# Switchboard Solana/SVM Feeds Skill

## Purpose

Integrate Switchboard on-demand feeds into Solana/SVM transactions and programs:

* Compose transactions that verify/update and then consume feed values correctly
* Implement on-chain verification patterns (Anchor/Rust) when your program consumes feeds
* Support “cranking” patterns to keep canonical quote accounts warm (push-like behavior)

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/on-demand@3.10.6`
* `@switchboard-xyz/common@5.8.5`
* `@solana/web3.js@1.98.0`
* `@coral-xyz/anchor@0.31.1` (TypeScript client)
* `switchboard-on-demand = "0.13.0"` (Rust on-chain)

This skill is about integration correctness, not designing new feed definitions (handled by `switchboard-feed-design`).

## Preconditions

* `OperatorPolicy` exists (network, RPC allowlist, autonomy/spend limits).

## Inputs to Collect

* `network`: mainnet-beta / devnet / custom
* `rpcUrl` (optional override)
* `crossbarUrl` (public or self-hosted)
* `feedId` list (32-byte `0x...` hex)
* consumer program ID / instruction shape (if integrating)
* validation targets: max staleness (slots), max deviation, min responses

## Solana/SVM Integration Invariants

* Consumer instruction must occur after Switchboard verification/update instructions in the same transaction.
* Use deterministic/canonical accounts; do not accept arbitrary “quote accounts” without canonical checks.
* New feed-hash integrations use `queue.fetchManagedUpdateIxs(...)` and canonical quote-program accounts. Do not suggest `PullFeed.fetchUpdateIx(...)` unless the user is maintaining an existing classic PullFeed account and confirms queue/gateway support for that legacy path.
* Variable overrides are secrets-only (never selectors/URLs/paths/IDs/multipliers).

## Minimal Example

```ts
const updateIxs = await queue.fetchManagedUpdateIxs(crossbar, [feedId], {
  numSignatures: 1,
  payer: keypair.publicKey,
  variableOverrides: {},
});

const tx = await sb.asV0Tx({
  connection,
  ixs: [...updateIxs, consumerIx],
  signers: [keypair],
});

await connection.sendTransaction(tx);
```

## Playbook

### 1) Resolve queue and canonical accounts

* Load the correct oracle queue for the network (default unless user specifies).
* Derive or verify canonical quote storage addresses as required by the SDK/program pattern.
* If your program accepts a quote account, enforce that it is canonical for the queue + feedId.

### 2) Update + consume in one transaction (TypeScript skeleton)

Goal: fetch Switchboard-managed update instructions, then call the consumer instruction.

```ts
import * as sb from "@switchboard-xyz/on-demand";

const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
const queue = await sb.Queue.loadDefault(program!);

const crossbar = new sb.Crossbar({
  rpcUrl: connection.rpcEndpoint,
  // NOTE: exact constructor args can differ by SDK version; treat as placeholder.
});

// Managed update instructions (must be BEFORE your consumer ix)
const updateIxs = await queue.fetchManagedUpdateIxs(crossbar, [feedId], {
  numSignatures: 1,
  variableOverrides: {},   // secrets only
  payer: keypair.publicKey,
});

// Your program instruction that reads verified data
const consumerIx = await buildYourConsumerIx(/* programId, accounts, args */);

const tx = await sb.asV0Tx({
  connection,
  ixs: [...updateIxs, consumerIx],
  signers: [keypair],
});

await connection.sendTransaction(tx);
```

Indexing rule:

* Keep each Ed25519/quote-program pair returned by `fetchManagedUpdateIxs` adjacent. `asV0Tx` resolves the final indices after all setup and consumer instructions are assembled.
* If you compile the transaction yourself, call `finalizeManagedUpdateInstructions` on the complete ordered instruction array immediately before compilation.

### 3) On-chain verification (Rust/Anchor pattern)

Use the on-demand verifier and enforce staleness:

```rust
use switchboard_on_demand::QuoteVerifier;

let quote = QuoteVerifier::new()
    .queue(&ctx.accounts.queue)
    .slothash_sysvar(&ctx.accounts.slothashes)
    .ix_sysvar(&ctx.accounts.instructions)
    .clock_slot(Clock::get()?.slot)
    .max_age(50)
    .verify_instruction_at(0)?;

for feed in quote.feeds() {
    // feed.value(), feed.decimals(), feed.hex_id(), etc.
}
```

> **Note:** `quote.feeds()` contains feed outputs (such as price values). Randomness uses a different path: read bytes from `RandomnessAccountData::get_value(...)` in the [Randomness Tutorial](/docs-by-chain/solana-svm/randomness/randomness-tutorial).

Also enforce, when applicable:

* canonical quote address constraints
* deviation checks vs stored “last good” value for high-risk logic

### 4) Cranking pattern (push-like / heartbeat imitation)

Use this when you want other transactions/users to read the **most recently cranked** value from the canonical quote account without providing updates every time.

Tradeoffs:

* Pros: readers can do cheaper reads; UI dashboards can stay warm.
* Cons: loses atomic update+use guarantees; must enforce staleness at read-time.

Minimal crank loop: send a transaction that contains only the managed update instructions.

```ts
async function crankOnce(feedId: string) {
  const updateIxs = await queue.fetchManagedUpdateIxs(crossbar, [feedId], {
    numSignatures: 1,
    payer: keypair.publicKey,
    variableOverrides: {},
  });

  const tx = await sb.asV0Tx({
    connection,
    ixs: [...updateIxs],
    signers: [keypair],
  });

  return connection.sendTransaction(tx);
}

// Example: crank every 10 seconds (tune to your needs/costs)
setInterval(() => crankOnce(feedId).catch(console.error), 10_000);
```

Operational guidance:

* Run cranks from a dedicated wallet with explicit spend limits.
* Monitor failures and staleness; treat missed cranks as “feed stale” for consumers.

## Outputs

Produce a `SolanaFeedIntegrationPlan` including:

* network + RPC/Crossbar URL
* feedId(s) and queue resolution method
* exact instruction ordering (list)
* consumer instruction shape (accounts/args) if integrating
* optional safety policy (staleness/deviation/signatures) if requested
* crank plan (if requested): cadence, cost bounds, monitoring

## Troubleshooting Checklist

* Signature verification index mismatch → re-check `instructionIdx` vs final tx instruction order
* Missing sysvars → include SlotHashes + Instructions sysvars in accounts
* Non-canonical quote account → derive canonical address; reject non-canonical inputs
* `pullFeedSubmitResponseConsensus` or `PullFeed.fetchUpdateIx(...)` returns `ORACLE_UNAVAILABLE`, but managed Ed25519 quote updates work → user is on the legacy PullFeed path; move to `queue.fetchManagedUpdateIxs(...)` and canonical quote accounts
* Successful simulation but signed updates fail with `RangeExceeded` → feed validation issue; check raw v2 `maxJobRangePct` scaling separately from PullFeed-vs-quote-program routing
* Compute limits → add compute budget ixs; reduce feeds/oracles per tx

## References

* <https://docs.switchboard.xyz/docs-by-chain/solana-svm>
* <https://docs.switchboard.xyz/docs-by-chain/solana-svm/price-feeds>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard EVM Feeds Skill

## Purpose

Integrate Switchboard on-demand feeds into EVM contracts and bots:

* Fetch verifiable update payloads off-chain
* Submit updates on-chain via the Switchboard contract (pay required fee)
* Read verified feed results and enforce freshness/deviation constraints
* Support “cranking” patterns to emulate push/heartbeat feeds

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/common@5.8.5`
* `@switchboard-xyz/on-demand-solidity@1.1.0`
* `ethers@6.13.1`

## Preconditions

* `OperatorPolicy` exists (chainId, RPC allowlist, contract allowlist, spend limits).

## Inputs to Collect

Always collect:

* `chainId` + network name
* `rpcUrl`
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* `switchboardContractAddress` (resolve from official deployments)
* `feedId` list (bytes32)

Collect safety policy only if relevant (risk-sensitive logic) or requested:

* `maxAgeSeconds`
* `maxDeviationBps`

## EVM Integration Invariants

* Always compute and pay fee (e.g., `getFee`) before `updateFeeds`.
* For safety-critical logic, accept `updates` as calldata and do update+read inside the same app function.

## Minimal Example

```solidity
function updateAndRead(bytes[] calldata updates, bytes32 feedId) external payable {
    uint256 fee = switchboard.getFee(updates);
    switchboard.updateFeeds{value: fee}(updates);
    switchboard.latestUpdate(feedId);
}
```

## Playbook

### 1) Resolve deployments and feeds

* Resolve `switchboardContractAddress` from official docs for the chain/network.
* Confirm it is allowlisted by `OperatorPolicy`.
* Obtain `feedId`(s) for the same network.

### 2) Fetch update payloads off-chain (Crossbar)

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

const crossbarUrl = process.env.CROSSBAR_URL ?? "https://crossbar.switchboard.xyz";
const crossbar = new CrossbarClient(crossbarUrl);

// For Feed Builder/custom feeds, use the v2 feed-hash flow.
await crossbar.simulateFeed(feedId, false, undefined, network);

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

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

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

### 3) Contract-side recommended pattern (atomic update+use)

```solidity
function updateAndUse(bytes[] calldata updates, bytes32[] calldata feedIds) external payable {
    uint256 fee = switchboard.getFee(updates);
    require(msg.value >= fee, "InsufficientFee");

    switchboard.updateFeeds{value: fee}(updates);

    for (uint256 i = 0; i < feedIds.length; i++) {
        // Read latest verified update for feedIds[i]
        // Enforce maxAgeSeconds and maxDeviationBps in your app logic
    }

    // Optional refund of msg.value - fee
}
```

### 4) Client-side submit flow

```ts
import { ethers } from "ethers";

const switchboard = new ethers.Contract(switchboardContractAddress, SWITCHBOARD_ABI, signer);

const fee = await switchboard.getFee(updates);
const tx = await switchboard.updateFeeds(updates, { value: fee });
await tx.wait();

const latest = await switchboard.latestUpdate(feedId);
```

### 5) Cranking pattern (push-like / heartbeat imitation)

Use this when you want other callers to read `latestUpdate(feedId)` without providing `updates` each time.

Tradeoffs:

* Pros: cheaper reads for many consumers; simpler UI integrations.
* Cons: data can go stale between cranks; consumers must enforce staleness.

Crank loop:

```ts
async function crankOnce(feedIds: string[]) {
  const response = await crossbar.fetchV2Update(feedIds, {
    chain: "evm",
    network,
    use_timestamp: true,
  });

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

  const updates = [response.encoded];

  const fee = await switchboard.getFee(updates);
  const tx = await switchboard.updateFeeds(updates, { value: fee });
  await tx.wait();
}

// Example: crank every 15 seconds (tune to costs and requirements)
setInterval(() => crankOnce([feedId]).catch(console.error), 15_000);
```

Operational guidance:

* Use a dedicated keeper wallet with explicit spend limits.
* For liquidation/settlement flows, prefer atomic update+use even if a crank exists.

## Outputs

Produce an `EvmFeedIntegrationPlan` including:

* chainId/network + RPC/Crossbar URL
* resolved Switchboard contract address (and source)
* feedId(s)
* atomic update+use pattern (recommended) vs crank pattern (optional)
* fee strategy with spend caps
* optional safety policy (maxAge/maxDeviation) if requested

## Troubleshooting Checklist

* Fee too low → always call `getFee(updates)` and set `msg.value`
* Stale timestamp → fetch fresh updates; raise max age only for non-critical paths
* Wrong feed/network → verify feedId and deployment match chainId/network
* Feed Builder custom feed on EVM → use `simulateFeed(...)` and `fetchV2Update(...)`, not `fetchEVMResults(...)`
* `ORACLE_UNAVAILABLE` after successful simulation -> check managed oracle/gateway availability and oracle-side validation errors such as `RangeExceeded`; raw v2 `maxJobRangePct` must be scaled by `1e9`

## References

* <https://docs.switchboard.xyz/docs-by-chain/evm>
* <https://docs.switchboard.xyz/docs-by-chain/evm/price-feeds>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Sui Feeds Skill

## Purpose

Integrate Switchboard on-demand feeds into Sui Move contracts using the Quote Verifier pattern:

* Fetch oracle quotes off-chain and attach to a Sui transaction
* Verify quotes on-chain against the correct oracle queue
* Enforce freshness/deviation constraints in Move
* Support “cranking” patterns to keep on-chain consumer state warm (push-like behavior)

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/sui-sdk@0.1.16`
* `@switchboard-xyz/on-demand@3.10.6`
* `@mysten/sui@1.38.0`

## Preconditions

* `OperatorPolicy` exists (Sui network, RPC allowlist, signer custody).

## Inputs to Collect

Always collect:

* `network`: mainnet / testnet
* `rpcUrl`
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* Switchboard deployment package ID (resolve from official docs)
* consumer package ID + consumer object ID
* `feedId` list (32-byte hex)
* `numOracles`

Collect safety policy only if relevant (risk-sensitive logic) or requested:

* `maxAgeMs`
* `maxDeviationBps`

## Sui Integration Invariants

* Verify quotes against the queue from `SwitchboardClient.fetchState()`.
* Verify before use; apply explicit staleness/deviation checks.

## Minimal Example

```ts
const tx = new Transaction();
const quotes = await Quote.fetchUpdateQuote(sb, tx, {
  feedHashes: [feedId],
  numOracles,
});

tx.moveCall({
  target: `${consumerPackageId}::module::update_price`,
  arguments: [tx.object(consumerObjectId), quotes, tx.pure.vector("u8", feedIdBytes), tx.object("0x6")],
});

await suiClient.signAndExecuteTransaction({ signer: keypair, transaction: tx });
```

## Playbook

### 1) Resolve queue and feeds

* Fetch Switchboard state to identify `oracleQueueId`.
* Confirm feed IDs exist for the chosen network.

### 2) Transaction flow (TypeScript skeleton)

```ts
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";
import { SwitchboardClient, Quote } from "@switchboard-xyz/sui-sdk";

const crossbarUrl = process.env.CROSSBAR_URL ?? "https://crossbar.switchboard.xyz";

const suiClient = new SuiClient({ url: rpcUrl });
const sb = new SwitchboardClient(suiClient);
const state = await sb.fetchState();

const tx = new Transaction();

const quotes = await Quote.fetchUpdateQuote(sb, tx, {
  feedHashes: [feedId],
  numOracles,
});

tx.moveCall({
  target: `${consumerPackageId}::module::update_price`,
  arguments: [
    tx.object(consumerObjectId),
    quotes,
    tx.pure.vector("u8", feedIdBytes),
    tx.object("0x6"), // Clock
  ],
});
```

### 3) Cranking pattern (push-like / heartbeat imitation)

Use this when you want the consumer object to hold a “recently verified” value so other txs can read it cheaply without attaching quotes every time.

Tradeoffs:

* Pros: cheaper reads for many consumers; simpler UI.
* Cons: consumer must enforce staleness; values can go stale between cranks.

Crank loop concept:

* Periodically execute the `update_price` (or your equivalent) Move entry function with fresh quotes.

```ts
async function crankOnce() {
  const tx = new Transaction();

  const quotes = await Quote.fetchUpdateQuote(sb, tx, {
    feedHashes: [feedId],
    numOracles,
  });

  tx.moveCall({
    target: `${consumerPackageId}::module::update_price`,
    arguments: [
      tx.object(consumerObjectId),
      quotes,
      tx.pure.vector("u8", feedIdBytes),
      tx.object("0x6"),
    ],
  });

  // signAndExecuteTransaction must follow OperatorPolicy rules
  return suiClient.signAndExecuteTransaction({ signer: keypair, transaction: tx });
}

// Example cadence (tune to requirements/cost)
setInterval(() => crankOnce().catch(console.error), 15_000);
```

## Outputs

Produce a `SuiFeedIntegrationPlan` including:

* network/RPC/Crossbar URL
* Switchboard package resolution method + queue ID resolution
* consumer object update/read strategy:
  * atomic quote+use, or
  * cranked storage + staleness checks
* optional safety policy if requested

## Troubleshooting Checklist

* feed not found in quotes → ensure requested feedId included in `fetchUpdateQuote`
* quote expired → fetch again; adjust max age only for non-critical flows
* wrong queue/network → verify package IDs and network match

## References

* <https://docs.switchboard.xyz/docs-by-chain/sui>
* <https://docs.switchboard.xyz/docs-by-chain/sui/price-feeds>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Aptos Feeds Skill

## Purpose

Use Switchboard on-demand feeds on Aptos:

* crank/update feeds client-side (pull model)
* consume verified results in Move
* enforce freshness/deviation policies in app logic

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/aptos-sdk@0.1.5`
* `@switchboard-xyz/common@5.8.5`
* `@aptos-labs/ts-sdk@6.1.0`

## Preconditions

* `OperatorPolicy` exists (Aptos network, signer custody, RPC allowlist).

## Inputs to Collect

* network (mainnet/testnet)
* RPC endpoint
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* aggregator/feed identifiers (addresses/object IDs)
* safety policy (staleness/deviation/min responses) only if risk-sensitive

## Invariants

* Pull-based: client must crank/update feeds to keep data fresh.
* Ensure update action is executed before reading within the same flow (where applicable).

## Playbook (high-level)

* Off-chain:
  * fetch/update payload(s) for the feed/aggregator
  * submit transaction to run the update action (or include it in the same entry call if supported)
* On-chain:
  * read current result + timestamp
  * enforce staleness and deviation vs last stored value

## Minimal Example

```move
use aptos_framework::aptos_coin::AptosCoin;
use aptos_framework::object::{Self, Object};
use switchboard::aggregator::{Self, Aggregator, CurrentResult};
use switchboard::update_action;

public entry fun read_feed(account: &signer, update_data: vector<vector<u8>>) {
    update_action::run<AptosCoin>(account, update_data);
    let feed: Object<Aggregator> = object::address_to_object<Aggregator>(@0xSomeFeedAddress);
    let current: CurrentResult = aggregator::current_result(feed);
    let _price = aggregator::result(&current);
}
```

## Outputs

Produce an `AptosFeedIntegrationPlan` including:

* identifiers and network alignment checks
* crank strategy (if requested)
* Move consumption point and validation policy (if requested)

## References

* <https://docs.switchboard.xyz/docs-by-chain/aptos>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Iota Feeds Skill

## Purpose

Use Switchboard on-demand feeds on Iota:

* store job definitions and create aggregators
* crank updates using Iota transactions (pull model)
* consume results in Move with freshness/deviation checks

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/iota-sdk@0.0.3`
* `@switchboard-xyz/common@5.8.5`
* `@iota/iota-sdk@1.11.0`

## Preconditions

* `OperatorPolicy` exists (Iota network, signer custody, RPC allowlist).

## Inputs to Collect

* network (mainnet/testnet)
* RPC endpoint
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* aggregator/feed object IDs
* safety policy (staleness/deviation/min responses) only if risk-sensitive

## Invariants

* Pull-based: updates must be executed client-side.
* Transaction ordering matters: update before consume.

## Playbook (high-level)

1. Resolve Switchboard state/queue for the network.
2. Store jobs (if creating a new feed) and initialize aggregator.
3. Fetch update transaction/actions and execute them.
4. Call consumer Move function after update actions.
5. Enforce staleness/deviation in Move.

## Minimal Example

```ts
import { Aggregator, SwitchboardClient } from "@switchboard-xyz/iota-sdk";
import { Transaction } from "@iota/iota-sdk/transactions";

const sb = new SwitchboardClient(iotaClient);
const aggregator = new Aggregator(sb, aggregatorId);

const tx = new Transaction();
await aggregator.fetchUpdateTx(tx);
await iotaClient.signAndExecuteTransaction({ signer: keypair, transaction: tx });
```

## References

* <https://docs.switchboard.xyz/docs-by-chain/iota>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Movement Feeds Skill

## Purpose

Use Switchboard on-demand feeds on Movement:

* crank/update feeds client-side (pull model)
* integrate verified values into Move-based application logic
* apply freshness/deviation validation policies appropriate to risk

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/aptos-sdk@0.1.5`
* `@switchboard-xyz/common@5.8.5`
* `@aptos-labs/ts-sdk@6.1.0`

## Preconditions

* `OperatorPolicy` exists (Movement network, signer custody, RPC allowlist).

## Inputs to Collect

Always collect:

* network (mainnet/testnet)
* RPC endpoint
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* feed/aggregator identifiers used by Movement integration

Collect validation thresholds only if relevant (risk-sensitive) or requested:

* `maxStaleness` (time-based freshness requirement)
* `maxDeviationBps` (sanity bound vs last value/expected value)
* `minResponses` / `minSampleSize` (how many oracle responses/signatures you require per update)

## Invariants

* Pull-based: client must crank/update feeds to keep data fresh.
* Ensure update occurs before read/use within the same flow recommended by Movement docs.

## Playbook (high-level)

1. Discover feed identifiers for the target network.
2. Crank/update feed using the Movement SDK flow.
3. In Move, read latest value + timestamp and enforce staleness/deviation as needed.
4. Define failure mode: pause/guard high-risk actions when stale or deviating.

## Minimal Example

```move
use aptos_framework::aptos_coin::AptosCoin;
use aptos_framework::object::{Self, Object};
use on_demand::aggregator::{Self, Aggregator, CurrentResult};
use on_demand::update_action;

public entry fun read_feed(account: &signer, update_data: vector<vector<u8>>) {
    update_action::run<AptosCoin>(account, update_data);
    let feed: Object<Aggregator> = object::address_to_object<Aggregator>(@0xSomeFeedAddress);
    let current: CurrentResult = aggregator::current_result(feed);
    let _price = aggregator::result(&current);
}
```

## References

* <https://docs.switchboard.xyz/docs-by-chain/movement>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Feed Design Skill

## Purpose

Turn a data requirement into a robust, verifiable feed definition:

* Design `OracleJob[]` pipelines with stable parsing and source diversity
* Normalize scaling/decimals consistently
* Choose aggregation strategy and consumer validation policy defaults
* Identify and mitigate substitution, outliers, and schema brittleness risks

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/common@5.8.5`

This skill designs feed definitions. Integration details (atomic update+use, on-chain verifier patterns) belong to chain-specific skills.

## Preconditions

* `OperatorPolicy` exists (especially if paid APIs, X402, or job storage is involved).

## Inputs to Collect

Required:

* target metric (price/index/rate/event outcome) and units
* target chain(s) and consumer type (how the value will be used)
* value-at-risk / criticality (UI display vs liquidation vs settlement)
* allowed/forbidden sources
* required secrets (API keys, auth headers, payment headers)

Optional (only if relevant):

* expected request frequency / external API rate limits
  * This matters if the user plans to run a crank/keeper or high-frequency bots.
  * On-demand does not impose a cadence by itself, but your infrastructure and upstream APIs still do.

## Security Invariants

* Variable overrides are secrets-only.
* Hardcode market IDs, selectors, URLs, JSON paths, multipliers in the job definition.
* Prefer 3+ independent sources when possible.

## Minimal Example

```json
{
  "tasks": [
    { "httpTask": { "url": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT" } },
    { "jsonParseTask": { "path": "$.price" } },
    { "multiplyTask": { "big": "1e18" } }
  ]
}
```

## Playbook

### 1) Specify output contract

Define:

* numeric scaling (e.g., 1e18)
* signedness (allow negative or not)
* bounds and failure mode (reject vs clamp)

### 2) Choose sources

* diversify upstream origins (avoid mirrored endpoints)
* prefer reliable schemas with stable versioning
* add at least one fallback source where possible
* commonly used free spot APIs (availability varies by region): Binance, Coinbase, Kraken, Bitstamp, OKX

### 3) Production templates

Template 1: Single source (tutorial baseline)

```json
{
  "tasks": [
    {
      "httpTask": {
        "url": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
        "method": "METHOD_GET"
      }
    },
    { "jsonParseTask": { "path": "$.price" } },
    { "multiplyTask": { "big": "100000000" } }
  ]
}
```

Template 2: 3-source median (production minimum, 2 of 3 required)

```json
{
  "tasks": [
    {
      "medianTask": {
        "min_successful_required": 2,
        "jobs": [
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.price" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.kraken.com/0/public/Ticker?pair=XBTUSD",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.result.XXBTZUSD.c.0" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.coinbase.com/v2/prices/spot?currency=USD",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.data.amount" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          }
        ]
      }
    }
  ]
}
```

Template 3: 5-source with fallback tolerance (production recommended). In `MedianTask`, `max_range_percent` is a human percent string.

```json
{
  "tasks": [
    {
      "medianTask": {
        "min_successful_required": 3,
        "max_range_percent": "2.5",
        "jobs": [
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.price" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.coinbase.com/v2/prices/spot?currency=USD",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.data.amount" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://api.kraken.com/0/public/Ticker?pair=XBTUSD",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.result.XXBTZUSD.c.0" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://www.bitstamp.net/api/v2/ticker/btcusd/",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.last" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          },
          {
            "tasks": [
              {
                "httpTask": {
                  "url": "https://www.okx.com/api/v5/market/ticker?instId=BTC-USD",
                  "method": "METHOD_GET"
                }
              },
              { "jsonParseTask": { "path": "$.data[0].last" } },
              { "multiplyTask": { "big": "100000000" } }
            ]
          }
        ]
      }
    }
  ]
}
```

Template 4: Custom API with auth headers (variable override pattern)

```json
{
  "tasks": [
    {
      "httpTask": {
        "url": "https://api.provider.com/v1/markets/btc-usd/spot",
        "method": "METHOD_GET",
        "headers": [
          { "key": "Authorization", "value": "Bearer ${ACCESS_TOKEN}" },
          { "key": "X-API-Key", "value": "${API_KEY}" }
        ]
      }
    },
    { "jsonParseTask": { "path": "$.data.price" } },
    { "multiplyTask": { "big": "100000000" } }
  ]
}
```

Variable override rule reminder:

* only use `${...}` for auth credentials (keys/tokens)
* never use `${...}` for URLs, paths, symbols, multipliers, or selection logic

### 4) Error handling and rate limiting

* set `min_successful_required` so one source outage does not break updates (for example 2-of-3, 3-of-5)
* set `max_range_percent` for drift/outlier protection before median is accepted; this `MedianTask` field is a human percent string, not the raw v2 feed-level `maxJobRangePct` scale
* stagger polling and cap request rate to provider quotas (especially free tiers)
* back off exponentially on HTTP 429/5xx and keep at least 3 independent sources

### 5) Recommended Validation Defaults (suggest, don’t enforce)

These are primarily **consumer policy defaults** (freshness/deviation) and **oracle sample requirements** (responses/signatures) that you apply when verifying/using the feed.

* Start from `OperatorPolicy` presets in the top-level [Switchboard Skill](/ai-agents-llms/switchboard-agent-skill), then tune per feed risk and volatility.
* `minResponses` / `minSampleSize`: 3 for higher-risk flows; 1 for dev/non-critical
* aggregation: median (or median-of-means where supported)
* deviation:
  * majors: 100–200 bps for high-risk flows; \~500 bps for standard flows
  * long-tail / volatile: wider deviation may be needed as an explicit override from preset defaults
* staleness:
  * bots/liquidations: 15–60 seconds (or chain-equivalent)
  * UI/general: 60–300 seconds

### 6) Produce outputs

Produce a `FeedBlueprint` including:

* `OracleJob[]` JSON
* sources + rationale
* aggregation choice
* required secrets and override variable names
* suggested consumer validation defaults (staleness/deviation/min responses)
* simulation plan (Crossbar or local job runner)

## References

* <https://docs.switchboard.xyz/custom-feeds/task-types>
* <https://docs.switchboard.xyz/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard Crossbar Ops Skill

## Purpose

Operate and use Crossbar for:

* Simulating feeds (QA before deployment)
* Storing/pinning feed definitions and obtaining a `feedId`
* Running the v2 feed-hash flow for current EVM custom feeds
* Fetching chain-specific update payloads (instructions/bytes)
* Running reliable high-throughput bot and UI backends

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/common@5.8.5`
* `@switchboard-xyz/cli@3.5.12` (optional for CLI workflows)

## Defaults

* Default `crossbarUrl`: `https://crossbar.switchboard.xyz` (public instance for quick testing)
* Recommend self-hosting Crossbar for frequent simulations/updates to avoid disruptions.

## Preconditions

* `OperatorPolicy` exists.
* If storing jobs/self-hosting: confirm secret handling policy for IPFS credentials.

## Inputs to Collect

Always collect:

* `crossbarUrl` (default public instance unless user requests self-host)
* chain RPC endpoints (allowlisted)

Only collect if needed:

* IPFS config (Pinata JWT or Kubo URL) if storing definitions
* expected throughput (simulation volume, update frequency)

## Playbook

### 1) Public vs self-hosted

* Public Crossbar: dev/testing, low volume.
* Self-host: production, higher volume, strict endpoint policies, frequent simulation.

### 2) Self-host (Docker Compose) — high-level steps

1. create `docker-compose.yml`
2. create `.env` with RPC + IPFS credentials (never print secrets)
3. run `docker-compose up -d`
4. verify health on configured ports

Common defaults:

* HTTP port: 8080
* WebSocket port: 8081

### 3) Core operations

* Store definitions → return a 32-byte feed identifier (`feedId`)
* Simulate feeds → obtain sample values/errors for QA
* Fetch update payloads:
  * Solana: instruction bundles
  * EVM custom feeds / Feed Builder: v2 `encoded` payload wrapped into `bytes[]`
  * EVM legacy aggregators: `bytes[]` updates from `/updates/evm/...`
  * Randomness: encoded settlement payloads (chain-specific)

### 4) EVM route selection

* Feed Builder/custom feed on EVM:
  * `GET /v2/fetch/{feed_id}`
  * `GET /v2/simulate/{feedHashes}`
  * `GET /v2/update/{feedHashes}?chain=evm&network=mainnet|testnet&use_timestamp=true`
* Legacy aggregator-based EVM integration:
  * `GET /simulate/evm/{network}/{aggregator_ids}`
  * `GET /updates/evm/{chainId}/{aggregatorIds}`
* Do not send a Feed Builder `bytes32` feed ID to `/updates/evm/...` as the default path. Use the v2 route first.

### 5) REST endpoint quick reference

| Endpoint                                         | Method | Description                                  | Example                                                                            |
| ------------------------------------------------ | ------ | -------------------------------------------- | ---------------------------------------------------------------------------------- |
| `/store`                                         | `POST` | Store a v1 feed definition                   | `curl -X POST "$CROSSBAR/store" -d '{"queue":"...","jobs":[...]}'`                 |
| `/fetch/{hash}`                                  | `GET`  | Fetch a stored v1 feed definition            | `curl "$CROSSBAR/fetch/$FEED_HASH"`                                                |
| `/v2/fetch/{feed_id}`                            | `GET`  | Fetch a stored v2 feed definition            | `curl "$CROSSBAR/v2/fetch/$FEED_ID"`                                               |
| `/simulate/jobs`                                 | `POST` | Simulate raw `OracleJob[]` from request body | `curl -X POST "$CROSSBAR/simulate/jobs" -d '{"jobs":[...]}'`                       |
| `/simulate/{feedHashes}`                         | `GET`  | Simulate one or more stored feed hashes      | `curl "$CROSSBAR/simulate/$FEED_HASH"`                                             |
| `/v2/simulate/{feedHashes}`                      | `GET`  | Simulate one or more v2 feed hashes          | `curl "$CROSSBAR/v2/simulate/$FEED_ID?network=testnet"`                            |
| `/v2/update/{feedHashes}`                        | `GET`  | Build v2 chain-specific update payloads      | `curl "$CROSSBAR/v2/update/$FEED_ID?chain=evm&network=testnet&use_timestamp=true"` |
| `/updates/solana/{network}/{feedPubkeys}`        | `GET`  | Build Solana pull update instructions        | `curl "$CROSSBAR/updates/solana/devnet/$FEED_PUBKEY?payer=$PAYER"`                 |
| `/updates/evm/{chainId}/{aggregatorIds}`         | `GET`  | Build legacy EVM encoded update bytes        | `curl "$CROSSBAR/updates/evm/1116/$AGGREGATOR_ID"`                                 |
| `/updates/aptos/{network}/{aggregatorAddresses}` | `GET`  | Build Aptos update payloads                  | `curl "$CROSSBAR/updates/aptos/testnet/$AGGREGATOR_ID"`                            |
| `/updates/sui/{network}/{aggregatorAddresses}`   | `GET`  | Build Sui update payloads                    | `curl "$CROSSBAR/updates/sui/mainnet/$AGGREGATOR_ID"`                              |
| `/updates/iota/{network}/{aggregatorAddresses}`  | `GET`  | Build Iota update payloads                   | `curl "$CROSSBAR/updates/iota/mainnet/$AGGREGATOR_ID"`                             |
| `/randomness/evm`                                | `POST` | Fetch EVM randomness settlement payload      | `curl -X POST "$CROSSBAR/randomness/evm" -d '{...}'`                               |

### 6) Payload snapshots

`POST /simulate/jobs` request:

```json
{
  "jobs": [
    {
      "tasks": [
        { "valueTask": { "value": 42 } }
      ]
    }
  ],
  "network": "mainnet"
}
```

`POST /simulate/jobs` response:

```json
{
  "feedHash": "direct",
  "results": ["42"],
  "error": null
}
```

`GET /v2/update/{feedHashes}?chain=evm&network=testnet&use_timestamp=true` response:

```json
{
  "medianResponses": [
    {
      "value": "123450000000000000000",
      "feedHash": "0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c",
      "numOracles": 3
    }
  ],
  "oracleResponses": [],
  "timestamp": 1730000000,
  "slot": 0,
  "recentHash": "0xabc123...",
  "encoded": "0x8f6f2b7c..."
}
```

For EVM consumers, wrap `encoded` into a one-element `bytes[]` when calling `getFee` or `updateFeeds`.

`GET /updates/evm/{chainId}/{aggregatorIds}` legacy response:

```json
{
  "results": [
    {
      "result": "123450000000000000000"
    }
  ],
  "failures": [],
  "encoded": [
    "0x8f6f2b7c..."
  ]
}
```

### 7) Operational guardrails

* Do not log full `.env`.
* Treat Crossbar as sensitive infra (rate limits, credentials, API keys).
* Use caching appropriately; monitor error rates and response latency.
* For Monad and current custom-feed integrations, prefer the v2 feed-hash flow even if legacy EVM routes are still available.

## Minimal Example

```bash
CROSSBAR="https://crossbar.switchboard.xyz"
FEED_ID="0x4cd1cad962425681af07b9254b7d804de3ca3446fbfd1371bb258d2c75059812"
AGGREGATOR_ID="0xfd2b067707a96e5b67a7500e56706a39193f956a02e9c0a744bf212b19c7246c"

# Simulate a direct OracleJob payload
curl -s -X POST "$CROSSBAR/simulate/jobs" \
  -H "content-type: application/json" \
  -d '{"jobs":[{"tasks":[{"valueTask":{"value":42}}]}]}' | jq .

# Fetch a Monad-compatible v2 EVM payload for one feed
curl -s "$CROSSBAR/v2/update/$FEED_ID?chain=evm&network=testnet&use_timestamp=true" | jq .

# Legacy EVM aggregator route, only for older integrations
curl -s "$CROSSBAR/updates/evm/1116/$AGGREGATOR_ID" | jq .
```

## Troubleshooting Checklist

* IPFS store fails → verify IPFS credentials and outbound access
* simulation intermittent errors → endpoints unstable; add source diversity/fallbacks
* update fetch fails → network mismatch, RPC unreachable, queue mismatch
* `ORACLE_UNAVAILABLE` on `/v2/update` after successful `/v2/fetch` and `/v2/simulate` -> check managed oracle/gateway availability and oracle-side validation errors such as `RangeExceeded`; raw v2 `maxJobRangePct` must be scaled by `1e9`
* EVM custom feed not resolving → confirm you are using `/v2/fetch`, `/v2/simulate`, and `/v2/update`, not the legacy aggregator route
* rate limits → self-host + caching + reduce polling

## References

* <https://docs.switchboard.xyz/tooling/crossbar>
* <https://docs.switchboard.xyz/tooling/crossbar/run-crossbar-with-docker-compose>


# Switchboard Surge Skill

## Purpose

Use Switchboard Surge for low-latency streaming:

* Subscribe to signed updates over WebSocket
* Monitor latency/health and implement reconnection
* Convert signed updates for on-chain settlement (chain-specific)

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/on-demand@3.10.6`
* `@switchboard-xyz/common@5.8.5` (EVM conversion path)
* `@switchboard-xyz/sui-sdk@0.1.16` (Sui conversion path)

## Preconditions

* `OperatorPolicy` exists.
* If creating/modifying a paid subscription, explicit approval is required.

## Inputs to Collect

* subscription wallet/network (Solana)
* symbol/feed list
* target usage: bot-only vs on-chain settlement vs UI display
* validation thresholds (max staleness, deviation checks) if safety-critical

## Minimal Example

```ts
const surge = new sb.Surge({ connection, keypair });
await surge.connectAndSubscribe([{ symbol: "BTC/USD" }]);

surge.on("signedPriceUpdate", (update: sb.SurgeUpdate) => {
  if (!update.getLatencyMetrics().isHeartbeat) {
    console.log(update.getFormattedPrices());
  }
});
```

## Playbook

### 1) Subscribe to signed updates (TypeScript skeleton)

```ts
import * as sb from "@switchboard-xyz/on-demand";

const { keypair, connection } = await sb.AnchorUtils.loadEnv();
const surge = new sb.Surge({ connection, keypair });

await surge.connectAndSubscribe([{ symbol: "BTC/USD" }, { symbol: "SOL/USD" }]);

surge.on("signedPriceUpdate", (update: sb.SurgeUpdate) => {
  const metrics = update.getLatencyMetrics();
  if (metrics.isHeartbeat) return;

  const prices = update.getFormattedPrices();
  // Use prices + metrics in bot logic
});
```

### Subscription

Surge subscriptions are managed **on Solana** via the Surge program (`orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz`). To subscribe programmatically:

1. **Choose a tier** (Plug/Pro/Enterprise). Tiers are on-chain PDAs.
2. **Acquire SWTCH tokens** (payments are in SWTCH only).
3. **Fetch a fresh SWTCH/USDT oracle quote** and include it in the same transaction.
4. **Call `subscription_init`** with `tier_id` and `epoch_amount`. The program prices the subscription in SWTCH using the live quote and creates your subscription PDA.

Key notes:

* If the keypair has no active subscription, `connectAndSubscribe` fails.
* Tiers and limits are enforced on-chain (max feeds, connections, min delay).
* For UI-free flows, derive PDAs (`STATE`, `TIER`, `SUBSCRIPTION`) and pass required accounts.

Minimal sketch (quote + `subscription_init` in one tx):

```ts
import * as sb from "@switchboard-xyz/on-demand";

const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
const queue = await sb.Queue.loadDefault(program!);
const crossbar = new sb.Crossbar({ rpcUrl: connection.rpcEndpoint, programId: queue.pubkey });

// Fetch SWTCH/USDT quote ixs (feed hash from program state)
const quoteIxs = await queue.fetchQuoteIx(crossbar, [swtchFeedHash], {
  numSignatures: 1,
  payer: keypair.publicKey,
});

const subscriptionInitIx = buildSubscriptionInitIx({ tierId, epochAmount, accounts });

const tx = await sb.asV0Tx({
  connection,
  ixs: [quoteIxs, subscriptionInitIx],
  signers: [keypair],
});

await connection.sendTransaction(tx);
```

### Connection Flow (Gateway Auth)

If you are not using the SDK's `surge.connectAndSubscribe(...)`, implement this auth flow directly:

1. **Discover a gateway**
   * `GET https://crossbar.switchboard.xyz/gateways?network=mainnet` (or `devnet`)
2. **Create signature headers**
   * Build message hash: `SHA256("{blockhash}:{timestamp}")`
   * Sign with Ed25519 (your Solana keypair)
   * Send headers:
     * `X-Switchboard-Signature`
     * `X-Switchboard-Pubkey`
     * `X-Switchboard-Blockhash`
     * `X-Switchboard-Timestamp`
3. **Request a stream session**
   * `POST {gateway}/gateway/api/v1/request_stream`
   * Include all `X-Switchboard-*` headers
   * Read `session_token` + `oracle_ws_url` from response
4. **Open authenticated WebSocket**
   * Connect to `oracle_ws_url` with:
     * `Authorization: Bearer {pubkey}:{session_token}`
     * the same `X-Switchboard-*` headers
5. **Subscribe + keepalive**
   * Send a `Subscribe` message (feeds + signature fields)
   * When gateway sends `SignedPing`, reply with `SignedPong` using a **fresh** blockhash + timestamp + signature

Minimal message shape:

```json
{
  "type": "Subscribe",
  "feed_bundles": [{ "feeds": [{ "symbol": { "base": "BTC", "quote": "USD" }, "source": "AUTO" }] }],
  "signature_scheme": "Ed25519",
  "pubkey": "<solana-pubkey>",
  "signature": "<ed25519-signature>",
  "blockhash": "<recent-blockhash>",
  "timestamp": "<current-timestamp>"
}
```

### 2) Convert for on-chain settlement

* Solana: convert to quote/update instructions and include before consumer ix in the same tx.
* EVM: convert to EVM-compatible bytes and submit via `updateFeeds`.

### 3) Reliability

* heartbeat monitoring
* exponential backoff reconnect
* last-seen tracking and gap detection
* metrics logging

## References

* <https://docs.switchboard.xyz/docs-by-chain/solana-svm/surge>
* <https://docs.switchboard.xyz/ai-agents-llms/surge-subscription-guide>
* <https://docs.switchboard.xyz/tooling/crossbar/gateway-protocol>
* <https://docs.switchboard.xyz/docs-by-chain/evm/surge>
* <https://docs.switchboard.xyz/docs-by-chain/sui/surge>


# Switchboard Randomness Skill

## Purpose

Implement verifiable randomness with Switchboard:

* Solana: commit → generate → reveal (commit-reveal)
* EVM: request → resolve via Crossbar → settle on-chain

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `switchboard-on-demand = "0.13.0"` (Solana Rust)
* `@switchboard-xyz/common@5.8.5` (EVM off-chain resolution)
* `@switchboard-xyz/on-demand-solidity@1.1.0` (EVM on-chain interfaces)

## Preconditions

* `OperatorPolicy` exists (approval/spend limits for on-chain calls).

## Inputs to Collect

* chain + network
* app contract/program identifiers
* minimum settlement delay requirement
* binding requirement (what state transition must be gated)
* replay protections and failure handling requirements

## Minimal Example

Solana settle/read (commit-reveal flow):

```rust
use switchboard_on_demand::accounts::RandomnessAccountData;

let data = RandomnessAccountData::parse(ctx.accounts.randomness_account_data.data.borrow()).unwrap();
let random_bytes = data.get_value(Clock::get()?.slot)?;
let is_heads = random_bytes[0] % 2 == 0;
```

EVM request + settle:

```solidity
bytes32 randomnessId = keccak256(abi.encodePacked(msg.sender, blockhash(block.number - 1)));
switchboard.createRandomness(randomnessId, 1);
// Later, with Crossbar payload:
switchboard.settleRandomness(encodedRandomness);
bool landed = uint256(switchboard.getRandomness(randomnessId).value) % 3 < 2;
```

## Solana Playbook (commit/reveal)

1. Create randomness account (one-time) or reuse (`sb.Randomness.create(...)`).
2. Commit in the same transaction as the randomized action (`randomness.commitIx(queue)`).
3. Wait oracle generation window.
4. Reveal and settle in a follow-up transaction (`randomness.revealIx()`).

Requirements:

* bind commit to action
* prevent replay/double-settle
* retries with exponential backoff

### Solana Account and Timing Reference

What the account structure looks like:

* **Switchboard randomness account**: created via `sb.Randomness.create(...)`; stores commit/reveal state parsed with `RandomnessAccountData` (for example `seed_slot`, revealed bytes).
* **App-owned state/PDA**: your program state that binds business logic to randomness (for example storing `randomness_account`, `commit_slot`, wager/round state).

What PDA seeds to use:

* Switchboard randomness account is not documented as an app PDA derivation pattern in this skill; create it using `sb.Randomness.create(...)`.
* App PDAs are application-specific. Tutorial examples use seeds like `[b"playerState", user.key().as_ref()]` and `[b"stateEscrow"]` for app state/escrow.

How oracle assignment works:

* Assignment is handled by Switchboard during request/commit and validated during reveal/settlement.
* Integrators should bind commit and reveal to the same randomness account reference (store at commit, verify at settle) and verify expected slot linkage (for example `seed_slot == commit_slot` in app state).

Generation window duration:

* Treat timing as a **policy decision**, not a hard-coded universal duration.
* For safety-sensitive flows, recommended strict policy is fresh-slot usage (for example `seed_slot == clock.slot - 1`).
* For less latency-sensitive flows, you can allow a looser freshness bound if your threat model allows it.

How to check readiness for reveal:

* On-chain: use `RandomnessAccountData::get_value(clock.slot)`.
  * Commit path guard: if it already returns `Ok(_)`, randomness was already revealed.
  * Settle path guard: require `Ok([u8; 32])`, otherwise treat as not yet resolved.
* Client-side: retry `randomness.revealIx()` with backoff until reveal succeeds.

Compact on-chain pattern:

```rust
use switchboard_on_demand::accounts::RandomnessAccountData;

let clock = Clock::get()?;
let randomness_data = RandomnessAccountData::parse(
    ctx.accounts.randomness_account_data.data.borrow()
).unwrap();

// Recommended strict freshness policy (application-level)
if randomness_data.seed_slot != clock.slot - 1 {
    return Err(ErrorCode::RandomnessExpired.into());
}

if is_commit_phase {
    // Commit path: reject already-revealed randomness
    if randomness_data.get_value(clock.slot).is_ok() {
        return Err(ErrorCode::RandomnessAlreadyRevealed.into());
    }
}

if is_settle_phase {
    // Settle path: require revealed value to be available
    let random_bytes = randomness_data
        .get_value(clock.slot)
        .map_err(|_| ErrorCode::RandomnessNotResolved)?;

    // Use random_bytes in app logic
}
```

## EVM Playbook (request/resolve/settle)

1. Request on-chain with a unique `randomnessId`.
2. Resolve off-chain via Crossbar to obtain an encoded settlement payload.
3. Settle on-chain, then read randomness value and execute logic.

Contract requirements:

* enforce minimum settlement delay
* CEI pattern (clear state before external calls)
* validate oracle assignment matches stored assignment

## References

* <https://docs.switchboard.xyz/docs-by-chain/solana-svm/randomness>
* <https://docs.switchboard.xyz/docs-by-chain/evm/randomness>
* <https://docs.switchboard.xyz/tooling/crossbar>


# Switchboard X402 Micropayments Skill

## Purpose

Use X402 micropayments with Switchboard feeds to access any X402-protected resource (premium data APIs, paid RPC, metered endpoints) in a way that:

* generates single-use payment authorization per request
* injects authorization securely at runtime (via variable overrides)
* avoids double-charging during simulation
* keeps sensitive payment material out of stored feed definitions

## Dependencies

Use exact pins from the [SDK Version Matrix](/tooling/sdk-version-matrix).

* `@switchboard-xyz/on-demand@3.10.6`
* `@switchboard-xyz/common@5.8.5`
* `@solana/web3.js@1.98.0`

## Preconditions

* `OperatorPolicy` exists (explicit approval for spending, secret handling rules).
* The target endpoint supports X402 and specifies:
  * required header name(s)
  * how signatures are derived (URL/method/body binding)

## Inputs to Collect

* X402-protected endpoint(s): URL, method, and any body/headers that are part of the signature
* funding source (e.g., USDC/SOL wallet) and spend caps
* `crossbarUrl` (default: `https://crossbar.switchboard.xyz`)
* whether this feed will be:
  * used atomically (update+use in same tx), or
  * cranked/pushed periodically (generally discouraged for X402 due to cost)

## Key Constraints (from the X402 tutorial)

* **`numSignatures` must be 1**
  * Why: X402 payment signatures are single-use. If more than one oracle tries to use the same payment authorization, subsequent requests fail.
* **Simulation can charge you**
  * Crossbar simulation can trigger the paid HTTP request and charge the payment method.
  * On-chain transaction simulation is safe (does not trigger the HTTP call).
* **Prefer inline feeds**
  * Payment signatures change every request and headers contain sensitive authorization.
  * Storing a static definition on IPFS is generally incompatible with per-request payment authorization.

## Minimal Example

```ts
const instructions = await queue.fetchManagedUpdateIxs(crossbar, [ORACLE_FEED], {
  numSignatures: 1,
  payer: keypair.publicKey,
  variableOverrides: {
    X402_PAYMENT_SIGNATURE: paymentSignature,
  },
});
```

## Generalized Playbook

### 1) Derive the X402 payment authorization off-chain

* Use the X402 client tooling to derive the payment header/signature.
* The derived authorization must match the exact request (URL, method, and sometimes body).

### 2) Define the Switchboard job with a placeholder header

* Put the payment authorization in an HTTP header as a variable placeholder.
* Use variable overrides to inject the real value at runtime.

Example (conceptual):

```json
{
  "tasks": [
    {
      "httpTask": {
        "url": "https://paid.example.com/endpoint",
        "method": "POST",
        "headers": [
          { "key": "X-PAYMENT", "value": "${X402_PAYMENT_HEADER}" },
          { "key": "Content-Type", "value": "application/json" }
        ],
        "body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getBlockHeight\"}"
      }
    },
    { "jsonParseTask": { "path": "$.result" } }
  ]
}
```

### 3) Execute the update with runtime overrides

* Provide `variableOverrides = { X402_PAYMENT_HEADER: <derived_value> }`
* Enforce `numSignatures = 1` to avoid reuse/failure.

### 4) Cost controls

* Treat each successful oracle HTTP call as a billable event.
* Enforce per-request and daily spend caps.
* Avoid Crossbar simulations against paid endpoints unless explicitly approved.

## Outputs

Produce an `X402IntegrationPlan` including:

* how payment authorization is derived and what request fields it binds to
* job definition template with placeholders
* required runtime overrides
* `numSignatures = 1` constraint and operational implications
* anti-double-charge simulation guidance
* spend cap and monitoring strategy

## References

* <https://docs.switchboard.xyz/docs-by-chain/solana-svm/x402/x402-tutorial>
* <https://docs.switchboard.xyz/custom-feeds/advanced-feed-configuration/data-feed-variable-overrides>
* <https://docs.switchboard.xyz/tooling/crossbar>




---

[Next Page](/llms-full.txt/1)

