# Pyth oracle (/developers/onchain/oracle)



`@cardano-swap/oracle` subscribes to Pyth Lazer, verifies every signed update against the
trusted signers held in the on-chain Pyth state, checks the price is fresh, compares a pool
price against it, and builds the redeemer for the zero lovelace withdrawal that proves the
price on chain.

Nothing in this package invents, interpolates or defaults a price. An update that cannot be
parsed and verified is rejected, because a wrong price silently accepted is a wrong quote paid
by a user.

## The deployment [#the-deployment]

Pyth publishes the withdraw script itself. Zerochord references it and reads the verified
price out of the withdrawal; it does not reimplement the signature check.

|                       | Cardano mainnet                                              | Cardano preprod                                                   |
| --------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------- |
| Withdraw script hash  | `4949403eece2eea554df601a883c50d0ef005fa66e7a7300bad5ebc2`   | `b13fcd25149dd1caacd53525f4e650aefa8dcd3ead7a8cef6c6b7ae7`        |
| State address         | `addr1w9kskr8xl3pndssgxa72sk5e5lsc455r5wcervgfhmae4vgpyhzl4` | `addr_test1wpz2mpc7etnqx9n37g0qrds7ays4ta5dpgaltfn97a8exps0v759t` |
| Trusted signer expiry | 2027-05-04                                                   | 2036-07-29                                                        |

Both networks configure feed id **16, ADA/USD**, a maximum price age of 120 000 ms and a
deviation threshold of 200 basis points.

## The signed update format [#the-signed-update-format]

The `solana` format is little endian throughout, and the layout was read from the Pyth
`pyth-lazer-protocol` Rust crate that produces the bytes, then confirmed against the signed
test vector that ships in the Pyth Cardano validator source.

```text
Envelope
  offset  0   u32   format magic, 0x821A01B9
  offset  4   [64]  Ed25519 signature
  offset 68   [32]  Ed25519 public key of the signer
  offset 100  u16   payload length in bytes
  offset 102  [n]   payload

Payload
  offset  0   u32   payload magic, 0x93C7D375
  offset  4   u64   publish timestamp in MICROseconds
  offset 12   u8    channel id
  offset 13   u8    number of feeds
  then per feed: u32 feed id, u8 property count, then id and value per property
```

The Ed25519 signature covers the **payload bytes only**, not the envelope. That was checked
against the Pyth test vector: verifying over the payload passes and verifying over the envelope
tail fails.

For the integer properties, the value `0` means absent rather than zero.

## Trusted signers come from the chain [#trusted-signers-come-from-the-chain]

The trusted Ed25519 keys are not configuration and are not hard coded. Pyth rotates them, and
the on-chain Pyth state is what the validator itself checks against, so it is the only correct
source.

`packages/oracle/src/state.ts` reads that same state UTxO, identified by the state NFT whose
asset name is `Pyth State`, and decodes the `trusted_signers` map with each key's validity
range. The off-chain check and the on-chain check therefore agree by construction.

## Freshness is the consumer's job [#freshness-is-the-consumers-job]

The Pyth Cardano validator checks the Ed25519 signature and that the signer is trusted, and
nothing else. &#x2A;*It does not enforce freshness.** A correctly signed update from last week
verifies exactly as well as one from this second.

So whatever consumes the price checks the publish time itself:

```ts
checkFreshness(price, nowMs, { maxPriceAgeMs, maxFutureSkewMs })
requireFresh(price, nowMs, options)
```

`checkFreshness` returns `{ fresh: true, ageMs }`, or a refusal carrying `reason` of `stale` or
`future` with the limit it broke.

The future skew allowance defaults to 2000 ms. Pyth's clock and this one are not the same
clock, and the router publishes every 200 ms, so a small positive skew is normal. A large one
means the local clock is wrong, and then the computed age is wrong too, so the price is refused
rather than trusted.

`now` is always a parameter. Reading the clock inside these functions would make the result
depend on when a test runs.

There is no variant that returns the last known good price. A quote built on a stale oracle is
exactly the failure the guard exists to stop.

## The deviation guard [#the-deviation-guard]

```ts
checkDeviation({ oracle, poolImplied, thresholdBps }): DeviationResult
```

All arithmetic is on `bigint`. A basis point difference on a stablecoin pair is the fourth
decimal place, which is where a float starts to lie. Both mantissas are scaled **up** to the
smaller exponent so nothing is truncated, and the division rounds up so a value between two
whole basis points is reported as the larger one. That makes the guard refuse rather than allow
at the boundary, which is the safe direction for a user's funds.

The result is one of three outcomes:

| Outcome        | Meaning                                                     |
| -------------- | ----------------------------------------------------------- |
| `within`       | The observed price is inside the threshold.                 |
| `exceeded`     | The quote must be refused.                                  |
| `no-reference` | No oracle feed covers this pair, so no comparison was made. |

`no-reference` is never folded into `within`. The caller has to know the guard did not run
rather than believe it passed.

### What each pool shape gets [#what-each-pool-shape-gets]

**A pool with ADA on one side**, for example USDM against ADA, implies an ADA/USD price if the
stablecoin is taken to be worth one dollar. `checkAdaUsdDeviation` compares that implied price
against Pyth ADA/USD.

A stale pool, a pool that has been pushed by a sandwich, and a stablecoin that has actually
depegged all appear the same way here, as a deviation. All three are reasons to refuse a quote,
so one guard covers them, and the guard cannot say which of the three happened.

**A pool with a stablecoin on both sides** has `checkStablePairDeviation`, which reports
`no-reference` and names both assets in its reason. The full Lazer symbol list of 3615 feeds was
searched: the feeds that look like a match are different assets on other chains. It exists so the
caller handles the case explicitly instead of skipping the guard by omission.

`packages/quote` does not depend on `@cardano-swap/oracle`, so the guard runs wherever a caller
wires it rather than implicitly inside the engine.

## Proving the price on chain [#proving-the-price-on-chain]

The Pyth validator declares its withdraw handler as `withdraw(updates: List<ByteArray>, credential, self)`,
so the redeemer is a Plutus list of raw signed update byte strings with no wrapping constructor.
The validator loops over the list and verifies each against the trusted signers in the state
reference input.

```ts
buildPythWithdrawRedeemer(updates): PlutusData
encodePythWithdrawRedeemer(updates): string
```

An empty list throws: it proves no price. Each update is parsed before it goes in, because a
malformed update would be rejected on chain only after the fee is paid and the script budget is
spent.

The transaction carrying that redeemer must also withdraw exactly 0 lovelace from the withdraw
script's reward account, carry the Pyth state UTxO as a reference input, and set a validity
range, because the trusted signer expiry is checked against it. Those parts belong to the builder
in `packages/tx/src/oracle/withdraw.ts`.

Zero withdrawal is the pattern that forces the withdrawal validator to run once per transaction.

## Two ways to obtain updates [#two-ways-to-obtain-updates]

**Subscribe directly.** Signed `solana` updates come from the Lazer router stream at
`wss://pyth-lazer-{0,1,2}.dourolabs.app/v1/stream`. Pyth publishes three redundant instances and
asks clients to spread across them.

The router stream requires an access token, sent as `Authorization: Bearer <token>` from a server
or in the websocket subprotocol list as `pyth-lazer-auth, <token>` from a browser, which cannot
set headers. Both forms are supported and the header form is the default. The variable holding
the token is named by `oracle.pyth.apiKeyEnv`, which is `PYTH_LAZER_API_KEY`.

Keys are free for Cardano projects under the Intersect and Pyth Pro offer.

Without a key the client throws. There is no substitute price source, no cached last value and no
public feed fallback. A router that quoted from a guessed price would be worse than a router that
refused to quote.

The api service stream at `wss://pyth-{0,1,2}.dourolabs.app/v1/prices/stream` serves parsed
unsigned JSON only, so it does not serve the payloads the Cardano redeemer needs.

**Relay from the chain.** Every zero lovelace withdrawal from the Pyth withdraw script carries the
signed update as its redeemer, and a transaction is public. `packages/oracle/src/relay.ts` reads
one back out of transaction CBOR, and needs no Pyth credential.

Authenticity comes from the Ed25519 signature over the payload, not from the transport. The same
bytes verify against the same trusted signer whichever way they arrived. The relay checks this on
every read rather than assuming it: the trusted signer set is read from the on-chain Pyth state of
the network the transaction will be submitted to, and an update signed by anything else is
rejected.

Two properties of a relayed update, both reported rather than smoothed over:

* **Age.** A relayed update is already 22 to 100 seconds old by the time it reaches a block and can
  be read back. Against a two minute window that leaves little room. The module reports the age and
  lets the caller refuse. It never relaxes the window to make an update usable.
* **Properties.** A relayed update carries only the properties the original consumer subscribed to,
  in practice price and exponent. A `PythPrice` needs a confidence value and the module does not
  invent one, so a relayed update supports the withdrawal redeemer. The deviation guard's reference
  price comes from a direct subscription.

## Related [#related]

<Cards>
  <Card title="The settlement validator" href="/developers/onchain/settlement-validator" />

  <Card title="Configuration reference" href="/developers/reference/configuration" />

  <Card title="Guarantees and security" href="/developers/security" />
</Cards>
