TEE-Secured Price Oracle

On-Demand Oracle with Sustainable Economics — Based on OutLayer

Key Features

  • ✓ Prices always "warm" in TEE — instant delivery to contracts
  • ✓ Zero trust — all data processed inside Intel TDX enclave
  • ✓ 9+ price sources with median aggregation
  • ✓ Pyth-compatible API for easy migration
  • ✓ Custom data fetching from any HTTP API
  • ✓ Subsidized mode — free calls when contract has funds
13
Supported Tokens
9+
Price Sources
<1s
Response Time

Quick Start

Recommended

Get Prices with Callback

Use oracle_call to request prices. Your contract receives data via oracle_on_call callback.

near call price-oracle.near oracle_call '{
  "receiver_id": "your-contract.near",
  "asset_ids": ["wrap.near", "aurora"],
  "msg": ""
}' --accountId your.near --deposit 0.02 --gas 200000000000000

Direct Price Request (no callback)

For scripts and testing — returns prices directly.

near call price-oracle.near request_price_data '{
  "asset_ids": ["wrap.near"]
}' --accountId your.near --deposit 0.02 --gas 200000000000000
⚠️

About View Methods (get_price_data)

get_price_data is a free view method, but it only returns data if someone recently paid for an update. Due to the on-demand nature, the cache is usually empty — prices are fetched when needed for specific operations (liquidations, borrowing, swaps), not stored permanently.

This is by design: Unlike traditional oracles with a central price feed, this oracle delivers prices directly to your contract. Any contract can integrate without intermediaries — no shared "price feed contract" required.

Response Format

{
  "timestamp": "1706889600000000000",
  "recency_duration_sec": 120,
  "prices": [
    {
      "asset_id": "wrap.near",
      "price": { "multiplier": "500000000", "decimals": 8 }
    }
  ]
}
// Price conversion: 500000000 / 10^8 = $5.00

Direct OutLayer Integration

You don't need to use price-oracle.near at all. Your contract can call OutLayer directly to fetch prices or any custom data from TEE.

Why Go Direct?

  • No intermediary contracts — full control over the flow
  • Custom WASI workers — fetch any data you need
  • Lower gas costs — one less cross-contract call
  • Your contract owns the entire integration

Step 1: Call OutLayer request_execution

use near_sdk::{ext_contract, AccountId, NearToken, Promise, serde_json};

#[ext_contract(ext_outlayer)]
pub trait OutLayer {
    fn request_execution(
        &mut self,
        execution_source: serde_json::Value,
        resource_limits: Option<serde_json::Value>,
        input_data: Option<String>,
        secrets_ref: Option<serde_json::Value>,
        response_format: Option<String>,
        payer_account_id: Option<AccountId>,
        callback_receiver_id: Option<AccountId>,
    ) -> Promise;
}

impl Contract {
    pub fn fetch_price(&mut self, token_id: String) -> Promise {
        // Use the deployed price oracle project
        // Mainnet: "price-oracle.near/price-oracle"
        // Testnet: "price-oracle.testnet/price-oracle"
        let execution_source = serde_json::json!({
            "Project": {
                "project_id": "price-oracle.near/price-oracle"
            }
        });

        // Resource limits (recommended)
        let resource_limits = serde_json::json!({
            "max_instructions": 10000000000_u64,
            "max_memory_mb": 128,
            "max_execution_seconds": 60
        });

        // Input data for the WASI worker (see OracleCommand in types.rs)
        let input_data = serde_json::json!({
            "command": "get_prices",
            "tokens": [token_id]
        }).to_string();

        // Call OutLayer directly
        ext_outlayer::ext("outlayer.near".parse().unwrap())
            .with_attached_deposit(NearToken::from_millinear(10)) // 0.01 NEAR
            .with_unused_gas_weight(1)
            .request_execution(
                execution_source,
                Some(resource_limits),          // resource limits
                Some(input_data),               // your request
                None,                           // no secrets needed
                Some("json".to_string()),       // response format
                Some(env::predecessor_account_id()), // payer
                Some(env::current_account_id()), // callback receiver
            )
    }
}

Step 2: Handle the Callback

// OutLayer calls this method with the TEE result
#[private] // Only callable by self (via promise)
pub fn on_outlayer_result(
    &mut self,
    #[callback_result] result: Result<serde_json::Value, near_sdk::PromiseError>,
) {
    match result {
        Ok(data) => {
            // Parse the price data from TEE response
            if let Some(prices) = data.get("prices") {
                // Process your prices here
                log!("Got prices from TEE: {:?}", prices);
            }
        }
        Err(e) => {
            log!("OutLayer call failed: {:?}", e);
        }
    }
}

Architecture: Direct vs Via Oracle Contract

Via price-oracle.near (simpler):
Your Contract → price-oracle.near → OutLayer → TEE → price-oracle.near → Your Contract

Direct OutLayer (more control):
Your Contract → OutLayer → TEE → Your Contract

Both are valid! Use price-oracle.near for quick integration,
or go direct for full customization.
⚠️

Important Notes

  • You need to deploy your own WASI worker or use an existing one (like the price oracle WASI)
  • For price fetching, it's easier to use price-oracle.near — it handles WASI configuration for you
  • Direct integration is best for custom data sources or when you need full control
  • See price-oracle contract source for a complete example

Price Oracle Contract

i

This contract is optional

price-oracle.near is provided as an example of caching data from TEE. You can integrate with OutLayer directly from your own contract (see section above).

Note on caching: Caching only helps when there are many simultaneous requests for the same data. In practice, data is fetched from TEE specifically for each user's request and immediately used in their operation. The cache is usually empty or stale — this is by design for an on-demand oracle.

Contract address: price-oracle.near

This contract recreates the interface (with additions) of the original NEAR Native Price Oracle — existing integrations can migrate with minimal changes.

View Methods (free)

⚠️

get_price_data only returns prices if someone recently called request_price_data or oracle_call. Due to the on-demand design, the cache is usually empty — use call methods to fetch fresh data.

MethodArgumentsDescription
get_price_dataasset_ids?: string[]Get cached prices (returns null if cache empty)
can_subsidize_outlayer_callsCheck if contract pays for calls
get_oracle_price_dataaccount_id, asset_ids?Get prices from specific oracle

Call Methods (require deposit)

MethodDepositDescription
request_price_data0.01+ NEARGet prices directly
oracle_call0.01+ NEARGet prices with callback
request_custom_data0.01+ NEARFetch custom external data
custom_call0.01+ NEARCustom data with callback

Data Types

// Price format: multiplier / 10^decimals = USD
struct Price {
    multiplier: u128,  // e.g., 500000000 for $5.00
    decimals: u8,      // usually 8
}

struct PriceData {
    timestamp: u64,              // nanoseconds
    recency_duration_sec: u32,   // max age for "fresh" prices
    prices: Vec<AssetOptionalPrice>,
}

struct AssetOptionalPrice {
    asset_id: String,
    price: Option<Price>,  // None if stale/unavailable
}

Callback Interface

// Your contract must implement this for oracle_call
pub fn oracle_on_call(
    &mut self,
    sender_id: AccountId,
    data: PriceData,
    msg: String,
) {
    // Verify caller is the oracle
    assert_eq!(
        env::predecessor_account_id(),
        "price-oracle.near".parse::<AccountId>().unwrap(),
        "Only oracle can call"
    );
    // Process prices...
}

Integration Example: Wrapper Contract

A complete example showing how to integrate the oracle with the full callback cycle. The wrapper contract self-funds oracle calls and handles callbacks internally.

Contract: price-oracle-wrapper.near | Source on GitHub

How It Works

User calls get_price() on Wrapper
        │
        ▼
Wrapper calls oracle_call() with SELF as receiver_id
(self-funded: 0.02 NEAR attached automatically)
        │
        ▼
Oracle processes request via OutLayer TEE
        │
        ▼
Oracle calls oracle_on_call() on Wrapper
        │
        ▼
Wrapper receives prices in callback, processes them

Key Pattern: Self-Funding Calls

// Wrapper pays for oracle calls itself - users don't need to attach deposits
pub fn get_price(&mut self, token_id: String) -> Promise {
    ext_oracle::ext(self.oracle_contract_id.clone())
        .with_attached_deposit(NearToken::from_millinear(20)) // 0.02 NEAR
        .with_unused_gas_weight(1)
        .oracle_call(
            env::current_account_id(), // callback comes back HERE
            Some(vec![token_id]),
            String::new(),
            None,
        )
}

// Callback handler - called by oracle with price data
pub fn oracle_on_call(
    &mut self,
    sender_id: AccountId,
    data: PriceData,
    msg: String,
) -> Option<Price> {
    // IMPORTANT: verify caller is the oracle!
    assert_eq!(env::predecessor_account_id(), self.oracle_contract_id);

    // Extract price from data
    if let Some(asset) = data.prices.first() {
        return asset.price.clone();
    }
    None
}

Why This Pattern?

  • Self-funding: Users call your contract without deposits — your contract pays for oracle calls
  • Full cycle: Request → TEE → Callback all handled in one user transaction
  • Security: Always verify predecessor_account_id in callbacks
  • Context: Use the msg field to pass context through async chain
i

All example contracts are optional! The contracts we provide (price-oracle.near, price-oracle-wrapper.near, etc.) are just examples. You can integrate with OutLayer directly from your own contract — see the next section.

Pyth-Compatible Wrapper

Drop-in replacement for pyth-oracle.near. Switch oracles with zero code changes.

Contract address: price-oracle-pyth.near

⚠️

Important: Call refresh_prices First!

View methods (get_price, list_prices) only return data after someone calls refresh_prices. Without this, all view calls return null.

Typical flow: Call refresh_prices (0.02 NEAR) → then use view methods (free). On mainnet, prices may already be warm from other users calling refresh.

Call Method (required before view methods)

MethodDepositDescription
refresh_prices0.02 NEARFetch fresh prices from TEE and update cache. Required before view methods return data.
# Step 1: Refresh prices (required once)
near call price-oracle-pyth.near refresh_prices '{}' \
  --accountId your.near --deposit 0.02 --gas 300000000000000

# Step 2: Now view methods work (free)
near view price-oracle-pyth.near get_price '{
  "price_identifier": "c415de8d2efa7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750"
}'

View Methods (free, but require refresh_prices first)

MethodDescription
get_price(price_identifier)Get price with staleness check (returns null if cache empty)
get_price_unsafe(price_identifier)Get price without staleness check (returns null if cache empty)
list_prices(price_ids)Batch get multiple prices (returns null values if cache empty)
price_feed_exists(price_identifier)Check if feed is configured

Price Feed IDs

AssetPyth Price ID
NEARc415de8d2efa7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750
ETHff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
BTCe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43

Migration from Pyth

// Before (Pyth)
const ORACLE: &str = "pyth-oracle.near";

// After (Oracle-Ark) — no other changes needed!
const ORACLE: &str = "price-oracle-pyth.near";

Custom Data Sources

Fetch data from any HTTP API via TEE using request_custom_data or custom_call.

Request Format

{
  "custom_data_request": [
    {
      "id": "my_data",           // Identifier for the result
      "token_id": "",            // Optional token identifier
      "source": {
        "custom": {
          "url": "https://api.example.com/data",
          "json_path": "result.value",   // Dot notation path
          "value_type": "number",        // "number", "string", "boolean"
          "method": "GET",               // "GET" or "POST"
          "headers": []                  // Optional headers
        }
      }
    }
  ]
}

Examples

Steam Game Price

{
  "url": "https://store.steampowered.com/api/appdetails?appids=1245620",
  "json_path": "1245620.data.price_overview.final_formatted"
}

Account NFTs (FastNEAR)

{
  "url": "https://api.fastnear.com/v1/account/root.near/nft",
  "json_path": "tokens"
}

Weather Data

{
  "url": "https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&current_weather=true",
  "json_path": "current_weather.temperature"
}

Code Examples

Rust Integration

use near_sdk::{ext_contract, AccountId, Gas, NearToken, Promise};

#[ext_contract(ext_oracle)]
pub trait Oracle {
    fn oracle_call(
        &mut self,
        receiver_id: AccountId,
        asset_ids: Option<Vec<String>>,
        msg: String,
        resource_limits: Option<serde_json::Value>,
    ) -> Promise;
}

impl Contract {
    pub fn get_prices_with_callback(&self) -> Promise {
        ext_oracle::ext("price-oracle.near".parse().unwrap())
            .with_attached_deposit(NearToken::from_millinear(20))
            .with_static_gas(Gas::from_tgas(150))
            .oracle_call(
                env::current_account_id(),
                Some(vec!["wrap.near".to_string()]),
                "swap".to_string(),
                None,
            )
    }
}

JavaScript Integration

import { connect, Contract } from 'near-api-js';

const oracle = new Contract(account, 'price-oracle.near', {
  viewMethods: ['get_price_data'],
  changeMethods: ['request_price_data', 'oracle_call'],
});

// View cached prices (free)
const cached = await oracle.get_price_data({
  asset_ids: ['wrap.near', 'aurora'],
});

// Convert price
const price = cached.prices[0].price;
const usd = Number(price.multiplier) / Math.pow(10, price.decimals);
console.log(`NEAR = $${usd}`);

Deposit Requirements

MethodFresh CacheStale (OutLayer)Subsidized
get_price_dataFreeN/AN/A
request_price_dataFree0.01+ NEARFree
oracle_call1 yoctoNEAR0.01+ NEARFree
request_custom_dataN/A0.01+ NEARFree

Subsidized Mode

When contract has >20 NEAR and subsidy is enabled, all OutLayer calls are free. Check with can_subsidize_outlayer_calls().

Try It Out

Use the interactive playground to test oracle methods without writing code.

Open Playground