@titon-network/phoebe-sdk
v0.5.0
Published
TypeScript SDK for Phoebe — TON-native 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 (group key) + ForgeTON (operator stake). Ships sandbox fixture, event decod
Downloads
110
Maintainers
Readme
@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 (group key) + ForgeTON (operator stake). Built on the TSA-audited @titon-network/atlas-sdk + @titon-network/forgeton-sdk.
Audit status
🛡️ TSA-audited — zero findings. Full report:
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 and @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.
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-sdkConnect — 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.
// ── 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:
// ── 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 dereferencingPHOEBE_MAINNET/PHOEBE_TESTNETdirectly — the helper throws an actionable error if the constant isnulland exposesexpectedCodeHash+expectedSchemaso you can diff against the live contract'scodeHashandgetSchemaVersions().
30-second integration — pull a price
Phoebe is live. This block reads feed 42 directly from the deployed
mainnet contract — copy, paste, run.
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.mdor runnpx phoebe scaffold consumer --out contracts/my-consumer.tolkto drop the canonical template into your project.
What this gives you
Phoebecontract wrapper — everysend*+ everyget*mirroring the on-chain ABIPhoebeMerkleTree— off-chain proof generator, byte-identical to the on-chain walker- BLS primitives (
signMessage/aggregateSignatures/computeSnapshotHash) — operator-side signing helpers, withBLS_DST_G2_POPbaked in (avoids the #1 cause ofE_INVALID_BLS_SIGNATURE) - Event decoder (
decodeEvent/decodeEvents) — typedPhoebeEventdiscriminated union, switch onkind - Error explainer (
explainError/PhoebeError) — every exit code →{ origin, name, message, hint } - Bundled artifact (
loadPhoebeCode) — no separate compile step - Sandbox testing (
@titon-network/phoebe-sdk/testingsubpath) —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/— load one per task
Quickstart — consumer
You're a TON dapp. You want to read a price.
1. Scaffold a consumer contract
npx phoebe scaffold consumer --out contracts/my-consumer.tolkCustomize 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:
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
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
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
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.
Quickstart — operator
You stake at ForgeTON, register a pkShare at Atlas, and push BLS-signed Merkle snapshots to Phoebe every ~30s.
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.
Quickstart — owner
Deploy + admit + tune.
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-owner-ops.md.
Public surface
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';// 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 (the AI navigator).
CLI — one-off introspection
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_TESTNETRun 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 indexllms.txt— 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 lookupsGUARANTEES.md— what the SDK guarantees vs. what's the consumer's responsibilityskills/— five persona-grouped task playbooks (consumer / deploy / operator / owner / debug)
Drop them into your AI assistant's context window:
# Claude Code — write the agent context into the consumer repo
npx phoebe init
# → writes PHOEBE.md alongside your existing CLAUDE.mdOr 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. 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 © 2026 Titon Network
