- 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>
205 lines
8.6 KiB
Markdown
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.
|