# phoebe-consume-price

> Read a verified TON-mainnet price from Phoebe in a dapp — no operator
> node required. Three lines of TypeScript.

This skill is for **dapp developers** who want a live price from
Phoebe. It uses `fetchVerifiedPrice` (added in
`@titon-network/phoebe-sdk@0.5.0`) to:

1. Fetch the latest snapshot from a phoebe operator over HTTP.
2. Reconstruct the merkle root locally and compare it against
   `phoebe.lastRoot` on-chain — no trust in the operator required.
3. Build a merkle proof for the requested `feedId`.
4. Return a `VerifiedPriceQuote` you can submit to your consumer
   contract.

Lying operators are caught by the local hash check; the helper falls
through to the next operator in the list.

## Install

```bash
pnpm add @titon-network/phoebe-sdk @ton/core @ton/ton
```

## The three-line read

```ts
import { TonClient } from '@ton/ton';
import { Phoebe, PHOEBE_MAINNET, assertDeployment, fetchVerifiedPrice } from '@titon-network/phoebe-sdk';

const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' });
const dep    = assertDeployment('mainnet');                    // PHOEBE_MAINNET
const phoebe = client.open(Phoebe.createFromAddress(dep.phoebe));

const quote = await fetchVerifiedPrice(phoebe, /*feedId*/ 0, dep.operators ?? []);
// quote.mantissa, quote.expo  → price = mantissa × 10^expo
// quote.proof, quote.leaf     → pass to your consumer contract
```

`dep.operators` is bundled in the SDK — kept in lockstep with the live
operator set. You can also supply your own list:

```ts
const quote = await fetchVerifiedPrice(phoebe, 0, [
    { address: 'UQBzQPo5...', url: 'http://18.153.225.178:9092' },
    { address: 'UQDddMqJ...', url: 'http://32.193.191.115:9092' },
]);
```

## Reading the price as a number (front-end UIs)

```ts
const price = Number(quote.mantissa) * Math.pow(10, quote.expo);
console.log(`TON/USD = $${price.toFixed(4)}`);
// → "TON/USD = $1.9550"
```

`mantissa` is `bigint` to preserve precision for large markets (BTC at
$100k × 10⁸ fits without rounding). Cast to `Number` only when you've
narrowed the range.

## Submitting `leaf + proof` to a consumer contract

The consumer contract is your dapp's on-chain code that wants the
price. It calls phoebe's `RequestPrice` (opcode `0x71`), passing the
leaf + proof; phoebe verifies the proof against `lastRoot` and
callbacks the consumer at `FulfillPrice` (opcode `0x72`).

```ts
import { sendRequestPrice } from '@titon-network/phoebe-sdk';
// Construct the RequestPrice body from the verified quote:
await sendRequestPrice(consumer, {
    feedId:    quote.feedId,
    leaf:      quote.leaf,
    proof:     quote.proof,
    // …consumer-specific fields…
});
```

See [`phoebe-integrate-consumer.md`](./phoebe-integrate-consumer.md)
for the consumer-contract side (Tolk handler shape +
`FulfillPrice` callback).

## Staleness handling

By default `fetchVerifiedPrice` returns whatever the operator has —
including snapshots that may be a few seconds old. Three options:

```ts
// 1. Inspect the age and decide in app code:
if (quote.ageSec > 60) throw new Error('phoebe price too stale');

// 2. Enforce client-side via maxAgeSec:
const quote = await fetchVerifiedPrice(phoebe, 0, dep.operators!, {
    maxAgeSec: 30,
});

// 3. Enforce on-chain in your consumer contract by checking
//    `leaf.pubTime` against `now()` before acting on the price.
//    (Recommended for production — trustless freshness.)
```

## Pyth-style "update + read" (mode B)

When a dapp wants the **freshest possible** price and is willing to
pay extra gas to advance the on-chain cache itself, phoebe also
supports mode B: the consumer pushes a fresh snapshot inside the same
tx as its read. This requires aggregating BLS partials off-chain
before submission. **Out of scope for `fetchVerifiedPrice` in v0.5.0
— use mode A (this helper) for now**, mode B helper is on the
roadmap.

## Multi-feed snapshots

A snapshot commits to many feeds in one merkle root. If you need
prices for several feeds in the same atomic read, the operator
returned in `sourceOperator` is the one to keep using — its snapshot
is what `lastRoot` matches:

```ts
const tonUsd = await fetchVerifiedPrice(phoebe, 0, dep.operators!);
// Reuse the same operator for sibling feeds (avoids re-verifying
// against a different snapshot if the round rolls between calls).
const btcUsd = await fetchVerifiedPrice(phoebe, 1, [
    dep.operators!.find((o) => o.address === tonUsd.sourceOperator)!,
]);
```

## Error model

`fetchVerifiedPrice` throws on:

| Reason | Recovery |
|---|---|
| Empty `operators` list | Pass `PHOEBE_MAINNET.operators` or your own. |
| `phoebe.lastRoot === 0n` (no snapshot ever) | Wait for first push (~30s after group key is published). |
| Every operator unreachable / 5xx | Transient — retry. |
| Every operator served a snapshot whose root ≠ on-chain | Operators are stale (between push windows) OR a malicious set served the same lie — retry in ~30s. |
| Snapshot has no leaf for `feedId` | That feed isn't published. Confirm the feedId is in the canonical registry. |
| `ageSec > maxAgeSec` | Snapshot is older than your bound — retry or relax `maxAgeSec`. |

## Production checklist

- [ ] Pin `@titon-network/phoebe-sdk` to a specific minor (e.g. `^0.5.0`).
- [ ] Use `PHOEBE_MAINNET.operators` — the SDK bumps this list as new
      operators come online.
- [ ] Don't trust `quote.ageSec` for security; enforce `leaf.pubTime`
      staleness ON-CHAIN in your consumer contract.
- [ ] Catch `fetchVerifiedPrice` errors and retry with backoff (10s
      between push windows is a good default).
- [ ] If your TPS requires it, cache the verified quote for the
      current push window in your dapp — don't re-fetch on every read.

## See also

- [`phoebe-integrate-consumer.md`](./phoebe-integrate-consumer.md) — building the consumer contract side
- [`../GUARANTEES.md`](../GUARANTEES.md) — what phoebe guarantees on freshness, monotonicity, and group-key rotation
- [`../RECIPES.md`](../RECIPES.md) — task-organised cookbook (14 paste-and-run snippets)
