Files
pyre/docs/TOKEN_CLASSIFICATION.md
RogueWave 1a556f33a6 docs+status: fix Token-2022 audit findings; Phase 1 live
- TOKEN_CLASSIFICATION.md: ASCII decision-flow diagram updated to match the
  Rev-2 prose (program → extension → lock → empty → non-empty protected → route),
  no longer routes all Token-2022 to UNSUPPORTED.
- CLAUDE.md: removed stale "Token-2022 support" from out-of-scope; documents
  the gated Token-2022 policy + that classifier code still skips it for now.
- status.json: Phase 1 (Wallet Scanner) marked done — app deployed live at
  feedthepyre.com (app at /, tracker at /status, api at /api), scan verified
  end-to-end through the public stack.

Reviewed by a doc-consistency audit agent (verdict after fixes: consistent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 03:59:34 +00:00

205 lines
8.6 KiB
Markdown

# Token Classification
> Spec for the PYRE token-account classifier. Aligned with
> [`PYRE_MVP_DESIGN.md`](PYRE_MVP_DESIGN.md) §6 and §7 — when in doubt, the design
> doc wins.
PYRE scans a wallet's SPL token accounts and assigns each one a single
conservative category. The category determines what action (if any) the user is
allowed to take and how recovered rent / Essence are handled.
---
## Philosophy: conservative by default
The classifier exists to protect the user from accidentally destroying value. It
is intentionally cautious. It never optimizes for "more accounts cleaned" at the
cost of safety. When two readings of an account are possible, it picks the safer
(less destructive) one.
> **Default rule: Unknown means skip.**
Anything the system cannot fully and safely reason about — an unknown token
program, bad or missing metadata, an unsupported account layout, unfamiliar
extension behavior — is classified into a non-destructive category
(`PROTECTED_SKIP` or `UNSUPPORTED`) and is **never acted on by default**. The
user must manually opt in to anything risky.
The classifier must never say *"this token is safe."* It may only say
*"this token appears eligible based on current checks."* See
[Wording rule](#wording-rule).
---
## Classification categories
Each token account is assigned exactly one of the following categories. The
canonical enum lives in `packages/core` (see [Canonical enum](#canonical-enum)).
### `EMPTY_CLOSE_ONLY`
- **Enum identifier:** `EMPTY_CLOSE_ONLY`
- **Meaning:** A zero-balance token account that can be closed.
- **Allowed action(s):** Close the associated token account (ATA).
- **Rent / Essence rules:** Recovered ATA rent is sent to the **user's wallet**.
Nothing is counted as Essence.
### `INCINERATE_ONLY`
- **Enum identifier:** `INCINERATE_ONLY`
- **Meaning:** An account with a balance that has no safe swap route but may be
burnable junk.
- **Allowed action(s):** The user may burn the balance to zero; if the account
becomes empty after the burn, it may then be closed.
- **Rent / Essence rules:** Recovered rent (from closing the emptied account)
returns to the **user's wallet**. Burned junk does **not** count as Essence.
### `TRANSMUTABLE`
- **Enum identifier:** `TRANSMUTABLE`
- **Meaning:** An account holding a token that has a safe swap route and passes
the risk checks.
- **Allowed action(s):** The user may swap the token into SOL.
- **Rent / Essence rules:** The net swapped SOL may become Essence **only if the
user explicitly opts in**. Without opt-in, proceeds go to the user. Recovered
rent from any subsequently closed account returns to the user.
### `PROTECTED_SKIP`
- **Enum identifier:** `PROTECTED_SKIP`
- **Meaning:** An account PYRE recognizes but will **not touch by default**
because acting on it could destroy or mishandle value.
- **Allowed action(s):** None by default. Acting on these requires an explicit,
manual advanced override by the user.
- **Rent / Essence rules:** No rent recovery and no Essence by default.
- **Example asset types:**
- SOL / WSOL special cases
- USDC / USDT / other major assets
- Valuable meme tokens
- NFTs
- LP tokens
- Receipt tokens
- Staked tokens
- Suspicious tokens
- Frozen accounts
- Delegated accounts
- High-value balances
### `UNSUPPORTED`
- **Enum identifier:** `UNSUPPORTED`
- **Meaning:** An account the system **cannot safely reason about** in the MVP.
- **Allowed action(s):** None. These are skipped.
- **Rent / Essence rules:** No rent recovery and no Essence.
- **Example asset types:**
- Unknown token program
- Bad / missing metadata
- Unsupported account layout
- **Token-2022 accounts/mints with extensions not yet safely handled** —
confidential transfer, withheld transfer fees, or any unrecognized extension
(see design doc §7.1). Token-2022 with benign/no extensions is classified
like classic SPL.
---
## Safety rules
Reproduced from design doc §7 as a checklist. The classifier and any action
builder must enforce **all** of these. MVP rules:
- [ ] Support classic SPL **and** Token-2022; gate Token-2022 on account+mint
extensions per design doc §7.1 (skip confidential transfer, withheld
transfer fees, frozen, and any unknown extension). Use the correct token
program per account; `CloseAccount` as a top-level instruction.
- [ ] Reclaimable rent = the account's live lamports (not a constant).
- [ ] Skip NFTs.
- [ ] Skip compressed NFTs.
- [ ] Skip LP tokens.
- [ ] Skip frozen accounts.
- [ ] Skip delegated accounts.
- [ ] Skip known valuable assets.
- [ ] Skip tokens above a user-safe USD threshold.
- [ ] Skip routes with high price impact.
- [ ] Skip routes with stale quotes.
- [ ] Skip unsafe or weird liquidity paths.
- [ ] Simulate all transactions before final signing.
---
## Wording rule
The system must **never** state that a token is safe.
- Forbidden: *"This token is safe."*
- Required: *"This token appears eligible based on current checks."*
This wording is load-bearing: it communicates that eligibility is a snapshot of
current automated checks, not a guarantee about the asset.
---
## Decision flow
How a single account flows from scan to a final category:
1. **Scan** — fetch the token account: owner, ATA address, mint, token program,
raw/UI balance, decimals, metadata if available, token program type,
frozen/delegated state, and (Token-2022) the account **and mint** extension
list.
2. **Token program check** — classic SPL and Token-2022 are both supported. An
unknown/other token program ⇒ `UNSUPPORTED`. Stop.
3. **Extension check (Token-2022)** — if the account/mint carries an extension
PYRE does not safely handle (confidential transfer, withheld transfer fees, or
any unrecognized extension), classify as `UNSUPPORTED`. Stop. Transfer-hook /
permanent-delegate mints continue but are flagged (see design doc §7.1).
4. **Account integrity check** — bad/missing metadata or unsupported layout ⇒
`UNSUPPORTED`. Stop.
5. **Lock check** — frozen or delegated accounts are skipped regardless of
balance: classify as `PROTECTED_SKIP`. Stop.
6. **Empty check** — if the balance is zero (and classic-SPL/Token-2022 with safe
extensions, not frozen/delegated), classify as `EMPTY_CLOSE_ONLY`. Stop.
(Empty accounts hold no value, so they close regardless of mint identity.)
7. **Protected check** — for a NON-empty balance: if the asset is an NFT,
compressed NFT, LP token, receipt token, staked token, major/known-valuable
asset, SOL/WSOL special case, suspicious token, or a high-value /
over-threshold balance, classify as `PROTECTED_SKIP`. Stop.
8. **Route check** — evaluate a swap route. If a safe route exists and passes all
risk checks (no high price impact, no stale quote, no weird liquidity path,
simulation passes), classify as `TRANSMUTABLE`. Stop. (No swap routing in MVP
v0.1, so this never fires yet.)
9. **Fallback** — otherwise the account has a balance with no safe route but may
be burnable junk: classify as `INCINERATE_ONLY`.
At any step where the system cannot safely decide, it falls back to a
non-destructive category — **Unknown means skip.**
```
scan account
├─ unknown/other token program (not SPL or Token-2022)? ─► UNSUPPORTED
├─ Token-2022 unsafe/unknown extension
│ (confidential transfer / withheld fee / unrecognized)? ► UNSUPPORTED
├─ bad metadata / unsupported layout? ───────────────────► UNSUPPORTED
├─ frozen or delegated? (regardless of balance) ────────► PROTECTED_SKIP
├─ balance == 0? (closes regardless of mint identity) ──► EMPTY_CLOSE_ONLY
├─ NON-empty & NFT / cNFT / LP / receipt / staked / major /
│ valuable / SOL-WSOL / suspicious / over-threshold? ───► PROTECTED_SKIP
├─ NON-empty & safe swap route + passes risk checks? ────► TRANSMUTABLE
└─ otherwise (burnable junk, no safe route) ─────────────► INCINERATE_ONLY
```
(Transfer-hook & permanent-delegate Token-2022 mints pass the program/extension
gates — they are cleanable — but are flagged and excluded from swap. See design
doc §7.1.)
---
## Canonical enum
The canonical classification enum and the risk rules live in **`packages/core`**
(shared types and business logic). Apps and the worker import the enum from
there; they must not redefine it.
**The server must recompute classification.** Client-submitted classifications
are never trusted — the backend recomputes classification server-side before
building any transaction (design doc §16). A category arriving from the client is
treated as a hint at most, never as authority for a destructive action.