TEE-Secured Price Oracle
On-Demand Oracle with Sustainable Economics — Based on OutLayer
Key Features
- ✓ Proactive price pushing — prices always fresh in contract (30-60s updates)
- ✓ Zero trust — all data processed inside Intel TDX enclave
- ✓ DAO-governed — all configuration managed through council proposals
- ✓ TEE-only signing — only keys generated inside TEE can push prices
- ✓ 10 price sources with median aggregation
- ✓ Native Pyth-compatible API — migrate from pyth-oracle.near by changing one address
- ✓ 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. Prices are proactively pushed to the contract every 30-60 seconds by TEE workers, so data is always fresh.
You can also call request_price_data to trigger an immediate on-demand update from TEE if needed.
Response Format
{
"timestamp": "1706889600000000000",
"recency_duration_sec": 120,
"prices": [
{
"asset_id": "wrap.near",
"price": { "multiplier": "500000000", "decimals": 8 }
}
]
}
// Price conversion: 500000000 / 10^8 = $5.00Governance & Security
All Changes Go Through DAO
Every contract state mutation — adding assets, configuring exchanges, registering push signers, upgrading the contract — requires a DAO council proposal with >50% approval. No single key can modify the oracle.
TEE-Only Price Pushing
Prices are pushed to the contract by implicit accounts derived from TEE-generated keys. The private key is created inside the TEE (Intel TDX) and never leaves it — no human, including the project owner, ever sees it.
How PROTECTED_ keys work: 1. Project owner creates a secret (e.g., PROTECTED_KEY_RHEA) in OutLayer dashboard 2. Private key generated INSIDE TEE — never exposed to anyone 3. DAO proposal registers the derived implicit account as trusted oracle 4. Only this account can call report_prices for assigned assets 5. WASI code inside TEE signs transactions with the key Result: No human holds the signing key. Only verified TEE code can push prices.
DAO Proposal Actions
| Action | Description |
|---|---|
AddAsset / RemoveAsset | Manage tracked assets |
SetAssetExchangeConfig | Configure exchange tickers, Pyth/Chainlink feeds per asset |
RegisterPushSigner | Register TEE-derived account as trusted price pusher |
ConfigureOutlayer | Set OutLayer integration parameters |
ProposeUpgrade / ExecuteUpgrade | Two-phase contract upgrade via DAO vote |
Self-Service for Projects
Third-party projects can operate their own push signers:
- Create a TEE secret (
PROTECTED_KEY_*) in OutLayer dashboard - DAO proposal to register the key for specific assets
- Fund the derived implicit account with NEAR
- Scheduler pushes prices autonomously from TEE
Native Pyth Interface
price-oracle.near implements Pyth-compatible view methods natively. DeFi contracts using pyth-oracle.near can migrate by changing one contract address — no code changes needed.
No refresh_prices Needed
Unlike the separate Pyth wrapper contract, the native interface reads directly from contract state which is proactively updated every 30-60 seconds by the scheduler. View methods always return fresh data.
View Methods (free, always fresh)
| Method | Description |
|---|---|
get_price(price_identifier) | Latest price with staleness check |
get_price_unsafe(price_identifier) | Latest price without staleness check |
get_price_no_older_than(price_id, age) | Price only if published within age seconds |
get_ema_price(price_id) | EMA price with staleness check |
get_ema_price_unsafe(price_id) | EMA price without staleness check |
list_prices(price_ids) | Batch: multiple feeds at once |
price_feed_exists(price_identifier) | Check if feed is configured |
get_update_fee_estimate(data) | Returns 1 yoctoNEAR (no update needed) |
Migration from Pyth
// Before (Pyth)
const ORACLE: &str = "pyth-oracle.near";
// After (Oracle-Ark) — no other changes needed!
const ORACLE: &str = "price-oracle.near";Response Format
// PythPrice format (same as pyth-oracle.near)
{
"price": 525000000, // price * 10^|expo|
"conf": 0, // confidence (always 0 for Oracle-Ark)
"expo": -8, // exponent: actual_price = price * 10^expo
"publish_time": 1706900000 // unix timestamp (seconds)
}
// Example: price=525000000, expo=-8 → $5.25Direct 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
Always-fresh prices
price-oracle.near receives proactive price updates every 30-60 seconds from TEE workers. View methods like get_price_data always return fresh data — no paid call needed.
You can also integrate with OutLayer directly from your own contract (see Direct OutLayer Integration section above).
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)
Prices are proactively pushed every 30-60 seconds. get_price_data always returns fresh data. You can also call request_price_data for an immediate on-demand update.
| 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.
Legacy Pyth Wrapper
Pyth-compatible methods are now built into price-oracle.near directly. The separate price-oracle-pyth.near wrapper is no longer needed.
See the section for migration instructions. Simply change your contract address to price-oracle.near — all Pyth view methods work natively, with always-fresh prices (no refresh_prices call needed).
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