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
Quick Start
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 200000000000000Direct 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 200000000000000About 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.00Direct 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
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.
| Method | Arguments | Description |
|---|---|---|
get_price_data | asset_ids?: string[] | Get cached prices (returns null if cache empty) |
can_subsidize_outlayer_calls | — | Check if contract pays for calls |
get_oracle_price_data | account_id, asset_ids? | Get prices from specific oracle |
Call Methods (require deposit)
| Method | Deposit | Description |
|---|---|---|
request_price_data | 0.01+ NEAR | Get prices directly |
oracle_call | 0.01+ NEAR | Get prices with callback |
request_custom_data | 0.01+ NEAR | Fetch custom external data |
custom_call | 0.01+ NEAR | Custom 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 themKey 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_idin callbacks - Context: Use the
msgfield to pass context through async chain
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)
| Method | Deposit | Description |
|---|---|---|
refresh_prices | 0.02 NEAR | Fetch 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)
| Method | Description |
|---|---|
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
| Asset | Pyth Price ID |
|---|---|
| NEAR | c415de8d2efa7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750 |
| ETH | ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace |
| BTC | e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43 |
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¤t_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
| Method | Fresh Cache | Stale (OutLayer) | Subsidized |
|---|---|---|---|
get_price_data | Free | N/A | N/A |
request_price_data | Free | 0.01+ NEAR | Free |
oracle_call | 1 yoctoNEAR | 0.01+ NEAR | Free |
request_custom_data | N/A | 0.01+ NEAR | Free |
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