TEE-Secured Price Oracle
On-Demand Oracle with Sustainable Economics — Based on OutLayer
Key Features
- ✓ On-demand delivery — your contract gets the price in a callback, no dependency on a shared feed
- ✓ 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
- ✓ 16 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", "eth.bridge.near"],
"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, and it is the one path that can hand you a stale price without saying so. On-chain writes are a separate, much slower cycle than the off-chain feed, they cost gas, and they pause on their own when the pushing account runs low — so a view can legitimately return something minutes old, or nothing at all. Check the timestamp it returns against your own bound and fail closed; never treat a view as evidence of freshness.
For anything that moves money, use request_price_data (or oracle_call): the price is fetched in the enclave for that call and delivered to your contract in a callback, so freshness is a property of the request rather than of whatever happens to be stored.
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 |
UpgradeContract | Contract upgrade via DAO vote (after upload_upgrade_code) |
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
Anyone Can Push Prices On-Chain
The on-chain update is permissionless. Anyone can call the worker with update_prices and update_contract: true and have fresh prices written to the oracle contract — paying only for the WASI execution, and working even when our scheduler is down. The feed does not depend on a single operator staying online.
A caller cannot influence the price. The worker fetches and aggregates the sources itself inside the enclave, and the resulting report_prices transaction is signed by a TEE-generated key whose private half never leaves the enclave. The contract accepts a report only from the push_signer_accounts registered for that asset, so a caller-supplied price is rejected by construction.
Two limits bound the cost. An asset reported to the contract less than 20 seconds ago is skipped, so repeated triggers cannot spam transactions. And gas comes from the push signer's implicit account: an empty balance simply means no on-chain push, while prices in public storage keep updating either way.
Data Freshness & Attestation
Every result is attested by OutLayer's TEE: the signature proves this WASM binary produced this output inside Intel TDX. For the full trust model — what the signature does and does not prove, and how to verify it — see the platform attestation docs. This section covers what is specific to the oracle.
Two ways prices reach your contract
Pull — yield/resume
Your contract calls oracle_call / request_price_data. If the on-chain cache is fresh it returns immediately; if stale, the contract yields, the TEE fetches and returns prices inline, and execution resumes with the result.
Push — scheduled
An off-chain scheduler triggers the TEE worker, which fetches, aggregates, and signs report_prices with a PROTECTED_ key generated inside the TEE. Prices stay warm in contract state for free get_price_data reads.
Generation time vs. source age
A price is timestamped when the runner read the source, not by the age of the source's own data. Only Pyth exposes an upstream publish time that the oracle enforces; every other endpoint returns a value with no timestamp, so its reading is only as fresh as the fetch.
| Source | Upstream timestamp | Staleness check |
|---|---|---|
pyth | Yes (publish_time) | Rejected if older than 120s |
| all others (coingecko, binance, chainlink, huobi, kucoin, gate, cryptocom, …) | No | Stamped with fetch time |
On-chain freshness bounds
Reads from contract state are bounded by several on-chain checks, so a consumer never silently gets an ancient value:
recency_duration_sec— reports older than this window are ignored; a stale asset returnsprice: null.- Majority-of-oracles quorum — a price is returned only if enough recent reports agree (median of recent reports).
pyth_stale_threshold(default 60s) — enforced by the Pyth-compatible getters.
For a cross-chain view call, include the source block height in the WASI output so your contract can enforce its own deadline — OutLayer attests whatever the program returns.
Verifiable Signed Prices
Pull prices over HTTPS with an Ed25519 signature you verify yourself. You check the signature instead of trusting the transport, the operator, or us. Verification works off-chain today and on-chain whenever you want it to — the signed bytes are the same.
Why you would want this
No TEE infrastructure of your own
Getting trustworthy prices normally means running your own enclave: attestation, key management, node operations, and the cost of keeping it alive. Here that work is already done and attested — you consume a signed feed and verify 64 bytes.
You stay in control of the on-chain write
We do not push anything into your contract. You decide when to submit, at what cadence, and under which conditions — and you pay that gas yourself. No dependency on a relayer that could stall, disappear, or price its service however it likes.
The relayer does not have to be trusted
Because the payload is signed at the source, whoever carries it cannot alter it. That can be your own server, a keeper bot, or anyone else — the signature, not the messenger, is what your contract checks.
One input among several
Already reading other oracles? Use exclude_sources to drop the ones you consume directly, and this feed stays genuinely independent rather than echoing a price you already have.
What the signature proves. The feed is signed inside the enclave with a key whose name starts with PROTECTED_. That prefix is not a convention — OutLayer generates such secrets inside the TEE, and their value is never shown to anyone, including the project owner (how PROTECTED_ secrets are created). A valid signature therefore means the payload came out of the attested binary, not from an operator holding a key on a laptop. That key is fixed in the worker's source and signs nothing else — in particular it never signs a NEAR transaction, so a feed signature can never be replayed as one, and no request can ask for a different signer.
It does not mean the price is correct — that follows from auditing the (open-source) worker and from the sources it aggregates. Signature = origin, not truth.
Step 1 — Get the public key (once)
Ask the worker for the public half of the signing key and pin it in your code or contract. get_public_key is the one call that names a key: key_name selects which PROTECTED_ secret to read, and it returns public material only.
curl -sX POST https://api.outlayer.ai/call/price-oracle.near/price-oracle \
-H "X-Payment-Key: $PAYMENT_KEY" -H "Content-Type: application/json" \
-d '{
"input": { "command": "get_public_key", "key_name": "PROTECTED_RHEA_FEED_KEY" },
"secrets_ref": { "profile": "oracle", "account_id": "price-oracle.near" }
}'You do not have to take our word for that key — the call that produced it is recorded and attested, and both records show the request and the response side by side:
- On-chain transaction. The execution is on NEAR, so the input and the returned public key are both public and immutable — example transaction. Anyone can read what was asked and what came back, years later.
- TEE attestation. The same call has a TDX attestation — example attestation. Press 🔍 Load & Verify from Blockchain there to pull the input and output straight from the chain and check them against the hashes committed in the quote. Since the Task Hash covers
output_hash, a matching quote proves this exact public key came out of the attested binary inside the enclave.
Step 2 — Request signed prices
Note there is no key parameter here. Unlike get_public_key, this request cannot select a signing key: the feed is always signed with the one key above, fixed in the worker's source. A request that still sends key_name is accepted and the field is ignored, so older clients keep working.
curl -sX POST https://api.outlayer.ai/call/price-oracle.near/price-oracle \
-H "X-Payment-Key: $PAYMENT_KEY" -H "Content-Type: application/json" \
-d '{
"input": {
"command": "get_signed_prices",
"tokens": ["wrap.near", "eth.bridge.near", "usdt.tether-token.near"],
"max_age_secs": 120,
"exclude_sources": ["pyth"]
},
"secrets_ref": { "profile": "oracle", "account_id": "price-oracle.near" }
}'| Field | Default | Meaning |
|---|---|---|
tokens | required | Only the assets you ask for are fetched, signed and billed — request one or twenty, the rest of the feed is not your concern |
max_age_secs | 120 | Your freshness window. It filters sources: the price is aggregated over exactly the venues observed within it, and publish_time is the oldest of them — so it is never larger than what you asked for. If too few venues qualify, we fetch fresh rather than serve a thinner set; if an asset still cannot be priced inside the window, the whole request fails instead of returning a stale entry |
exclude_sources | none | Drop sources you already consume yourself, so our feed stays an independent input. Unknown names are rejected, never ignored |
sig_format | json | json or borsh |
expo | -8 | Price is an integer scaled by 10expo |
min_sources_num | 1 | Minimum venues that must be inside your window for an asset to be priced. The default of 1 is a floor, not a recommendation — a lending market should raise it, so a narrow window can never be answered by a single venue |
Example response (real output)
{
"success": true,
"payload": "{\"eth.bridge.near\":{\"price\":\"196837500000\",\"expo\":-8,\"publish_time\":1785149621},\"usdt.tether-token.near\":{\"price\":\"99908500\",\"expo\":-8,\"publish_time\":1785149622},\"wrap.near\":{\"price\":\"184283333\",\"expo\":-8,\"publish_time\":1785149621}}",
"signature": "YC2Nd2IyViEp7JKIDqkehHT9bnn2qTJl8iP0frNrlK63NJTjNO0LbI8u28qmH66+mEP2IIi+NrC4oIkXxk/uBw==",
"public_key": "ed25519:FU6EnB4UaAiDCAxvQPkRUu5QQExgzvKQAX891wMEX3rU",
"sig_format": "json",
"error": null
}payload is a string, not an object — it is the signed message. Keys are the oracle's own asset ids, sorted, so the bytes are reproducible. price is an integer sent as a string (it is an i64; a JSON number would lose precision in some parsers). Real price = price × 10^expo, e.g. 184283333 × 10⁻⁸ = $1.84283333. publish_time is the unix second at which the enclave read and aggregated the sources.
The one rule that breaks integrations: verify the signature over the exact bytes of the payload string. Do not parse it and re-serialize before verifying — key order, whitespace and number formatting will differ and the signature will fail. Parse it only after the signature checks out. For borsh, verify over base64_decode(payload), not over the base64 text.
Verifying the signature (JavaScript / Python / Rust)
JavaScript (Node 18+, no dependencies beyond a base58 helper):
import { verify, createPublicKey } from 'node:crypto';
import bs58 from 'bs58';
const PINNED = 'ed25519:FU6EnB4UaAiDCAxvQPkRUu5QQExgzvKQAX891wMEX3rU';
function verifyFeed(res) {
if (res.public_key !== PINNED) throw new Error('unexpected signing key');
// DER-wrap the raw 32-byte key so node's crypto can import it
const raw = bs58.decode(res.public_key.split(':')[1]);
const der = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), raw]);
const key = createPublicKey({ key: der, format: 'der', type: 'spki' });
const message = Buffer.from(res.payload, 'utf8'); // EXACT bytes, no re-serialize
if (!verify(null, message, key, Buffer.from(res.signature, 'base64')))
throw new Error('bad signature');
const prices = JSON.parse(res.payload); // safe only after verifying
const now = Math.floor(Date.now() / 1000);
for (const [asset, p] of Object.entries(prices)) {
if (now - p.publish_time > 120) throw new Error(`${asset} too old`);
}
return prices;
}Python:
import base64, base58, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
PINNED = "ed25519:FU6EnB4UaAiDCAxvQPkRUu5QQExgzvKQAX891wMEX3rU"
def verify_feed(res):
assert res["public_key"] == PINNED, "unexpected signing key"
vk = Ed25519PublicKey.from_public_bytes(base58.b58decode(PINNED.split(":", 1)[1]))
vk.verify(base64.b64decode(res["signature"]), res["payload"].encode()) # raises if invalid
return json.loads(res["payload"]) # parse only after the signature is verifiedRust (also what an on-chain verifier does):
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let key_bytes: [u8; 32] = bs58::decode(pinned_pubkey).into_vec()?.try_into().unwrap();
let vk = VerifyingKey::from_bytes(&key_bytes)?;
let sig = Signature::from_bytes(&base64_decode(signature_b64)?.try_into().unwrap());
vk.verify(payload.as_bytes(), &sig)?; // payload as received, byte for byteVerifying inside a NEAR contract
NEAR exposes Ed25519 verification as a host function, so checking the feed on-chain is cheap and needs no crypto library. This is what makes the relayer untrusted: anyone may submit the payload, and the contract accepts it purely on the signature.
Rust: a receiver anyone can call
use near_sdk::{env, near, require, store::LookupMap};
use near_sdk::base64::{engine::general_purpose::STANDARD, Engine};
use near_sdk::json_types::I64;
use near_sdk::serde::Deserialize;
use std::collections::HashMap;
/// The pinned feed key: base58 of "ed25519:..." decoded to 32 raw bytes.
const FEED_PUBKEY: [u8; 32] = [/* 32 bytes */];
const MAX_AGE_SECS: u64 = 120;
#[derive(Deserialize)]
#[serde(crate = "near_sdk::serde")]
struct Entry {
price: I64, // sent as a decimal string; I64 is exactly that convention
expo: i32,
publish_time: i64,
}
#[near]
impl Contract {
/// Permissionless: the caller is untrusted, the signature is what counts.
pub fn submit_prices(&mut self, payload: String, signature: String) {
// 1. Verify over the EXACT bytes received — never re-serialize first.
let sig: [u8; 64] = STANDARD.decode(&signature).expect("bad base64")
.try_into().expect("signature must be 64 bytes");
require!(
env::ed25519_verify(&sig, payload.as_bytes(), &FEED_PUBKEY),
"invalid feed signature"
);
// 2. Only after it verifies is the payload safe to parse.
let entries: HashMap<String, Entry> =
near_sdk::serde_json::from_str(&payload).expect("malformed payload");
let now = env::block_timestamp() / 1_000_000_000;
for (asset, e) in entries {
let published = e.publish_time as u64;
require!(now.saturating_sub(published) <= MAX_AGE_SECS, "price too old");
// 3. Replay guard: a signed payload stays valid forever, so refuse
// anything that is not strictly newer than what we already store.
if let Some(prev) = self.prices.get(&asset) {
require!(published > prev.publish_time, "not newer than stored");
}
self.prices.insert(asset, StoredPrice { price: e.price.0, expo: e.expo, publish_time: published });
}
}
}Three checks carry the whole design: the signature (authenticity), the age bound (freshness), and the strictly-increasing publish_time (replay). Drop any of them and an old but validly signed payload can be replayed later.
Borsh format (for on-chain verification)
Pass "sig_format": "borsh". The payload becomes base64 of the borsh bytes, and the signature is over the decoded bytes. Layout of BTreeMap<String, PriceEntry>, keys ascending:
u32 entry_count (little-endian)
repeated per entry:
u32 key_len (little-endian)
.. key bytes (UTF-8)
i64 price (little-endian)
i32 expo (little-endian)
i64 publish_time (little-endian)Use near_sdk::json_types::I64 for price in both formats. Under borsh it is interchangeable with a plain i64 — it is a newtype, so the same eight little-endian bytes are written either way. Under JSON it is not optional: price is a decimal string, and a bare i64 field fails to deserialize with invalid type: string. Declaring it as I64 covers both and needs no manual parse.
Running it in production
- Pin the public key from step 1 and verify its attestation once. Treat a changed key as a failure, not as something to auto-accept.
- Poll on your own cadence. Prices are refreshed continuously, so a poll normally returns the cached value. Ask for the window you actually need —
max_age_secs: 40means "built only from venues seen in the last 40 seconds". Narrowing it below our refresh cadence is allowed and simply makes the call fetch fresh, which costs a few seconds of latency; widening it lets the slower venues (Pyth and Chainlink, which run on their own wider cycle) contribute as well.
There is a practical floor: a fresh fetch takes several seconds, and the prices it produces are already that old by the time the request is answered, so a very tight window fails whenever the cache cannot serve it. Do not guess where that floor is — the answer tells you.publish_timeis the oldest source that contributed, so measure a few responses and set your window from what you observe. The assets under the heaviest use are refreshed on the fastest cycle. - Verify first, parse second — over the raw payload bytes.
- Enforce freshness yourself. Check
publish_timeagainst your own bound; do not rely only on our server-side check. - Reject non-increasing timestamps if you write prices to a contract — that is what stops an old signed payload from being replayed. Note repeated polls within one refresh window legitimately return the same
publish_time; treat that as "no new data", not as an error. - Fail closed. A failed request or signature means no price — never fall back to an unsigned or stale value.
Access. Calls need a payment key authorised for this project. If you already have an OutLayer payment key, ask for this project to be allowed on it; otherwise we can issue one. The request is compute-only — it never touches the chain and costs no gas.
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, so there is no refresh call to make. That state is written on its own slow, gas-paying cycle — read the timestamp it returns and enforce your own staleness bound, exactly as you would against any on-chain feed.
View Methods (free — check the timestamp)
| 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 Example) — 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 Example)
"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
Free reads, but verify the age
price-oracle.near is written to by TEE workers on a slow, gas-paying cycle, so get_price_data costs you nothing but carries no promise of freshness. Compare the timestamp it returns against your own bound and fail closed if it is too old.
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)
On-chain state is written on a slow cycle, so get_price_data is free but may be stale — check its timestamp. Call request_price_data when you need a price fetched for that specific call.
| Method | Arguments | Description |
|---|---|---|
get_price_data | asset_ids?: string[] | Get cached prices. Always returns a PriceData object; per-asset price is null when stale/unavailable |
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', 'eth.bridge.near'],
});
// 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