Skip to main content
This guide explains how to securely integrate with Chaos oracle price feeds on the Solana blockchain. It follows a similar structure to the EVM Integration Guide to provide a consistent developer experience for both pull and push oracles.

How to Use the Pull Oracle

To integrate with the pull oracle on Solana, you will need to fetch signed price data from the Chaos API and then verify and consume it in your on-chain program.

Step 1: Obtain API Keys and Signer Address

Before you can fetch data or verify signatures, you must know the oracle’s trusted signer address. Contact the Chaos team to obtain your API keys and the public key of the signer for the feeds you intend to use. This signer address is required for making API requests and for on-chain signature verification.

Step 2: Fetch Signed Price Data

Using your API key, make a GET request to the /prices/{feedId}/latest endpoint to retrieve the latest SVM-signed price data. You can find more details in the API reference. Other options for requesting SVM compatible price data are the following:
  • Batch prices: Use /prices/batch to fetch multiple feeds in a single request
  • Historical prices: Use /prices/queryhistory to retrieve historical price data
  • Latest or historical: Use /prices to get either latest or historical prices based on parameters
Example API Response:
{
  "feedId": "BTCUSD",
  "price": 11936907500000,
  "ts": 1753085844,
  "expo": -8,
  "signature": "61410a3b6be6fd59351ff053455e97c9840dff6c7c8d4668b46b8f90ca705f7157aed0b53f534e2706cfff9f8d44badaf9c2a94f7443a35117cfaf4c0dd484ee",
  "recoveryId": 1
}

Step 3: Verify and Consume the Price On-Chain

The data from the API response must be passed to your Solana program to be verified. The program should hash the price data and verify the signature against the trusted signer address you obtained in Step 1 and stored on-chain. Here is an example of how to implement the verification logic and consume the price in your program.
The following code is for illustrative purposes only, has not been audited, and should not be used in production without thorough testing.
use anchor_lang::{
    prelude::*,
    solana_program::{keccak, secp256k1_recover},
};

/// Represents a signed price message from the Chaos Pull Oracle
/// Each message contains:
/// - A price value with its exponent (e.g., 19476500000 * 10^-8 = $194.765)
/// - A timestamp in seconds since Unix epoch
/// - A secp256k1 signature (64 bytes) and recovery ID (1 byte)
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct SignedPriceMessage {
    /// Recovery ID (0-3) used in secp256k1 signature recovery
    pub recovery_id: u8,
    /// 64-byte secp256k1 signature of the encoded price message
    pub signature: [u8; 64],
    /// Integer price value (must be combined with expo for actual price)
    pub price: u64,
    /// Price exponent (e.g., -8 means price is divided by 10^8)
    pub expo: i8,
    /// Unix timestamp in seconds when the price was signed
    pub timestamp: i64,
}

impl SignedPriceMessage {
    /// Encodes the price message into the standard 49-byte format for verification
    /// Format:
    /// - Bytes 0-31:  Feed pair ID (e.g., "BTCUSD" padded with zeros)
    /// - Bytes 32-39: Price as little-endian u64
    /// - Bytes 40:    Exponent as little-endian i8
    /// - Bytes 41-48: Timestamp as little-endian i64
    pub fn encode_price_message(&self, feed: &PriceFeed) -> [u8; 49] {
        let mut m = [0u8; 49];
        // First 32 bytes: Feed pair identifier
        m[..32].clone_from_slice(&feed.pair);
        // Next 8 bytes: Price in little-endian
        m[32..40].clone_from_slice(&self.price.to_le_bytes());
        // Next 1 byte: Exponent in little-endian
        m[40..41].clone_from_slice(&self.expo.to_le_bytes());
        // Last 8 bytes: Timestamp in little-endian
        m[41..49].clone_from_slice(&self.timestamp.to_le_bytes());
        m
    }

    /// Creates the Keccak256 hash of the encoded price message
    /// This hash is what gets signed by the oracle
    pub fn create_verification_hash(&self, feed: &PriceFeed) -> [u8; 32] {
        keccak::hash(&self.encode_price_message(feed)).to_bytes()
    }

    /// Verifies the signature using Solana's secp256k1 recovery
    /// Steps:
    /// 1. Creates message hash from encoded price data
    /// 2. Recovers the public key from the signature
    /// 3. Verifies the recovered key matches the authorized signer
    /// 4. Checks both X-coordinate and parity (even/odd) of the key
    pub fn verify_signature(&self, feed: &PriceFeed) -> Result<()> {
        let signer: &[u8; 33] = &feed.signer;
        // Check if the expected public key is even or odd
        let is_even = signer[0].rem(2) == 0;
        
        // Recover the public key from signature
        let pubkey = secp256k1_recover::secp256k1_recover(
            &self.create_verification_hash(feed),
            self.recovery_id,
            &self.signature,
        ).map_err(|_| error!(ErrorCode::InvalidSigner))?;
        
        // Verify the X-coordinate matches
        require!(
            pubkey.0[..32].eq(&signer[1..]), 
            ErrorCode::InvalidSigner
        );
        
        // Verify the parity (even/odd) matches
        require_eq!(
            (pubkey.0[63].rem(2) == 0),
            is_even,
            ErrorCode::InvalidSigner
        );
        
        Ok(())
    }
}
To use the verification logic, you need to:
  1. Store the Signer Address: The trusted signer’s public key must be stored on-chain in an account (like the PriceFeed account shown above) so your program can access it for verification.
  2. Integrate the Verification Logic: Incorporate the SignedPriceMessage struct and its implementation into your Solana program. You will also need a PriceFeed account structure to hold the trusted signer key for each price feed.
  3. Create a Consume Instruction: Create an instruction in your program that accepts a SignedPriceMessage and the PriceFeed account. Your off-chain client will fetch the signed price from the API and then call this instruction to verify and consume the price on-chain.
This code is provided for illustrative purposes only and has not undergone any formal security audit.
#[program]
pub mod price_consumer {
    use super::*;
    
    pub fn consume_price(ctx: Context<ConsumePrice>, msg: SignedPriceMessage) -> Result<()> {
        // 1. Verify the signature against the trusted signer in the 'feed' account.
        msg.verify_signature(&ctx.accounts.feed)?;
        
        // 2. Check the timestamp to prevent replay attacks.
        let clock = Clock::get()?;
        require!(clock.unix_timestamp - msg.timestamp < 60, ErrorCode::StalePrice);
        
        // 3. Use the verified price.
        let feed_id = std::str::from_utf8(&ctx.accounts.feed.pair).unwrap().trim_end_matches('\0');
        msg!("Verified price for feed {}: {}", feed_id, msg.price);
        
        // Your logic to consume the price goes here.
        
        Ok(())
    }
}

#[derive(Accounts)]
pub struct ConsumePrice<'info> {
    pub feed: Account<'info, PriceFeed>,
    // Add any other accounts your instruction needs.
}

Support

For assistance with SVM integration, contact our support team at [email protected].
I