---
name: phoebe-integrate-consumer
description: Write a Tolk consumer contract that pulls prices from Phoebe — RequestPrice outbound + FulfillPrice receiver. Use when the user says "integrate Phoebe", "add a price feed to my contract", "consume prices from Phoebe", or "write a Phoebe consumer".
---

# Integrate a Phoebe price-oracle consumer

You're adding on-chain price reads to a Tolk contract. Phoebe's consumer surface is tiny: one outbound (`RequestPrice` 0x71) + one inbound (`FulfillPrice` 0x72). Scaffold via `npx phoebe scaffold consumer` then customize.

## The wire shapes (frozen, byte-identical)

```tolk
// Outbound to Phoebe
struct (0x00000071) RequestPrice {
    queryId:        uint64                  // consumer-supplied; correlate the FulfillPrice callback
    feedId:         uint16                  // 0..255
    proofRef:       cell                    // pruned-branch merkle_proof (~417 B) — see PhoebeMerkleTree.proof()
    leaf:           PriceLeaf               // claimed PriceLeaf at slot `feedId`
    maxStaleness:   uint32                  // 0 = accept any active root; non-zero = max age in seconds
    freshUpdateRef: Cell<FreshUpdate>?      // null = cached fast-path; set = Pyth-style update + read
}

// Optional inline update (mode B — sub-heartbeat freshness)
struct FreshUpdate {
    signedDataRef: Cell<SignedSnapshot>     // 68B: phoebeHash + timestamp + root
    aggSigRef:     Cell<AggSig>             // 96B: BLS-G2 aggregate
}

// Inbound from Phoebe
struct (0x00000072) FulfillPrice {
    queryId: uint64         // echoes your request
    leaf:    PriceLeaf      // verified PriceLeaf — trust mantissa/expo/conf/pubTime/source
}

struct PriceLeaf {
    feedId:    uint16
    mantissa:  int128       // signed; price = mantissa × 10^expo
    expo:      int8         // signed exponent
    confBps:   uint16       // confidence interval, basis points of price
    pubTime:   uint32       // unix seconds when off-chain aggregator produced this
}
```

## Scaffold + customize

```bash
npx phoebe scaffold consumer --out contracts/my-consumer.tolk
```

Drops a minimal template with the receiver-safety banner, admin-triggered request path, and the essential storage fields. Customize:

1. **Business logic** inside `handleFulfillPrice` — replace the placeholder "store last leaf" with your price-consuming code (settle, liquidate, fill, mark, etc.).
2. **Trigger path** — if your contract triggers pulls from an internal event (a swap, a settlement, a liquidation check), inline the outbound `RequestPrice` directly; delete the owner-triggered `TriggerPull` path.
3. **Storage** — add fields for pending-request tracking if multiple pulls can be in flight (e.g. `map<queryId, RequestState>`).

## Sending a pull

**Compute the attached value correctly** — the #1 cause of `E_INSUFFICIENT_VALUE` (240):

```tolk
val outboundValue = phoebeConfig.pullFee + ton("0.03") + ton("0.05");
//                  pullFee + MIN_GAS_FOR_PULL + forward-fee slack
```

Or hardcode generously during bring-up: `ton("0.1")` easily covers the default tunables.

In TypeScript via the SDK:

```typescript
import { estimatePullValue, PHOEBE_DEFAULTS } from '@titon-network/phoebe-sdk';
const value = estimatePullValue({ pullFee: PHOEBE_DEFAULTS.pullFee });    // 0.09 TON
```

For **live contracts** always read the actual `pullFee` first — tunables drift via owner `UpdateConfig`:

```typescript
const cfg = await phoebe.getConfig();
const value = estimatePullValue({ pullFee: cfg.pullFee });
```

## Optional: Pyth-style update + read (mode B)

When a 30-second-stale price could break your protocol (lending liquidations, perps, options settlement), attach a fresh signed snapshot inline. Phoebe BLS-verifies it, advances its cache, and walks your proof against the fresh root. Permissionless caller — the BLS aggregate under Atlas's `groupPk` is the auth.

```typescript
import { computeSnapshotHash, estimatePullValue } from '@titon-network/phoebe-sdk';

// 1. Query the operator's signed-data feed for a fresher snapshot.
const fresh = await operatorClient.getSignedSnapshot();   // { timestamp, root, sig, leaves }

// 2. Defensive: query getSnapshot() to make sure fresh is actually fresher.
//    (Avoids E_FRESH_NOT_FRESHER (187) — the consumer eats the gas if not.)
const { lastSnapshotTime } = await phoebe.getSnapshot();
if (fresh.timestamp <= lastSnapshotTime) {
    return await pullCached();    // fall through to mode A
}

// 3. Build the proof against the FRESH root (not the cached one).
const freshTree = PhoebeMerkleTree.fromSparseLeaves(fresh.leaves);
const signedData = computeSnapshotHash(phoebe.address, fresh.timestamp, fresh.root);

await phoebe.sendRequestPrice(consumerSender, {
    value:        estimatePullValue({ pullFee: cfg.pullFee, withFresh: true }),  // +0.05 TON for BLS
    queryId:      nextQueryId(),
    feedId:       42,
    proof:        freshTree.proof(42),
    leaf:         fresh.leaves.get(42)!,
    maxStaleness: 0,                              // or a tight bound the cache would fail
    freshUpdate:  { signedData, sig: fresh.sig },
});
```

**Cost trade-off:** mode B adds ~60k gas (BLS_VERIFY) on top of mode A's ~13k. Use it only when you need the freshness; routine reads should stick to mode A. **Positive externality:** when mode B succeeds, the cache advances atomically — subsequent same-block pulls read your advanced cache for free.

## Building the proof off-chain

The consumer (or its indexer) needs to know the latest signed leaves the operator pushed AND build a Merkle proof for the requested feedId.

```typescript
import { PhoebeMerkleTree } from '@titon-network/phoebe-sdk';

// Sparse map of feedId → leaf — empty slots get the canonical placeholder.
const tree = PhoebeMerkleTree.fromSparseLeaves(new Map([[42, myLeaf]]));
const proof = tree.proof(42);    // proof.hash() == tree.rootAsBigint() (verified by Phoebe)
```

In production, an off-chain indexer watches `EvtSnapshotPushed` events + caches the original leaves the operator signed; the consumer fetches them from the indexer at pull time.

## The callback safety checklist

In `handleFulfillPrice`:

1. **Sender pin** — `assert (sender == storage.phoebe)`. A missing pin = anyone can deliver fake prices to your contract by sending a 0x72 body.
2. **QueryId correlation** — echo'd from your request; look up the pending work by it.
3. **Cheap body** — Phoebe forwards whatever you attached minus its own gas + pullFee. Keep the callback cheap; heavy operations should emit an event and be processed in a separate message.
4. **Revert-safe** — callbacks arrive with `bounce: false`. A revert here does NOT bounce back to Phoebe; the request state has already been finalized (pullFee retained). Defensive coding: minimize asserts, avoid external calls.

## Reading the verified price

`leaf.mantissa × 10^leaf.expo` is the price. `leaf.confBps` is the uncertainty in basis points of price.

```tolk
// Settle a position at the verified price:
val priceMantissa: int128 = msg.leaf.mantissa;
val priceExpo:     int8   = msg.leaf.expo;
val confBps:       uint16 = msg.leaf.confBps;     // e.g. 50 = ±0.5%

// Reject pulls with too much uncertainty (your policy):
assert (confBps <= 100) throw E_PRICE_TOO_UNCERTAIN;

// Reject stale prices (your policy — the contract already enforced
// `maxStaleness` if you set it on the request, but you can re-check):
val nowTs: uint32 = blockchain.now() as uint32;
assert (nowTs - msg.leaf.pubTime <= 600) throw E_PRICE_STALE;

// Settle: payoutMantissa = positionAmount × leaf.mantissa
// (mind the expo when scaling — leaf.mantissa × 10^leaf.expo is the price.)
```

For off-chain byte-identity verification of the leaf bytes the consumer received: `priceLeafFromSlice(body)` from the SDK returns the same `PriceLeaf` shape.

## Testing

Use the SDK's sandbox fixture — deploys Phoebe + real Atlas + mock-ForgeTON in one line:

```typescript
import { Blockchain } from '@ton/sandbox';
import { deployPhoebeFixture } from '@titon-network/phoebe-sdk/testing';

const blockchain = await Blockchain.create();
const fx = await deployPhoebeFixture(blockchain);

const sampleLeaf = { feedId: 42, mantissa: 100n, expo: 0, confBps: 50,
                     pubTime: 2_000_000_000 };
const { tree } = await fx.pushSnapshot({ leaves: new Map([[42, sampleLeaf]]) });

// Now your consumer can call `requestPrice` against fx.phoebe.address.
// The fixture's `requestPrice({ tree, feedId, leaf })` helper covers the
// flow from the consumer's perspective.
```

For a complete real-consumer round-trip example, see [`examples/price-aware-vault/`](../examples/price-aware-vault/).

## Watch out for

- **Don't reuse a Merkle proof across snapshots.** Every `lastRoot` rotation invalidates all prior proofs. Build a fresh proof per pull.
- **Don't pass a leaf that doesn't match the leaf in the tree.** `E_BAD_MERKLE_PROOF` (183) — the contract asserts `walkMerkleProof(proof, feedId).hash() == leaf.toCell().hash()`.
- **Don't expect a callback if the request reverted.** If `RequestPrice` reverts (insufficient value, bad proof, paused), no FulfillPrice fires. Use `summarizeTx(tx)` from the SDK to check the receiving tx's exit code.
- **Don't hand-decode the FulfillPrice body.** Use `parseFulfillPrice(body)` from the SDK — the leaf field widths aren't obvious (mantissa is `int128`, etc.).
- **Don't trust an unauthenticated 0x72 body.** Always `assert (sender == storage.phoebe)` first.

## Cross-references

- [`templates/consumer.tolk`](../templates/consumer.tolk) — drop-in starter
- [`examples/price-aware-vault/`](../examples/price-aware-vault) — complete example with vault payouts
- [`AGENTS.md`](../AGENTS.md) — full SDK surface
- [`ERRORS.md`](../ERRORS.md) — every E_* with a hint
