Files
pyre/docs/TOKEN_CLASSIFICATION.md
RogueWave c20094ab56 chore: scaffold PYRE MVP monorepo (structure + docs)
pnpm + TypeScript workspace per design doc §13:
- apps/{web,api,worker} skeletons (Next.js 16, Fastify 5, BullMQ)
- packages/{core,solana,prometheus,db,config} (core has real types/DTOs;
  solana/prometheus are stubs)
- programs/pyre-core placeholder (future Anchor, v1.0)
- docs/: PYRE_MVP_DESIGN (canonical), ARCHITECTURE, SECURITY, TOKEN_CLASSIFICATION
- CLAUDE.md, README, .env.example (no private-key var by design)

Skeleton + docs only — no Solana/business logic yet. All workspaces typecheck clean.

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

7.3 KiB

Token Classification

Spec for the PYRE token-account classifier. Aligned with 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.


Classification categories

Each token account is assigned exactly one of the following categories. The canonical enum lives in packages/core (see 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:
    • Token-2022 accounts (in the MVP)
    • Unknown token program
    • Bad / missing metadata
    • Unsupported account layout
    • Accounts with extension behavior not yet handled

Safety rules

Reproduced from design doc §7 as a checklist. The classifier and any action builder must enforce all of these. MVP rules:

  • Classic SPL only.
  • Skip Token-2022 by default.
  • 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, and frozen/delegated state if available.
  2. Token program check — if not classic SPL (e.g. Token-2022, unknown program), classify as UNSUPPORTED. Stop.
  3. Account integrity check — if metadata is bad/missing, the account layout is unsupported, or extension behavior is unhandled, classify as UNSUPPORTED. Stop.
  4. Protected check — if the asset is an NFT, compressed NFT, LP token, receipt token, staked token, major/known-valuable asset, SOL/WSOL special case, suspicious token, frozen account, delegated account, or a high-value / over-threshold balance, classify as PROTECTED_SKIP. Stop.
  5. Empty check — if the balance is zero, classify as EMPTY_CLOSE_ONLY. Stop.
  6. 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.
  7. 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
  ├─ not classic SPL? ───────────────────────────────► UNSUPPORTED
  ├─ bad metadata / layout / unhandled extension? ───► UNSUPPORTED
  ├─ NFT / cNFT / LP / receipt / staked / major /
  │  valuable / SOL-WSOL / suspicious / frozen /
  │  delegated / over-threshold? ────────────────────► PROTECTED_SKIP
  ├─ balance == 0? ──────────────────────────────────► EMPTY_CLOSE_ONLY
  ├─ safe swap route + passes risk checks? ──────────► TRANSMUTABLE
  └─ otherwise (burnable junk, no safe route) ───────► INCINERATE_ONLY

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.