# Session-Aware Equity Price Feed

### Contents

* [Requirements](#requirements)
* [Getting Started](#getting-started)
* [Building the Session-Aware Equities Price Feed](#building-your-session-aware-equities-price-feed)
* [Oracle Program Structure](#oracle-program-structure)
* [Defining Inputs & Outputs](#defining-your-inputs-and-outputs)
* [Defining the Inputs & Outputs](#defining-your-inputs-and-outputs)
* [Build & Deploy](#build-and-deploy)
* [SEDA Fast Deployment](#seda-fast-deployment)
* [SEDA Core Deployment](#seda-core-deployment)

## Requirements

* **Bun**: Install [Bun](https://bun.sh/) for package management and building.
* **Rust**: Install [Rust](https://rustup.rs/) for development and building.
* **WASM**: Install the [`wasm32-wasip1`](https://doc.rust-lang.org/rustc/platform-support/wasm32-wasip1.html) target with `rustup target add wasm32-wasip1` for WASM compilation.

```rust
rustup target add wasm32-wasip1
```

Alternatively, use the [devcontainer](https://containers.dev/) for a pre-configured environment.

#### Additional Requirement for SEDA Fast

{% hint style="info" icon="magnifying-glass" %}

* **SEDA Fast** requires a FAST API key for querying.

* **SEDA Core** does not require a FAST API key, but instead requires submitting a Data Request onchain from a supported network or on SEDA Chain directly.
  {% endhint %}

* A **SEDA FAST developer key**. Visit the [Developer Page](https://seda.xyz/dev) and access a 7 day trial.

{% hint style="info" %}
If you need more than 7 days for your trial key, please reach out to our team on [Discord](https://discord.com/invite/uBrQJZ2nrB) for an extended free trial.
{% endhint %}

***

## Getting Started

Clone and checkout the `seda-starter-kit` repository:

```
git clone https://github.com/sedaprotocol/seda-starter-kit.git session-aware-feed; cd session-aware-feed
```

Install dependencies:

```
bun install
```

Copy and populate the `.env` file's MNEMONIC:

```
cp .env.example .env
```

{% hint style="info" %}
If you need a SEDA mnemonic this can most easily be achieved by downloading [Keplr wallet](https://keplr.app). You can then claim Testnet SEDA tokens at the [SEDA faucet](https://ping-api.testnet.seda.xyz/faucet)&#x20;
{% endhint %}

***

## Building the Session-Aware Equities Price Feed

### Session Awareness

Publicly listed equities typically follow three regular trading sessions, between 4:00am ET - 8:00pm ET. With SEDA, developers can access the Blue Ocean ADS overnight trading session provided exclusively in partnership with Pyth. The full 24/5 trading sessions are as follows:

| Session name  | Session timeframe (US Eastern Time) |
| ------------- | ----------------------------------- |
| Pre-market    | 04:00am-09:30am                     |
| Regular hours | 9:30am-04:00pm                      |
| Post-market   | 04:00pm-08:00pm                     |
| Overnight     | 08:00pm-04:00am+1                   |

{% hint style="info" %}
**Key Points for Weekend Pricing:**

* Friday there is no overnight trading session
* Saturday there is no trading session
* Sunday overnight session provided by Blue Ocean ADS begins at 8pm
  {% endhint %}

Pyth aggregates pricing data across all institutional-grade regulated exchanges for equities. With SEDA you can combine all pricing sessions into one clean end-point for consumption.

***

## Oracle Program Structure

Start by navigating to the `src/execution_phase.rs` file. The execution phase contains the main computational component of your feed. This phase dictates how data is fetched and aggregated. The `src/tally_phase.rs` is more important for feeds that require onchain delivery via **SEDA Core** or that need to be backwards compatible with a SEDA Core feed.

For this feed we will take individual feeds, such as the Pre-market and Regular-market hours to create a unified endpoint to query the latest available price. The program automatically determines what session should be active and queries and returns the data correlated to this session.

{% hint style="info" %}

* **SEDA Fast**: Typically runs a single executor and forwards the result.
* **SEDA Core**: Multiple Overlay Nodes can execute the program, and the tally phase aggregates results.
  {% endhint %}

***

## Defining the Inputs & Outputs

We will be using Pyth Core as a data source. Navigate to <https://insights.pyth.network/price-feeds> and pick a feed.

We are going with NVDA:USD which correlates to Asset IDs:

* Pre-market\
  `0x61c4ca5b9731a79e285a01e24432d57d89f0ecdd4cd7828196ca8992d5eafef6`
* Regular hours\
  `0xb1073854ed24cbc755dc527418f52b7d271f6cc967bbf8d8129112b18860a593`

As inputs we require the two trading sessions, in chronological order.

```rust
#[derive(Deserialize)]
struct SessionAwareInputs {
    pyth_assets: Vec<String>,
}
```

As output we expect the latest in-session price of the two selected sessions. We also want to know which session's data is currently being returned, and if the market is currently in-session. If not, the price is stale.

```rust
#[derive(Serialize, Deserialize)]
enum Session {
    PreMarket,
    RegularHours,
    Closed
}

#[derive(Serialize, Deserialize)]
pub struct PriceFeedResponse {
    price: String,
    session: Session
}
```

We then parse the expected inputs in the `execution_phase` function and replace the rest of the code with placeholders.

The code will look something like this:

```rust
use anyhow::Result;
use seda_sdk_rs::{elog, http_fetch, log, Process};
use serde::{Deserialize, Serialize};


#[derive(Deserialize)]
struct SessionAwareInputs {
    pyth_assets: Vec<String>,
}

#[derive(Serialize, Deserialize)]
enum Session {
    PreMarket,
    RegularHours,
    Closed
}

#[derive(Serialize, Deserialize)]
pub struct PriceFeedResponse {
    price: String,
    session: Session
}

pub fn execution_phase() -> Result<()> {
    // Get the input parameters for the data request (DR).
    let session_aware_inputs = serde_json::from_slice::<SessionAwareInputs>(&Process::get_inputs())?;

    // TODO: Determine the session based on the current time.

    // TODO: Get the price for the current session.
    
    // Temporary placeholder response.
    let placeholder_response = PriceFeedResponse {
        price: "100.0".to_string(),
        session: Session::RegularHours
    };
    Process::success(&serde_json::to_vec(&placeholder_response)?);
}

```

We continue by adding two dependencies to our Cargo.toml, `chrono` and `chrono-tz`:

`Cargo.toml` :

```toml
[package]
name = "oracle-program"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
seda-sdk-rs = { version = "1.2" }
chrono = { version = "0.4", default-features = false }
chrono-tz = "0.10"

[profile.release-wasm]
inherits = "release"
lto = "fat"
opt-level = "z"
```

Now we create a new file in `/src` called `sessions.rs` . We will write our session determination logic in there.&#x20;

`sessions.rs`:

```rust
// sessions.rs
use anyhow::Result;
use chrono::{Datelike, Timelike, Weekday};
use chrono_tz::America::New_York;
use serde::{Deserialize, Serialize};

/// Moved from execution_phase.rs
#[derive(Serialize, Deserialize)]
pub enum Session {
    PreMarket,
    RegularHours,
    Closed,
}

impl Session {
    pub fn as_index(&self) -> usize {
        match self {
            Session::PreMarket => 0,
            Session::RegularHours => 1,
            Session::Closed => 2,
        }
    }
}

pub fn get_current_session() -> Result<Session> {
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?;
    let now_utc = chrono::DateTime::<chrono::Utc>::from_timestamp(now.as_secs() as i64, now.subsec_nanos())
        .expect("Failed to convert system time to UTC timestamp");

    // Convert to Eastern Time
    let now_et = now_utc.with_timezone(&New_York);
    let weekday = now_et.weekday();
    let hour = now_et.hour();
    let minute = now_et.minute();
    let time_mins = hour * 60 + minute; // minutes since midnight

    // Weekend check
    if weekday == Weekday::Sat || weekday == Weekday::Sun {
        return Ok(Session::Closed);
    }

    // US market hours (Eastern Time):
    // Pre-market: 4:00 AM - 9:30 AM ET
    // Regular:    9:30 AM - 4:00 PM ET
    const PREMARKET_START: u32 = 4 * 60;        // 4:00 AM = 240 mins
    const REGULAR_START: u32 = 9 * 60 + 30;     // 9:30 AM = 570 mins
    const REGULAR_END: u32 = 16 * 60;           // 4:00 PM = 960 mins

    let session = if time_mins >= PREMARKET_START && time_mins < REGULAR_START {
        Session::PreMarket
    } else if time_mins >= REGULAR_START && time_mins < REGULAR_END {
        Session::RegularHours
    } else {
        Session::Closed
    };

    Ok(session)
}
```

Add `mod sessions.rs;` to `main.rs` .

We then call the `get_current_session` function from `execution_phase.rs` :

```rust
pub fn execution_phase() -> Result<()> {
    // Get the input parameters for the data request (DR).
    let session_aware_inputs = serde_json::from_slice::<SessionAwareInputs>(&Process::get_inputs())?;

    // Determine the session based on the current time.
    let session = get_current_session()?;

```

We now want to fetch data for the correct, latest session. We can use the same `fetch_pyth` function we used in our previous example. Create the `src/fetch_pyth.rs` file:

```rust
// fetch_pyth.rs
use seda_sdk_rs::{HttpFetchOptions, proxy_http_fetch};
use serde::Deserialize;

const TESTNET_PROXY_PUBLIC_KEY: &str =
    "033ed60cfdeb7e91f718bf28e514e5c2f2990400b4643e86856e530de9b46dfb47";
const TESTNET_PROXY_URL: &str = "http://pyth.proxy.testnet.seda.xyz/proxy/";

/* Example Proxy Response:
{
    "parsed": [
        {
            "price": {
               "price": "3206259",
               "conf": "4107",
               "expo": -5,
               "publish_time": 1761595219
            }
        }
*/

#[derive(Debug, Deserialize)]
struct PythPriceParsedResponse {
    price: PythPriceData,
}

#[derive(Debug, Deserialize)]
pub struct PythPriceResponse {
    parsed: Vec<PythPriceParsedResponse>,
}

// They both have the same structure for price data, so we can reuse the same structs
#[derive(Clone, Debug, Deserialize,)]
pub struct PythPriceData {
    pub price: String,
    // pub conf: String, // Confidence interval
    // pub expo: i32, // Exponent - can be used to convert the price to a more readable format
    pub publish_time: i64,
}


pub fn fetch_hermes_pyth_price(ids: Vec<String>) -> Result<PythPriceData, serde_json::Error> {
    // This proxy expects the ids to be in the format "ids[]=id1&ids[]=id2&ids[]=id3"
    let ids_str = ids.join("&ids[]=");

    // Fetch the price data from the Pyth proxy - verify the response is valid and signed by the proxy key
    let response = proxy_http_fetch(
        [TESTNET_PROXY_URL, "price/latest?ids[]=", ids_str.as_str()].concat(),
        Some(TESTNET_PROXY_PUBLIC_KEY.to_string()),
        Some(HttpFetchOptions {
            method: seda_sdk_rs::HttpFetchMethod::Get,
            headers: Default::default(),
            body: None,
            timeout_ms: Some(2_000),
        }),
    );

    // Parse the response into a PythPriceResponse struct
    let parsed_pyth_response = serde_json::from_slice::<PythPriceResponse>(&response.bytes)?;

    // Get the first (and only) price data from the response
    let price_data = 
        parsed_pyth_response
        .parsed
        .first()
        .expect("No price data found")
        .price.clone();

    // Return the price data
    Ok(price_data)
}
```

Add `mod fetch_pyth.rs;` to `main.rs` .

Now fetch the price for the correct session and return it as `PriceFeedResponse`:

```rust
pub fn execution_phase() -> Result<()> {
    // Get the input parameters for the data request (DR).
    let session_aware_inputs = serde_json::from_slice::<SessionAwareInputs>(&Process::get_inputs())?;

    // Determine the session based on the current time.
    let session = get_current_session()?;

    let price = fetch_hermes_pyth_price(vec![session_aware_inputs.pyth_assets[session.as_index()].clone()])?;    

    // Temporary placeholder response.
    let response = PriceFeedResponse {
        price: price.price.to_string(),
        session: session,
    };
    Process::success(&serde_json::to_vec(&response)?);
}
```

Almost finished! Just implement the simple `tally_phase.rs` , similar as what we did with the crypto data feed example:

```rust
use anyhow::Result;
use crate::execution_phase::PriceFeedResponse;
use seda_sdk_rs::{Process, get_reveals};

pub fn tally_phase() -> Result<()> {
    // Retrieve consensus reveals from the tally phase.
    let reveals = get_reveals()?;

    if reveals.is_empty() {
        Process::error("No consensus among revealed results".as_bytes());
    }

    // Parse the first reveal as the response
    // SEDA Fast only has one executor, so we use the first (and only) reveal
    let latest_response: PriceFeedResponse = reveals
        .into_iter()
        .map(|reveal| serde_json::from_slice(&reveal.body.reveal))
        .next()
        .ok_or_else(|| anyhow::anyhow!("No reveals to process"))??;

    Process::success(&serde_json::to_vec(&latest_response)?)
}
```

### SEDA Fast Tally Phase (Single Executor Forwarding)

```rust
pub fn tally_phase() -> Result<()> {
    let reveals = get_reveals()?;

    if reveals.is_empty() {
        Process::error("No consensus among revealed results".as_bytes());
    }

    let latest_response: PriceFeedResponse = reveals
        .into_iter()
        .map(|reveal| serde_json::from_slice(&reveal.body.reveal))
        .next()
        .ok_or_else(|| anyhow::anyhow!("No reveals to process"))??;

    Process::success(&serde_json::to_vec(&latest_response)?)
}
```

***

### SEDA Core Tally Phase (Consensus-Based Aggregation)

For SEDA Core compatibility, you can extend tally logic to:

* Parse all reveals
* Verify matching session
* Select median price or most recent publish\_time
* Encode result in big-endian for EVM compatibility

Core uses multiple Overlay Nodes and aggregates execution results before returning the final output onchain.

***

## Build & Deploy

Build:

```
bun run build
```

***

### SEDA Fast Deployment

```
bun run deploy
```

Query:

```
curl -L -X POST 'https://fast-api.testnet.seda.xyz/execute?encoding=json&includeDebugInfo=true' \
-H 'Authorization: Bearer {{BEARER_TOKEN}}' \
-H 'Content-Type: application/json' \
--data-raw '{
    "execProgramId": "8cf7808cdb5d16e9fb328007968b31727f8e67ac3c83b655aa61c4ccd4077125",
    "execInputs": {
        "pyth_assets": [
            "0x61c4ca5b9731a79e285a01e24432d57d89f0ecdd4cd7828196ca8992d5eafef6",
            "0xb1073854ed24cbc755dc527418f52b7d271f6cc967bbf8d8129112b18860a593"
        ]
    }
}'
```

***

### SEDA Core Deployment

After building, you'll have the `.wasm` artifacts in the `build` directory.

These artifacts can be deployed to the SEDA network and referenced in onchain Data Requests from supported networks.

Differences between SEDA Core and Fast:

| Component             | SEDA Fast              | SEDA Core                          |
| --------------------- | ---------------------- | ---------------------------------- |
| Execution Replication | Single executor        | Multiple Overlay Nodes             |
| Tally Behavior        | Forward first reveal   | Aggregated consensus (e.g. median) |
| Access Method         | Authenticated REST API | Onchain Data Request               |
| Encoding              | JSON common            | Big-endian for EVM                 |

The Oracle Program logic remains portable between both delivery methods.
