---
name: phoebe-owner-ops
description: Run owner administrative ops on a deployed Phoebe — pause / unpause, update tunables, register feeds, withdraw fees, code upgrade lifecycle. Use when the user says "pause Phoebe", "update pullFee", "withdraw fees", "upgrade the contract", or "manage the oracle".
---

# Phoebe owner operations

You're the owner of a deployed Phoebe. Everything below requires sending from `storage.owner` (read via `phoebe.getOwnership().owner`).

## Pause / Unpause

Blocks `PushSnapshot` + `RequestPrice`; admin paths still work (so you can keep tuning during pause).

```typescript
import { toNano } from '@ton/core';
import { Phoebe } from '@titon-network/phoebe-sdk';

const phoebe = tonClient.open(Phoebe.createFromAddress(phoebeAddr));

await phoebe.sendPause(ownerSender, { value: toNano('0.05') });
// → emits EvtPaused; PushSnapshot / RequestPrice now revert with
//   E_OPERATION_PAUSED (101)

await phoebe.sendUnpause(ownerSender, { value: toNano('0.05') });
// → emits EvtUnpaused
```

Idempotent guards: `Pause` while already paused → `E_ALREADY_PAUSED` (103); `Unpause` while not paused → `E_NOT_PAUSED` (102).

## Update tunables

Partial-update — null fields preserve the existing value. Lets you tune pullFee + drift bound + minStorageReserve without redeploying.

```typescript
await phoebe.sendUpdateConfig(ownerSender, {
    value: toNano('0.05'),
    pullFee: toNano('0.02'),       // raise pull fee from default 0.01 → 0.02 TON
    // minStorageReserve: omitted → preserved
    maxPushDrift: 600,             // widen drift bound to 10 minutes
    operatorBps: 8000,             // shift split to 80% operator / 20% owner
});
// → emits EvtConfigUpdated with the FULL post-update tunable surface (includes operatorBps)
```

Defensive bounds:
- `minStorageReserve >= MIN_RESERVE_FLOOR` (0.05 TON) — set lower → `E_RESERVE_INVARIANT` (241).
- `operatorBps <= 10000` (100%) → `E_INVALID_OPERATOR_BPS` (189) if exceeded.

`operatorBps` controls the per-pull revenue split: `pullFee * operatorBps / 10000` credits the latest-pusher's `operatorRewards` entry (operators claim via `ClaimReward`); the remainder lands in `rewardPool` (owner-collectable via `WithdrawFees` — see below). Default at deploy is 7500 (75% operator / 25% owner).

## Publishing the feed → symbol map (off-chain)

Phoebe doesn't carry an on-chain feed registry. The owner / operator publishes a `feeds.json` (S3, GitHub Pages, IPFS — anywhere consumers can fetch) and consumers pin its hash off-chain. The on-chain verify path is feedId-registration-agnostic — pulls succeed for any `feedId` in `0..MAX_FEED_COUNT` (256) regardless of what's documented in `feeds.json`.

```json
{
    "0": { "symbol": "BTC/USDT", "decimalsHint": -8 },
    "1": { "symbol": "ETH/USDT", "decimalsHint": -8 }
}
```

To rotate the canonical feed list, republish `feeds.json` and bump the hash consumers pin against. No on-chain transaction needed.

## Withdraw accumulated fees

`rewardPool` accumulates the **owner share** of `pullFee` revenue from `RequestPrice` (25% by default, configurable via `operatorBps`). The operator share lives in a separate map and is drained by operators via `ClaimReward` — `WithdrawFees` does NOT touch it. Owner sweeps `rewardPool` to a treasury address.

```typescript
const cfg = await phoebe.getConfig();
console.log('rewardPool (owner share):', cfg.rewardPool);
console.log('operator share goes to operatorRewards map — owner cannot withdraw it');

await phoebe.sendWithdrawFees(ownerSender, {
    value: toNano('0.05'),
    to:    treasuryAddr,                  // distinct from phoebeAddr (E_BAD_WITHDRAW_TARGET 243)
    amount: cfg.rewardPool,                // full drain — or partial
});
// → emits EvtFeesWithdrawn { to, amount }
```

`amount > rewardPool` reverts with `E_AMOUNT_EXCEEDS_ACCUMULATED` (242).

## Re-trigger Atlas bootstrap

If Phoebe was admitted at Atlas but the initial `GroupKeySync` fan-out didn't land (network glitch, Atlas redeployed without re-fanning, etc.), the owner re-triggers manually.

```typescript
await phoebe.sendSyncAtlas(ownerSender, { value: toNano('0.1') });
// → Phoebe sends SyncRequest (0x50) to Atlas; Atlas re-pushes GroupKeySync (0x51).
//   Both legs land in the same tx batch.
```

After: verify via `phoebe.getGroupKey()` — should now return non-null.

## Timelocked code upgrade (3-step)

Mainnet-grade upgrades use the timelock. 24h delay between Propose + Execute lets off-chain observers react to a malicious admin tx (matches Fortuna's pattern).

```typescript
import { compile } from '@ton/blueprint';

const newCode = await compile('Phoebe');     // Blueprint compile output

// Step 1 — propose. eta computed from `delaySeconds`.
await phoebe.sendProposeCodeUpgrade(ownerSender, {
    value: toNano('0.05'),
    newCode,
    delaySeconds: 86400,         // 24h minimum (E_UPGRADE_ETA_TOO_SOON 225)
});
// → emits EvtCodeUpgradeProposed { newCodeHash, eta }

// Wait the timelock window.
// Then step 2 — execute. Reverts (E_UPGRADE_NOT_READY 226) if eta hasn't elapsed.
await phoebe.sendExecuteCodeUpgrade(ownerSender, { value: toNano('0.05') });
// → emits EvtCodeUpgradeExecuted { oldCodeHash, newCodeHash }
//   contract.setCodePostponed swaps the code at end of tx.

// Or step 2-alt — cancel before execute. Useful if a malicious-looking
// proposal was front-run + you need to abort.
await phoebe.sendCancelCodeUpgrade(ownerSender, { value: toNano('0.05') });
// → emits EvtCodeUpgradeCancelled { newCodeHash }
```

Status check: `phoebe.getPendingUpgrade()` returns `{ newCodeHash, eta }` (or `(0n, 0)` if no pending).

Concurrent guards:
- `ProposeCodeUpgrade` while another is pending → `E_UPGRADE_ALREADY_PENDING` (227). Cancel first.
- `Execute` / `Cancel` with no pending → `E_NO_PENDING_UPGRADE` (228).
- `Propose` with empty `newCode` cell → `E_EMPTY_CODE_CELL` (229). Pass a real bytecode cell.

## Watch out for

- **Owner key compromise = total compromise.** Pattern 1 basic upgrade (no timelock) is fine for testnet iteration but unacceptable for mainnet — a compromised owner key can swap the contract bytecode in one tx and steal `rewardPool` + future revenue. Mainnet ships Pattern 2 (timelocked).
- **Multisig the owner.** Production deployments use a multisig as the owner address. Single-key owners are convenient for dev but a single-point-of-failure on mainnet.
- **Gas floors are non-negotiable.** Most owner ops require ≥ `MIN_XC_GAS` (0.01 TON); upgrade ops need `MIN_GAS_FOR_UPGRADE` (0.05 TON). Under-fee → `E_INSUFFICIENT_VALUE` (240).
- **Withdraw target ≠ phoebe.** `WithdrawFees { to: phoebeAddr }` reverts with `E_BAD_WITHDRAW_TARGET` (243) — would strand fees as untracked excess. Always pick a distinct destination.
- **Reserve invariant.** Owner ops respect `assertReserve()` — withdrawing too much would breach `minStorageReserve`. The contract pre-checks; the only failure mode is bad accounting (file a bug if it ever fires under normal use).

## Cross-references

- [`AGENTS.md`](../AGENTS.md) — full SDK surface
- [`phoebe-deploy.md`](./phoebe-deploy.md) — initial setup
- [`phoebe-debug.md`](./phoebe-debug.md) — what to do when something reverts
- [`ERRORS.md`](../ERRORS.md) — every E_* with a hint
