# Scallop Xoracle Integration

> Scallop multi-source oracle handling via xOracle (Pyth + Supra + Switchboard). Use when user says "xOracle", "x_oracle", "multi-source oracle", "Supra", "Switchboard", "oracle aggregation", "price rule", "secondary oracle", "price update policy", "oracle resolver", or asks how Scallop validates prices from multiple sources before settling on-chain.

- Skill: `scallop-io/scallop-xoracle-integration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add scallop-io/scallop-xoracle-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/scallop-io/scallop-xoracle-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: scallop-io (https://skillmd.com/u/scallop-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/scallop-io/scallop-xoracle-integration

---


# xOracle: Multi-Source Oracle Integration

Aggregate and validate prices from Pyth and Switchboard through Scallop's xOracle layer.

> **Version note — read this first.** There are two generations of these contracts. The standalone [sui-x-oracle repo](../../../sui-x-oracle/) is **stale**: in it, *both* the Pyth and Switchboard rules write `set_secondary_price`, so its signatures cannot satisfy a primary policy. The **production** sources live in [sui-lending-protocol/contracts/sui_x_oracle/](../../../sui-lending-protocol/contracts/sui_x_oracle/) (packages: `x_oracle`, `pyth_rule`, `switchboard_rule`, `switchboard_on_demand_rule`, `supra_rule`, plus custom LST rules). Everything below is built from the production sources and cross-checked against the moveCalls that `@scallop-io/sui-scallop-sdk` **v4.3.0** actually emits ([src/txBuilders/oracles/index.ts](../../../sui-scallop-sdk/src/txBuilders/oracles/index.ts)) — treat the SDK builder as ground truth for the deployed packages.

## Overview

`sui-x-oracle` is the on-chain resolver every price-sensitive Scallop op flows through. It:

- Holds the **primary** and **secondary** rules registered for each coin.
- Enforces a **price-update policy** — only price feeds produced by an authorized rule module can update a `PriceFeed`.
- Confirms a price update only after every required rule has written, so a single compromised feed cannot drive a borrow/liquidate.

The vast majority of assets resolve via Pyth alone — the basic flow is already covered in the [oracle skill](../scallop-oracle/SKILL.md). This skill covers the cases where you need to see, configure or compose with the multi-source layer (Switchboard alongside Pyth).

Production contract layout: [sui-lending-protocol/contracts/sui_x_oracle/](../../../sui-lending-protocol/contracts/sui_x_oracle/)

```
sui_x_oracle/
├── x_oracle/                   core: price_feed, x_oracle, price_update_policy
├── pyth_rule/                  Pyth adapter + registry (primary for most assets)
├── switchboard_on_demand_rule/ Switchboard On-Demand adapter + registry
├── switchboard_rule/           legacy Switchboard v2 adapter
├── supra_rule/                 Supra adapter (deployed; check getAssetOracles per asset)
└── custom_afsui_rule/ ...      LST-specific rules
```

## On-Chain Flow

```
updateAssetPricesQuick(coin)                       # TS SDK helper
  └─ request = x_oracle::price_update_request<Coin>(x_oracle)
       ├─ pyth_rule::rule::set_price_as_primary<Coin>(&mut request, ...)      # per policy
       └─ switchboard rule set_as_secondary_price<Coin>(&mut request, ...)    # if configured
  └─ x_oracle::confirm_price_update_request<Coin>(x_oracle, request, clock)
       (consumes the request; reverts if a required rule didn't write)
```

`price_update_request<T>` returns a hot-potato `XOraclePriceUpdateRequest<T>` — it has no abilities, so every rule call must receive it by `&mut` reference and `confirm_price_update_request<T>` must consume it in the same PTB. The `confirm_*` step is what binds the multi-source guarantee: Scallop only accepts the price if every required rule produced one.

> A `supra_rule` package exists in the production contracts and the SDK can still emit calls for it, but in practice assets resolve with Pyth (primary) and, where configured, Switchboard (secondary). Don't assume a coin's rule set — check it per asset with `getAssetOracles()`.

## Inspect the Resolver

> **SDK note**: `@scallop-io/sui-scallop-sdk` v4.3.0 exports the package root plus the `./client`, `./query`, `./builder`, `./types`, `./errors`, and `./logger` subpaths. Internal deep paths are not exported — reach the resolver helpers through `scallop.client.query.*`.

```typescript
const scallopSDK = new Scallop({ networkType: 'mainnet' });
await scallopSDK.init();
const query = scallopSDK.client.query;
```

### Per-Coin Oracle Rules

```typescript
// Returns Record<coinName, { primary: SupportOracleType[], secondary: SupportOracleType[] }>
const oracles = await query.getAssetOracles();
// oracles.sui  => { primary: ['pyth'], secondary: ['switchboard'] }
// oracles.usdc => { primary: ['pyth'], secondary: [] }
```

The wrapper delegates to the xOracle repository ([src/repositories/xOracle/](../../../sui-scallop-sdk/src/repositories/xOracle/)), which reads the primary and secondary price-update-policy tables and merges the result. For normal app code, the `query.getAssetOracles()` wrapper is the right entry point.

### Price-Update Policy

```typescript
// Returns { primary, secondary } — two SuiObjectResponse handles for the policy tables
const policies = await query.getPriceUpdatePolicies();
```

### Switchboard On-Demand Aggregators

```typescript
// Takes coin names, returns aggregator object IDs in the same order
const aggIds = await query.getSwitchboardOnDemandAggregatorObjectIds(['sui', 'usdc']);
// aggIds[0] -> SUI/USD aggregator object ID
// aggIds[1] -> USDC/USD aggregator object ID
```

Source: `getOnDemandAggObjectIds` in [src/repositories/xOracle/](../../../sui-scallop-sdk/src/repositories/xOracle/), exposed via [src/models/scallopQuery/index.ts](../../../sui-scallop-sdk/src/models/scallopQuery/index.ts).

## Composing a Multi-Source PTB Manually

`updateAssetPricesQuick` handles the whole flow: SDK v4.3.0 reads each coin's `{ primary, secondary }` rule set from the resolver and emits a rule call for every configured source — Pyth, Switchboard, and (if a policy required it) Supra — before confirming. This is implemented in `updatePrice` in [src/txBuilders/oracles/index.ts](../../../sui-scallop-sdk/src/txBuilders/oracles/index.ts), which iterates the rules per type and dispatches `updatePythPrice` / `updateSwitchboardPrice` / `updateSupraPrice` accordingly. Drop down to raw `moveCall`s only when:

- You're integrating Scallop oracle confirmation from outside the SDK.
- You're debugging why a `confirm_price_update_request` is reverting.

The production Move signatures (from [sui-lending-protocol/contracts/sui_x_oracle/](../../../sui-lending-protocol/contracts/sui_x_oracle/)):

```
x_oracle::price_update_request<T>(self: &XOracle): XOraclePriceUpdateRequest<T>

pyth_rule::rule::set_price_as_primary<CoinType>(     // set_price_as_secondary also exists
  request: &mut XOraclePriceUpdateRequest<CoinType>,
  pyth_state: &PythState,
  pyth_price_info_object: &PriceInfoObject,
  pyth_registry: &PythRegistry,
  clock: &Clock,
)

// Switchboard On-Demand — SDK v4.3.0 emits `set_as_primary_price` / `set_as_secondary_price`
// against the deployed package (the repo snapshot shows the same params under a
// `set_price_as_*` naming; trust the SDK's target for mainnet):
<switchboard_on_demand_rule>::rule::set_as_secondary_price<CoinType>(
  request: &mut XOraclePriceUpdateRequest<CoinType>,
  aggregator: &Aggregator,
  switchboard_registry: &SwitchboardRegistry,
  clock: &Clock,
)

x_oracle::confirm_price_update_request<T>(
  self: &mut XOracle,
  request: XOraclePriceUpdateRequest<T>,
  clock: &Clock,
)
```

Note the Pyth rule takes **no VAA bytes and no fee coin** — the Pyth `PriceInfoObject` must already be fresh. The SDK refreshes it first via the Pyth client (`updatePythPriceFeeds`, using Hermes VAAs) as a separate step in the same PTB, and only then calls the rule.

The request is a hot potato — capture the return value of step 1 and thread it through every rule call and the final confirm, or the PTB won't compile:

```typescript
const tx = builder.createTxBlock();

// 0. Refresh the Pyth PriceInfoObject (Hermes VAA push) — the SDK does this
//    via updatePythPriceFeeds; without it the rule reads a stale price.

// 1. Request — returns the hot-potato XOraclePriceUpdateRequest<T>
const request = tx.moveCall({
  target: `${xOraclePkg}::x_oracle::price_update_request`,
  typeArguments: [coinType],
  arguments: [tx.object(xOracleId)],
});

// 2. Pyth rule (primary for this coin) — writes its price feed into the request
tx.moveCall({
  target: `${pythRulePkg}::rule::set_price_as_primary`,
  typeArguments: [coinType],
  arguments: [
    request,
    tx.object(pythStateId),
    tx.object(pythPriceInfoObjectId),
    tx.object(pythRegistryId),
    tx.object('0x6'), // clock
  ],
});

// 3. Switchboard secondary — only if registered for this coin
tx.moveCall({
  target: `${switchboardRulePkg}::rule::set_as_secondary_price`,
  typeArguments: [coinType],
  arguments: [
    request,
    tx.object(switchboardAggregatorId),
    tx.object(switchboardRegistryId),
    tx.object('0x6'), // clock
  ],
});

// 4. Confirm — consumes the request; reverts if a required rule didn't write
tx.moveCall({
  target: `${xOraclePkg}::x_oracle::confirm_price_update_request`,
  typeArguments: [coinType],
  arguments: [tx.object(xOracleId), request, tx.object('0x6')],
});

// 5. Continue with borrow / liquidate / withdraw_collateral
```

Package IDs and rule/registry/aggregator object IDs come from `ScallopAddress` (`core.packages.xOracle.id`, `core.packages.pyth.id`, `core.oracles.pyth.*`, `core.packages.switchboard.id`, `core.oracles.switchboard.registry`, `core.coins.<coin>.oracle.*`) — mirror the exact IDs the SDK builder uses rather than hard-coding.

## Why Two Sources?

Scallop's xOracle does not average prices — it accepts the primary and uses the secondary as a **sanity bound**. The price is rejected when the two diverge beyond a configured threshold. This is the security property you're paying for when an asset is configured with a secondary feed: a single bad VAA or aggregator value cannot move collateral risk.

For risk-sensitive coins (highly volatile, newly listed, low Pyth confidence), expect two rules registered. For majors (SUI, USDC, ETH, BTC), Pyth alone is typically sufficient.

## Diagnostics: `confirm_price_update_request` Revert

Most common causes, in order:

1. **Secondary rule missing** — the resolver expects a Switchboard write that you didn't include. Inspect via `getAssetOracles`.
2. **Stale aggregator** — Switchboard aggregator hasn't been updated within the protocol's freshness bound. Trigger an on-demand update first.
3. **Price deviation** — primary and secondary diverged past the threshold. Wait for the next Hermes VAA / aggregator round, or temporarily route around the affected coin.

## References

- [oracle Skill](../scallop-oracle/SKILL.md) - Pyth-only flow (covers most assets)
- [oracle-integration.md](../../references/oracle-integration.md) - Protocol oracle reference
- [sui-x-oracle README](../../../sui-x-oracle/README.md) - On-chain resolver
- [advanced-transactions Skill](../scallop-advanced-transactions/SKILL.md) - Raw moveCall patterns

