-
Notifications
You must be signed in to change notification settings - Fork 0
Classifier Rules
The classifier lives in packages/classifier/. It transforms RawEvent[] into LedgerEntry[] using an 8-rule chain. Rules run in order — the first rule that claims an event wins.
File: rules/01-cb-pair-merger.ts
Safety net for Coinbase internal move pairs (Retail Staking Transfer, Retail Eth2 Deprecation). Groups by (timestamp, abs(quantity)) and produces internal_move ledger entries. Most pairing is already handled by the Coinbase adapter, but this rule catches any that slip through.
File: rules/02-cb-self-transfer.ts
For every Coinbase Send event, parses the destination address from the Notes field. If the address matches any known wallet of the user, marks as transfer_self. Otherwise leaves for later rules.
File: rules/03-cross-source-match.ts
Matches transfers across sources using fuzzy criteria:
- Same direction (one outbound, one inbound)
- Amount within ±0.5% (gas tolerance)
- Timestamp within ±10 minutes
When matched, both raw events are merged into one internal_move ledger entry. This handles the common case of a Coinbase withdrawal appearing as both a Coinbase Send and an on-chain incoming transfer.
File: rules/04-dex-swap-collapse.ts
Groups on-chain events by txHash. If the to address matches a known DEX router (from dex-routers.json), collapses the multiple transfer legs into one synthetic trade event with 2 legs + 1 gas-fee leg.
Supported routers: Uniswap V2/V3, Universal Router, MetaMask Swap Router, QuickSwap (Polygon).
File: rules/05-bridge-detection.ts
Marks transfers to/from known bridge contracts as internal_move if a corresponding receive on the destination chain exists within 24 hours. Otherwise leaves as unknown for user review.
Supported bridges: Celer cBridge V2, Polygon PoS Bridge.
File: rules/06-approval-gas.ts
ERC-20 approve calls have no asset movement but cost gas. This rule produces fee_disposal entries for the gas spent. Also handles crypto_out events where all legs have feeFlag: true (failed transaction gas).
File: rules/08-nft-classification.ts
Detects NFT acquisition and disposal patterns by grouping nft_event raw events by txHash and pairing them with fungible counterpart legs in the same transaction.
Classification logic:
| NFT Direction | Fungible Counterpart | Result | Reason |
|---|---|---|---|
| NFT in + fungible out | Yes | nft_acquisition |
NFT purchase |
| NFT in from null address + fungible out | Yes | nft_acquisition |
NFT mint |
| NFT in alone | No | nft_acquisition |
NFT airdrop |
| NFT out + fungible in | Yes | nft_disposal |
NFT sale |
| NFT out alone | No | nft_disposal |
NFT transfer out |
| NFT in + NFT out (same tx) | Either | Both nft_disposal + nft_acquisition
|
NFT-for-NFT trade |
Key behaviors:
- Mint detection: checks if the NFT's
counterpartyis the null address (0x0000000000000000000000000000000000000000) - Preserves
contractAddressandtokenIdon all NFT legs - Sets NFT leg amount to
'1'for acquisitions,'-1'for disposals - Consumes both NFT events and their paired fungible events (prevents double-counting by later rules)
- Only claims
nft_eventtype events — all other types pass through
User overrides take precedence over all rules. Before the rule chain runs, the classifier checks for ClassifierOverride records matching the event's raw event IDs. If found, the override's type is used directly.
Overrides are created via:
-
daybook classify --review(interactive Ink UI) - Direct database insertion
Overrides survive re-sync and re-classification.
File: dex-routers.json
A curated list of known DEX router contract addresses used by Rule 4 for swap detection.
File: bridges.json
A curated list of known bridge contract addresses used by Rule 5 for bridge detection.
daybook classify --dry-run runs the full rule chain without writing to the database. It displays:
- Total events processed
- Ledger entries per type
- A diff against the current database state (added, removed, unchanged)
daybook classify --review launches an Ink-based interface after classification. It shows all unclassified entries and lets you assign types interactively. Each override is persisted as a ClassifierOverride record.
The review is automatically skipped when:
- There are no unclassified entries
- stdout is not a TTY (piped output)
-
--no-reviewflag is passed
Getting Started
Usage
Architecture