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
21
Supported Tokens
10
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", "eth.bridge.near"],
  "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
i

About 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.00

Governance & 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

ActionDescription
AddAsset / RemoveAssetManage tracked assets
SetAssetExchangeConfigConfigure exchange tickers, Pyth/Chainlink feeds per asset
RegisterPushSignerRegister TEE-derived account as trusted price pusher
ConfigureOutlayerSet OutLayer integration parameters
UpgradeContractContract upgrade via DAO vote (after upload_upgrade_code)

Self-Service for Projects

Third-party projects can operate their own push signers:

  1. Create a TEE secret (PROTECTED_KEY_*) in OutLayer dashboard
  2. DAO proposal to register the key for specific assets
  3. Fund the derived implicit account with NEAR
  4. 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.

SourceUpstream timestampStaleness check
pythYes (publish_time)Rejected if older than 120s
all others (coingecko, binance, chainlink, huobi, kucoin, gate, cryptocom, …)NoStamped 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 returns price: 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" }
  }'
FieldDefaultMeaning
tokensrequiredOnly 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_secs120Your 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_sourcesnoneDrop sources you already consume yourself, so our feed stays an independent input. Unknown names are rejected, never ignored
sig_formatjsonjson or borsh
expo-8Price is an integer scaled by 10expo
min_sources_num1Minimum 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 verified

Rust (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 byte

Verifying 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

  1. 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.
  2. 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: 40 means "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_time is 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.
  3. Verify first, parse second — over the raw payload bytes.
  4. Enforce freshness yourself. Check publish_time against your own bound; do not rely only on our server-side check.
  5. 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.
  6. 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)

MethodDescription
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.25

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

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)

i

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.

MethodArgumentsDescription
get_price_dataasset_ids?: string[]Get cached prices. Always returns a PriceData object; per-asset price is null when stale/unavailable
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.

Legacy Pyth Wrapper

i

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&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', '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

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