# @titon-network/phoebe-sdk

TypeScript SDK for **Phoebe** — TON's price oracle. One BLS_VERIFY + one cell-walk per pull, for any feed in a 256-feed BLS-aggregated Merkle snapshot. Verifier consumer of [Atlas](https://github.com/titon-network/atlas) (group key) + [ForgeTON](https://github.com/titon-network/forgeton) (operator stake). Built on the TSA-audited [`@titon-network/atlas-sdk`](https://www.npmjs.com/package/@titon-network/atlas-sdk) + [`@titon-network/forgeton-sdk`](https://www.npmjs.com/package/@titon-network/forgeton-sdk).

[![npm version](https://img.shields.io/npm/v/@titon-network/phoebe-sdk.svg)](https://www.npmjs.com/package/@titon-network/phoebe-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

## Audit status

> 🛡️ **TSA-audited — zero findings.** Full report: [`AUDIT.md`](https://github.com/titon-network/phoebe/blob/main/AUDIT.md).

| Layer | Result |
|---|---|
| TSA symbolic execution (8 checks: 3 standard + 3 single-contract custom + 2 inter-contract custom vs Atlas + ForgeTON) | **0 findings** — every check returned empty SARIF |
| Manual review against `references/vulnerabilities.md` | **52 / 52** confirmed defenses; no critical / high / medium / low / informational issues |
| Contract test suite (`pnpm run test`) | **316 / 316** passing |
| SDK test suite (`pnpm run test:sdk`) | **172 / 172** passing |
| Cross-contract ABI byte-shape pin | `tests/Integration.spec.ts` + `tests/SchemaEvolution.spec.ts` green against real Atlas + ForgeTON SDK fixtures |
| Pool-side foundation | Built on the TSA-audited [`@titon-network/forgeton-sdk`](https://www.npmjs.com/package/@titon-network/forgeton-sdk) and [`@titon-network/atlas-sdk`](https://www.npmjs.com/package/@titon-network/atlas-sdk) — Phoebe inherits both zero-findings postures and adds no new economic-security layer. |

Live deployments: **testnet (2026-05-13)** and **mainnet (2026-05-18, multi-op n=2)**. The SDK's `PHOEBE_TESTNET` + `PHOEBE_MAINNET` constants track the canonical addresses.

```bash
npm install @titon-network/phoebe-sdk @ton/core @ton/ton
# optional — pull these in only if you're sandbox-testing:
npm install --save-dev @ton/sandbox @titon-network/atlas-sdk @titon-network/forgeton-sdk
```

## Connect — testnet vs mainnet (the only difference)

The SDK is network-agnostic. You pick the network in **two lines**: the
`TonClient` endpoint and which deployment constant you import.

```ts
// ── testnet ──────────────────────────────────────────────────────────
import { TonClient } from '@ton/ton';
import { Phoebe, PHOEBE_TESTNET } from '@titon-network/phoebe-sdk';

const tonClient = new TonClient({
    endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
});
const phoebe = tonClient.open(Phoebe.createFromAddress(PHOEBE_TESTNET.phoebe));

const cfg = await phoebe.getConfig();
console.log(`connected to testnet — pullFee ${cfg.pullFee} nano-TON`);
```

For mainnet, change exactly two lines:

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

const tonClient = new TonClient({
    endpoint: 'https://toncenter.com/api/v2/jsonRPC',                  // ← no `testnet.`
});
const phoebe = tonClient.open(
    Phoebe.createFromAddress(PHOEBE_MAINNET.phoebe),                   // ← was PHOEBE_TESTNET
);
```

That's it. The same npm package, same bytecode, same code paths work on
either network — only the `TonClient` endpoint and the deployment constant
change. Pointing at a custom deployment (your own fork, sandbox, a private
chain) is just `Phoebe.createFromAddress(Address.parse('EQ...'))` — no
constant required.

> 💡 **Drift check.** Call `assertDeployment('mainnet')` (or `'testnet'`)
> instead of dereferencing `PHOEBE_MAINNET` / `PHOEBE_TESTNET` directly —
> the helper throws an actionable error if the constant is `null` and
> exposes `expectedCodeHash` + `expectedSchema` so you can diff against
> the live contract's `codeHash` and `getSchemaVersions()`.

## 30-second integration — pull a price

Phoebe is live. This block reads feed `42` directly from the deployed
mainnet contract — copy, paste, run.

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

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

// 1. Receive the signed snapshot leaf from your operator / indexer.
const myLeaf = {
    feedId:   42,
    mantissa: 6_543_210n * 100_000_000n,    // $65,432.10
    expo:     -8,
    confBps:  50,
    pubTime:  Math.floor(Date.now() / 1000),
};

// 2. Build the Merkle proof off-chain (~417 B pruned-branch exotic cell).
const tree  = PhoebeMerkleTree.fromSparseLeaves(new Map([[42, myLeaf]]));
const proof = tree.proof(42);

// 3. RequestPrice — mode A (cached fast-path, ~16k gas).
await phoebe.sendRequestPrice(yourConsumerSender, {
    value:        estimatePullValue({ pullFee: PHOEBE_DEFAULTS.pullFee }),
    queryId:      1n,
    feedId:       42,
    proof,
    leaf:         myLeaf,
    maxStaleness: 0,                    // accept any cached snapshot
});
```

Phoebe verifies `proof.hash() == lastRoot`, walks the proof to slot 42,
asserts the walked-leaf hash matches `myLeaf.toCell().hash()`, then sends
`FulfillPrice { queryId, leaf }` to your consumer at opcode `0x72`.

**For the target consumer contract pattern** (the receiver-safety banner
+ wire-shape discipline every Phoebe consumer must follow), see
[`skills/phoebe-integrate-consumer.md`](./skills/phoebe-integrate-consumer.md)
or run `npx phoebe scaffold consumer --out contracts/my-consumer.tolk` to
drop the canonical template into your project.

---

## What this gives you

- **`Phoebe` contract wrapper** — every `send*` + every `get*` mirroring the on-chain ABI
- **`PhoebeMerkleTree`** — off-chain proof generator, byte-identical to the on-chain walker
- **BLS primitives** (`signMessage` / `aggregateSignatures` / `computeSnapshotHash`) — operator-side signing helpers, with `BLS_DST_G2_POP` baked in (avoids the [#1 cause of `E_INVALID_BLS_SIGNATURE`](./ERRORS.md))
- **Event decoder** (`decodeEvent` / `decodeEvents`) — typed `PhoebeEvent` discriminated union, switch on `kind`
- **Error explainer** (`explainError` / `PhoebeError`) — every exit code → `{ origin, name, message, hint }`
- **Bundled artifact** (`loadPhoebeCode`) — no separate compile step
- **Sandbox testing** (`@titon-network/phoebe-sdk/testing` subpath) — `deployPhoebeFixture()` stands up the full real-stack (Atlas + Phoebe + mock-ForgeTON + bootstrapped operator) in one call
- **CLI** (`npx phoebe`) — `explain`, `decode`, `hash`, `schema`, `estimate pull/push`, `scaffold consumer`, `deployments`
- **Five persona-grouped agent skills** under [`skills/`](./skills/) — load one per task

---

## Quickstart — consumer

You're a TON dapp. You want to read a price.

### 1. Scaffold a consumer contract

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

Customize `handleFulfillPrice` for your business logic. The template includes the canonical receiver-safety banner + wire-shape discipline.

### 2. Build a Merkle proof off-chain

You receive a signed snapshot from an operator (or your indexer). To pull feed `42`:

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

const myLeaf = {
    feedId:   42,
    mantissa: 6_543_210n * 100_000_000n,    // $65,432.10
    expo:     -8,
    confBps:  50,
    pubTime:  Math.floor(Date.now() / 1000),
};

// Build a tree containing your slots; the root must match what the
// operator signed.
const tree = PhoebeMerkleTree.fromSparseLeaves(new Map([[42, myLeaf]]));
// `tree.proof()` returns a pruned-branch merkle_proof exotic cell (~417 B
// per pull). Use `tree.fullTreeProof(feedId)` for the legacy ~8.5 KB
// full-tree shape if your tooling can't construct exotic cells.
const proof = tree.proof(42);
```

### 3. Send `RequestPrice`

```typescript
import { Phoebe, newPhoebe, assertDeployment } from '@titon-network/phoebe-sdk';

// PHOEBE_TESTNET / PHOEBE_MAINNET resolve to canonical addresses where
// available; for sandbox testing or pre-mainnet use your own deployed
// Phoebe address.
const phoebe = tonClient.open(Phoebe.createFromAddress(myPhoebeAddr));

await phoebe.sendRequestPrice(myConsumerSender, {
    value:        estimatePullValue({ pullFee: PHOEBE_DEFAULTS.pullFee }),
    queryId:      1n,
    feedId:       42,
    proof,
    leaf:         myLeaf,
    maxStaleness: 0,    // accept any cached snapshot
});
```

Phoebe verifies `proof.hash() == lastRoot`, walks the proof to slot 42, asserts the walked-leaf hash matches `myLeaf.toCell().hash()`, then sends `FulfillPrice { queryId, leaf }` to your consumer at opcode `0x72`.

### 4. Decode the callback in your consumer

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

// In your contract's receiver (or your wrapper's getter for the latest callback):
const { queryId, leaf } = parseFulfillPrice(rawCallbackBody);
console.log(`feed ${leaf.feedId}: ${leaf.mantissa}e${leaf.expo} (conf ±${leaf.confBps}bps)`);
```

---

## Quickstart — sandbox tests

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

const blockchain = await Blockchain.create();
const fx = await deployPhoebeFixture(blockchain);
//   ↳ Deploys real Atlas + mock-ForgeTON + Phoebe, admits Phoebe at Atlas,
//     mirrors a solo operator. Ready for push + pull immediately.

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]]) });
const { result } = await fx.requestPrice({ tree, feedId: 42, leaf: sampleLeaf });

// Now assert on `result.transactions` to verify the FulfillPrice landed.
```

Full tutorial: [`skills/phoebe-integrate-consumer.md`](./skills/phoebe-integrate-consumer.md).

---

## Quickstart — operator

You stake at ForgeTON, register a pkShare at Atlas, and push BLS-signed Merkle snapshots to Phoebe every ~30s.

```typescript
import {
    PhoebeMerkleTree,
    computeSnapshotHash,
    signMessage,
    estimatePushValue,
    Phoebe,
} from '@titon-network/phoebe-sdk';

// 1. Build the snapshot tree from your latest aggregated prices
const tree = PhoebeMerkleTree.fromSparseLeaves(new Map([
    [0,  { feedId: 0,  ...btcPrice }],
    [1,  { feedId: 1,  ...ethPrice }],
    // ... up to 256 slots
]));

// 2. Sign (timestamp, root) with the operator's pkShare secret
const timestamp = Math.floor(Date.now() / 1000);
const root = tree.rootAsBigint();
const sigInput = computeSnapshotHash(phoebe.address, timestamp, root);
const aggSig = signMessage(operatorSk, sigInput);

// 3. Submit to Phoebe — `value` covers gas + slack; excess refunds.
const phoebe = tonClient.open(Phoebe.createFromAddress(myPhoebeAddr));
await phoebe.sendPushSnapshot(operatorSender, {
    value:     estimatePushValue(),     // 0.1 TON default
    timestamp,
    root,
    aggSig,
});
```

Full operator-setup playbook: [`skills/phoebe-operator-setup.md`](./skills/phoebe-operator-setup.md).

---

## Quickstart — owner

Deploy + admit + tune.

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

// 1. Deploy
const phoebe = tonClient.open(newPhoebe({
    owner:    ownerAddr,
    forgeton: forgetonAddr,
    atlas:    atlasAddr,
}));
await phoebe.sendDeploy(ownerSender, toNano('1'));

// 2. Atlas owner admits Phoebe as a verifier (triggers GroupKeySync bootstrap)
await atlas.sendSetVerifier(atlasOwnerSender, {
    value: toNano('0.5'),
    contract: phoebe.address,
    isActive: true,
});

// 3. ForgeTON owner admits Phoebe as a consumer (operators get mirrored)
await forgeton.sendSetConsumer(forgetonOwnerSender, {
    value: toNano('0.1'),
    contract: phoebe.address,
    isActive: true,
});

// 4. Tune over time
await phoebe.sendUpdateConfig(ownerSender, { value: toNano('0.05'), pullFee: toNano('0.02') });

// 5. Withdraw accumulated fees
await phoebe.sendWithdrawFees(ownerSender, { value: toNano('0.05'), to: treasuryAddr, amount: toNano('5') });
```

Full deploy + owner-ops playbooks: [`skills/phoebe-deploy.md`](./skills/phoebe-deploy.md) + [`skills/phoebe-owner-ops.md`](./skills/phoebe-owner-ops.md).

---

## Public surface

```typescript
import {
    // Contract
    Phoebe, newPhoebe, PhoebeMerkleTree,
    PHOEBE_DEFAULTS, MIN_RESERVE_FLOOR, MIN_UPGRADE_DELAY, DEFAULT_MAX_PUSH_DRIFT,

    // Constants — opcodes, error codes, schema, BLS, protocol
    OP, ERR,
    PHOEBE_STORAGE_VERSION, PHOEBE_CONFIG_BLOB_VERSION,
    BLS_PUBKEY_BYTES, BLS_SIG_BYTES, BLS_DST_G2_POP,
    DEFAULT_GROUP_ID, MIN_UPGRADE_DELAY_SECONDS,
    MAX_FEED_COUNT, TREE_DEPTH, MAX_MAX_INACTIVITY_SECONDS,
    CAUSE_FIRST_SYNC, CAUSE_ACTIVATION_CHANGE,

    // BLS primitives
    randomBlsSecret, blsPublicKey, aggregateGroupPublicKey,
    signMessage, aggregateSignatures, computeSnapshotHash,

    // Wire-format codecs
    priceLeafToCell, priceLeafFromSlice, parseFulfillPrice,
    phoebeConfigToCell,

    // Events — typed union + per-event interfaces + decoders
    decodeEvent, decodeEvents, tryDecodeEvent,
    type PhoebeEvent, type EventKind,
    type SnapshotPushedEvent, type PricePulledEvent,
    type GroupKeyCachedEvent, type OperatorMirroredEvent,
    // ... and 8 more event types

    // Errors
    explainError, PhoebeError,
    type ErrorCode, type ErrorOrigin, type ErrorExplanation,

    // Diagnostics — collapse a `Transaction` for logging / tests
    summarizeTx, summarizeTxs, formatTxSummary,
    QueryIdStream, estimatePullValue, estimatePushValue,
    type TxSummary,

    // Compiled artifact
    loadPhoebeCode, loadPriceConsumerCode,
    phoebeCodeHash, priceConsumerCodeHash, PHOEBE_CODE_HASH,
    type CompiledArtifact,

    // Live deployment addresses
    PHOEBE_TESTNET, PHOEBE_MAINNET, PHOEBE_EXPECTED_SCHEMA, assertDeployment,
    type PhoebeDeployment,
} from '@titon-network/phoebe-sdk';
```

```typescript
// Sandbox testing subpath — peer-deps @ton/sandbox + @titon-network/atlas-sdk
import {
    deployPhoebeFixture,
    type PhoebeFixture,
    type DeployPhoebeFixtureOpts,
    type FulfillPriceResult,
} from '@titon-network/phoebe-sdk/testing';
```

Full reference: [`AGENTS.md`](./AGENTS.md) (the AI navigator).

---

## CLI — one-off introspection

```bash
phoebe explain 161                    # describe an exit code
phoebe decode <hex|base64>            # decode an external-out event body
phoebe hash                           # bundled Phoebe code hash
phoebe schema                         # SDK-expected schema versions + BLS suite
phoebe estimate pull --pull-fee 0.01  # recommended RequestPrice value
phoebe estimate push                  # recommended PushSnapshot value
phoebe scaffold consumer              # drop a Tolk consumer template
phoebe init                           # write PHOEBE.md (agent context) into cwd
phoebe deployments                    # list known live Phoebe addresses
phoebe info <addr> --testnet          # pretty-print live state
phoebe verify --testnet               # drift check vs PHOEBE_TESTNET
```

Run with `--json` for machine-readable output.

---

## AI-assistant integration

This SDK is **AI-first**. Every published doc is structured for direct LLM ingestion:

- **`AGENTS.md`** — agent navigator: type map, opcode/error ranges, skill index
- **`llms.txt`** — [llmstxt.org](https://llmstxt.org)-format short context (single page)
- **`llms-full.txt`** — full prose digest (multiple pages, deep dive)
- **`ERRORS.md`** + **`OPCODES.md`** — flat tables for grep-friendly lookups
- **`GUARANTEES.md`** — what the SDK guarantees vs. what's the consumer's responsibility
- **`skills/`** — five persona-grouped task playbooks (consumer / deploy / operator / owner / debug)

Drop them into your AI assistant's context window:

```bash
# Claude Code — write the agent context into the consumer repo
npx phoebe init
# → writes PHOEBE.md alongside your existing CLAUDE.md
```

Or load `node_modules/@titon-network/phoebe-sdk/llms-full.txt` directly into your chat.

---

## Status

Phoebe contract is **TSA static-analysis cleared, zero findings**, audited at the same posture as Atlas / Fortuna / ForgeTON / Kronos. **316 contract tests + 172 SDK tests green.** `RequestPrice` supports an optional `freshUpdate` field (Pyth-style "update + read" combined into one op): two operating modes — cached fast-path (~16k gas) for routine reads, fresh-update path (~73k gas) for sub-heartbeat freshness.

`PHOEBE_TESTNET` is populated (2026-05-13 deploy, solo operator, epoch 1, threshold 1/1).
`PHOEBE_MAINNET` is populated (2026-05-18 bootstrap, multi-op n=2, epoch 1, threshold 2). Code-hash `ba7893206a69524d845c7d7cd1406fec28c91dabcf91ba87505502f45d16f395`. Use `assertDeployment(network)` to retrieve the canonical address + expected schema + code-hash for drift checks.

---

## Versioning

This SDK follows [SemVer](https://semver.org/). Breaking changes to the public surface bump the major; new features bump the minor; bug fixes bump the patch. The deployed contract's `PHOEBE_STORAGE_VERSION` + `PHOEBE_CONFIG_BLOB_VERSION` track the on-chain schema separately — `assertDeployment(network)` will surface drift between the two.

---

## License

[MIT](./LICENSE) © 2026 Titon Network
