# SOON Chain Integration Guide

## Description

APRO Oracle Data Pull is a pull mode oracle product for those who require low-latency data feed service.

Following process chart is how pull mode works:

![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXfFfbI--N-iakm2_Cl3OHtL4nL_z5YNujm-91HVo-3kb28irOy_soWhyVvoRSAArFNtIShwO8N3-96bgSxMyAWFiqGlFi2RCir5AK6y3xKs_v4ztM8mju7lM72iC0Li5sTU2AQIUQ?key=lO7VgCeFWS492mN1m8ub0_Li)

This doc is the integration guide for projects on SOON Devnet.

## Domains

| Description                             | Devnet URL                                                      | Mainnet URL                                           |
| --------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
| REST Endpoint to query specific reports | [https://live-api-test.apro.com](http://live-api-test.apro.com) | [https://live-api.apro.com](http://live-api.apro.com) |

## Program IDs

| Description       | Devnet                                       | Mainnet                                      |
| ----------------- | -------------------------------------------- | -------------------------------------------- |
| Apro\_SVM\_Oracle | 4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY | 4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY |
| Sample\_Client    | HUJ8ouH6fVonhF1hPV6ENoLid5nbHfyZSpvfujw6X6Hm | HUJ8ouH6fVonhF1hPV6ENoLid5nbHfyZSpvfujw6X6Hm |

## Authentication

#### Headers

All routes require the following two headers for user authentication:

<table><thead><tr><th width="266">Header</th><th>Description</th></tr></thead><tbody><tr><td><strong>Authorization</strong></td><td>The user’s unique identifier, provided as a UUID (Universally Unique IDentifier).</td></tr><tr><td><strong>X-Authorization-Timestamp</strong></td><td>The current timestamp, with precision up to milliseconds. The timestamp must closely synchronize with the server time, allowing a maximum discrepancy of 5 seconds (by default).<br><strong>For a value of 10 digits, it is multiplied directly by 1000 to milliseconds.</strong></td></tr></tbody></table>

To get authorization, **please contact our BD Team**:

* Email: <bd@apro.com>
* Telegram:[ Head of Business Development](https://t.me/Annie_LLLEEE)

## API endpoints

1. Return a single report at a given timestamp

**Endpoint**

/api/soon/reports

<table><thead><tr><th width="152">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Returns a single report for a given timestamp.</td><td><ul><li>feedID: A Data Streams feed ID.</li><li>timestamp: The Unix timestamp for the report.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports?feedID=\<feedID>\&timestamp=\<timestamp>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXekGnFxtgvXRBqMDeINDXhKRI3zuU7MRX6BzS1crHEqRvC9XQGu6acT0SbyioNHGu5ydhm3MBQITvXjhSptrNyP5A5R61MG3R_4tRSsCzMU6j2pSuZ7gfIPrq9JQQtHWoWEltnL?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

2. Return a single report with the latest timestamp

**Endpoint**

/api/soon/reports/latest

| Type     | Parameter(s)                                      |
| -------- | ------------------------------------------------- |
| HTTP GET | <ul><li>feedID: A Data Streams feed ID.</li></ul> |

**Sample request**

GET /api/soon/reports/latest?feedID=\<feedID>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXff8IedD3DctaqXK0Du4ll7WronYnw1x8cAcEynYxan1NbB5_pUzkx4a0sERE70ZeH8-_s1GtK7qJXPwJFAQtlga-n5oCQ_HO0kVWO4waGMGorZSuCNIta_wreBgsh2_xLDg9yjbQ?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

3. Return a report for multiple FeedIDs at a given timestamp

**Endpoint**

/api/soon/reports/bulk

<table><thead><tr><th width="133">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Return a report for multiple FeedIDs at a given timestamp.</td><td><ul><li>feedIDs: A comma-separated list of Data Streams feed IDs.</li><li>timestamp: The Unix timestamp for the first report or the string 'latest' for getting the latest report.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports/bulk?feedIDs=\<feedID1>,\<feedID2>,...\&timestamp=\<timestamp>

GET /api/soon/reports/bulk?feedIDs=\<feedID1>,\<feedID2>,...\&timestamp=latest

**Sample response**<br>

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfHzVwaINmCvV3gi56cZL4aVtnaSqlTXanZMJ_Y8V4_28zjoGzIe-aA7EkDsLBv3uCPL-dGNfbYAK-9c7sK6iV3_z2raIS7BWXKJTVpObao-cUuI3IQooeqrG0UtDvX1_BsZrt0rQ?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

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

4. Return multiple sequential reports for a single FeedID, starting at a given timestamp

**Endpoint**

/api/soon/reports/page

<table><thead><tr><th width="144">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Return multiple sequential reports for a single FeedID, starting at a given timestamp.</td><td><ul><li>feedID: A Data Streams feed ID.</li><li>startTimestamp: The Unix timestamp for the first report.</li><li>limit (optional): The number of reports to return.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports/page?feedID=\<FeedID>\&startTimestamp=\<StartTimestamp>\&limit=\<Limit>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdYEN4RAB5HgiZCgWibM6Gf7JymQLt03opIguOAEvW6N0hZO268n-31pGKGTFIkz80jg1jZh1XXjtrWHC-adRdEqLnsV2ejFRkjnP6_9mfBJVpa9-DjOAQ9ST0xCxTao3xqELlgsA?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

5. WebSocket Connection

Establish a streaming WebSocket connection that sends reports for the given feedID(s) after they are verified.

**Endpoint**

/api/soon/ws

<table><thead><tr><th width="186">Type</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>WebSocket</td><td><ul><li>feedIDs: A comma-separated list of Data Streams feed IDs.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/ws?feedIDs=\<feedID1>,\<feedID2>,...

\
**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXc1l_zXPvLCR8IBYVX08vfOs1EVHEpGi352RowGvNSS7IqrMPFjrJue2GA_M-qJWygs8w6irR3SgKOwIuHoFkxhbGHrdCHZO5r2tVw8dcFn66baKBH98TKyCtwXJkFDiKwX7URW?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

## Price Feed ID

**Devnet:**

<table><thead><tr><th width="208">Feed</th><th>Information</th></tr></thead><tbody><tr><td><strong>BTC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003665949c883f9e0f6f002eac32e00bd59dfe6c34e92a91c37d6a8322d6489</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 1</li></ul></td></tr><tr><td><strong>ETH/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003555ace6b39aae1b894097d0a9fc17f504c62fea598fa206cc6f5088e6e45</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 2</li></ul></td></tr><tr><td><strong>SOL/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000343ec7f6691d6bf679978bab5c093fa45ee74c0baac6cc75649dc59cc21d3</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 3</li></ul></td></tr><tr><td><strong>BONK/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00032b250dddd30d526b4ae66d170e1ab204ab0567531798d3bbd09737f6c3c6</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 4</li></ul></td></tr></tbody></table>

**Mainnet:**

<table><thead><tr><th width="188">Feed</th><th>Information</th></tr></thead><tbody><tr><td><strong>BTC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003665949c883f9e0f6f002eac32e00bd59dfe6c34e92a91c37d6a8322d6489</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 1</li></ul></td></tr><tr><td><strong>ETH/USD</strong></td><td><ul><li>I<strong>D:</strong> 0x0003555ace6b39aae1b894097d0a9fc17f504c62fea598fa206cc6f5088e6e45</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 2</li></ul></td></tr><tr><td><strong>Valueless_Test/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000369f643e4e5d9b9d04863249c07c15937d0b36af803dd8d213827b0badd5c</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 3</li></ul></td></tr><tr><td><strong>USDT/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00039a0c0be4e43cacda1599ac414205651f4a62b614b6be9e5318a182c33eb0</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 4</li></ul></td></tr><tr><td><strong>USDC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00034b881a0c0fff844177f881a313ff894bfc6093d33b5514e34d7faa41b7ef</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 5</li></ul></td></tr><tr><td><strong>BONK/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00032b250dddd30d526b4ae66d170e1ab204ab0567531798d3bbd09737f6c3c6</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 10</li></ul></td></tr><tr><td><strong>SOL/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000343ec7f6691d6bf679978bab5c093fa45ee74c0baac6cc75649dc59cc21d3</li><li> <strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 11</li></ul></td></tr><tr><td><strong>WEETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003a41091252f838042a1ebb32e545788f0760743eb2aa129d55a6ceb53a716</li><li> <strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 12</li></ul></td></tr><tr><td><strong>EIGEN/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x000345b536c6b8307db63d95fb38e0bd27996f9be48fb224cc8d5c24a8d00640</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 13</li></ul></td></tr><tr><td><strong>PUFETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x00031a3a47d0230c312ab9f3a453a6d828464b582c754226233f4c5e86d0ed8d</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 14</li></ul></td></tr><tr><td><strong>RSETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003468783dab63735ffee9fe08084240acb16e81ca93ee2eb78ab81388c32ea</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 15</li></ul></td></tr><tr><td><strong>METH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003f815ffeb0d25f8945e0263bd3ce6c31311351a3dde2e90bc01539a5b06bb</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 16</li></ul></td></tr></tbody></table>

## Error response codes

<table><thead><tr><th width="246">Status Code</th><th width="467">Description</th></tr></thead><tbody><tr><td>400 Bad Request</td><td><p>This error is triggered when:</p><ul><li>There is any missing/malformed query argument.</li><li>Required headers are missing or provided with incorrect values.</li></ul></td></tr><tr><td>401 Unauthorized User</td><td><p>This error is triggered when:</p><ul><li>Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.</li><li>A user requests access to a feed without the appropriate permission or that does not exist.</li></ul></td></tr><tr><td>500 Internal Server</td><td>Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side.</td></tr><tr><td>206 Missing data (/bulk endpoint only)</td><td>Indicates that at least one feed ID data is missing from the report. E.g., you requested a report for feed IDs <mark style="color:green;">&#x3C;feedID1></mark>, <mark style="color:green;">&#x3C;feedID2></mark>, and <mark style="color:green;">&#x3C;feedID3></mark> at a given timestamp. If data for <mark style="color:green;">&#x3C;feedID2></mark> is missing from the report (not available yet at the specified timestamp), you get [<mark style="color:green;">&#x3C;feedID1 data></mark>, <mark style="color:green;">&#x3C;feedID3 data></mark>] and a 206 response.</td></tr></tbody></table>

## Sample Integration Program

#### **oracle\_sdk:**

```rust
use anchor_lang::prelude::*;
use anchor_lang::solana_program::hash::hash;
use anchor_lang::solana_program::instruction::Instruction;
use anchor_lang::solana_program::program::invoke;

pub const APRO_SVM_PROGRAM_ID: &str = "4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY";

pub fn load_price_feed_from_account_info(price_account_info: &AccountInfo) -> Result<PriceFeed> {
    let data = price_account_info.try_borrow_data()?;

    let mut price_feed_data = &data[8..];
    let price_feed = PriceFeed::deserialize(&mut price_feed_data)?;

    Ok(price_feed)
}

pub fn update_price<'info>(
    oracle_state: &AccountInfo<'info>,
    price_feed: &AccountInfo<'info>,
    payer: &AccountInfo<'info>,
    admin: &AccountInfo<'info>,
    system_program: &AccountInfo<'info>,
    oracle_program: &AccountInfo<'info>,
    feed_id: [u8; 32],
    valid_time_stamp: u128,
    observe_time_stamp: u128,
    native_fee: u128,
    apro_token_fee: u128,
    expire_at: u128,
    benchmark_price: u128,
    ask_price: u128,
    bid_price: u128,
    config_digest: [u8; 32],
    epoch_and_round: u128,
    extra_hash: [u8; 32],
    signatures: Vec<[u8; 64]>,
    recovery_ids: Vec<u8>,
) -> Result<()> {
    let ix = Instruction {
        program_id: *oracle_program.key,
        accounts: vec![
            AccountMeta::new_readonly(*oracle_state.key, false),
            AccountMeta::new(*price_feed.key, false),
            AccountMeta::new(*payer.key, true),
            AccountMeta::new(*admin.key, false),
            AccountMeta::new_readonly(*system_program.key, false),
        ],
        data: UpdatePriceArgs {
            feed_id,
            valid_time_stamp,
            observe_time_stamp,
            native_fee,
            apro_token_fee,
            expire_at,
            benchmark_price,
            ask_price,
            bid_price,
            config_digest,
            epoch_and_round,
            extra_hash,
            signatures,
            recovery_ids,
        }
        .data(),
    };

    invoke(
        &ix,
        &[
            oracle_state.clone(),
            price_feed.clone(),
            payer.clone(),
            admin.clone(),
            system_program.clone(),
            oracle_program.clone(),
        ],
    )?;

    Ok(())
}

#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)]
pub struct PriceFeed {
    pub feed_id: [u8; 32],
    pub valid_time_stamp: u128,
    pub observe_time_stamp: u128,
    pub native_fee: u128,
    pub apro_token_fee: u128,
    pub expire_at: u128,
    pub benchmark_price: u128,
    pub ask_price: u128,
    pub bid_price: u128,
    pub config_digest: [u8; 32],
    pub epoch_and_round: u128,
    pub extra_hash: [u8; 32],
}

#[derive(AnchorSerialize, AnchorDeserialize)]
struct UpdatePriceArgs {
    feed_id: [u8; 32],
    valid_time_stamp: u128,
    observe_time_stamp: u128,
    native_fee: u128,
    apro_token_fee: u128,
    expire_at: u128,
    benchmark_price: u128,
    ask_price: u128,
    bid_price: u128,
    config_digest: [u8; 32],
    epoch_and_round: u128,
    extra_hash: [u8; 32],
    signatures: Vec<[u8; 64]>,
    recovery_ids: Vec<u8>,
}

impl UpdatePriceArgs {
    fn data(&self) -> Vec<u8> {
        let mut data = Vec::new();
        let preimage = "global:update_price".as_bytes();
        let hash = hash(preimage);
        let discriminator = &hash.to_bytes()[..8];
        data.extend_from_slice(discriminator);
        data.extend_from_slice(&AnchorSerialize::try_to_vec(self).unwrap());

        data
    }
}
```

#### **Clients code:**

```rust
use anchor_lang::prelude::*;
use oracle_sdk::{load_price_feed_from_account_info, update_price};

declare_id!("GzWu85MdbZtVBDcC4Xrmp1dWix7Qo52nUAkD2FjraJ6f");//modify by solana address -k

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

    pub fn fetch_price(ctx: Context<FetchPrice>) -> Result<()> {
        //fetch price feed from oracle
        let price_feed = load_price_feed_from_account_info(&ctx.accounts.price_account)?;

        msg!("Price Feed Details:");
        msg!("Feed ID: {:?}", price_feed.feed_id);
        msg!("Valid Timestamp: {}", price_feed.valid_time_stamp);
        msg!("Observe Timestamp: {}", price_feed.observe_time_stamp);
        msg!("Benchmark Price: {}", price_feed.benchmark_price);
        msg!("Ask Price: {}", price_feed.ask_price);
        msg!("Bid Price: {}", price_feed.bid_price);

        let current_timestamp = Clock::get()?.unix_timestamp as u128;
        let staleness_threshold = 3600u128;

        //store the price in the price_result account
        let price_result = if current_timestamp - price_feed.valid_time_stamp <= staleness_threshold
        {
            PriceResult {
                price: price_feed.benchmark_price,
                is_valid: true,
            }
        } else {
            PriceResult {
                price: 0,
                is_valid: false,
            }
        };

        ctx.accounts.price_result.set_inner(price_result);

        Ok(())
    }

    pub fn update_oracle_price(
        ctx: Context<UpdateOraclePrice>,
        feed_id: [u8; 32],
        valid_time_stamp: u128,
        observe_time_stamp: u128,
        native_fee: u128,
        apro_token_fee: u128,
        expire_at: u128,
        benchmark_price: u128,
        ask_price: u128,
        bid_price: u128,
        config_digest: [u8; 32],
        epoch_and_round: u128,
        extra_hash: [u8; 32],
        signatures: Vec<[u8; 64]>,
        recovery_ids: Vec<u8>,
    ) -> Result<()> {
        //update the price feed through CPI
        update_price(
            &ctx.accounts.oracle_state.to_account_info(),
            &ctx.accounts.price_feed.to_account_info(),
            &ctx.accounts.payer.to_account_info(),
            &ctx.accounts.admin.to_account_info(),
            &ctx.accounts.system_program.to_account_info(),
            &ctx.accounts.oracle_program.to_account_info(),
            feed_id,
            valid_time_stamp,
            observe_time_stamp,
            native_fee,
            apro_token_fee,
            expire_at,
            benchmark_price,
            ask_price,
            bid_price,
            config_digest,
            epoch_and_round,
            extra_hash,
            signatures,
            recovery_ids,
        )?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct FetchPrice<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: This account is owned by the Oracle program
    pub price_account: UncheckedAccount<'info>,
    #[account(
        init_if_needed,
        payer = payer,
        space = 8 + 16 + 1, // discriminator + price (u128) + is_valid
        seeds = [b"price_result", price_account.key().as_ref()],
        bump
    )]
    pub price_result: Account<'info, PriceResult>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateOraclePrice<'info> {
    /// CHECK: This account is verified in the update_price function
    pub oracle_state: UncheckedAccount<'info>,
    /// CHECK: This account is verified in the update_price function
    #[account(mut)]
    pub price_feed: UncheckedAccount<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: This account is verified in the update_price function
    #[account(mut)]
    pub admin: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
    /// CHECK: This account is verified in the update_price function
    pub oracle_program: UncheckedAccount<'info>,
}

#[account]
pub struct PriceResult {
    pub price: u128,
    pub is_valid: bool,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Invalid account data length")]
    InvalidAccountDataLength,
}
```


---

# Agent Instructions: Querying This Documentation

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

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

```
GET https://docs.apro.com/en/data-pull-svm-chain/soon-chain-integration-guide.md?ask=<question>
```

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

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