Разработка системы классификации крипто-транзакций
Правильная классификация транзакций — фундамент корректного налогового учёта. Trade, income, airdrop, staking reward — каждый тип имеет разную налоговую трактовку в каждой юрисдикции. Автоматическая классификация снижает ручную работу на 80-90% для большинства пользователей.
Иерархия классификации
enum TaxCategory {
// Capital events
BUY = "buy", // покупка за фиат — не taxable сама по себе
SELL = "sell", // продажа — disposal event
SWAP = "swap", // обмен крипто→крипто — disposal в большинстве юрисдикций
NFT_MINT = "nft_mint",
NFT_SALE = "nft_sale",
NFT_ROYALTY = "nft_royalty",
// Income events
STAKING_REWARD = "staking_reward",
MINING_REWARD = "mining_reward",
LENDING_INTEREST = "lending_interest",
LIQUIDITY_FEES = "liquidity_fees", // Uniswap LP fees
AIRDROP = "airdrop",
HARD_FORK = "hard_fork",
REFERRAL = "referral",
PLAY_TO_EARN = "play_to_earn",
// Non-taxable
TRANSFER = "transfer", // перевод между своими кошельками
COLLATERAL_DEPOSIT = "collateral", // залог в DeFi lending
COLLATERAL_RETURN = "collateral_return",
WRAPPED_TOKEN_MINT = "wrap", // ETH→WETH не является disposal
WRAPPED_TOKEN_BURN = "unwrap",
LP_DEPOSIT = "lp_deposit", // спорно, зависит от юрисдикции
LP_WITHDRAWAL = "lp_withdrawal",
// Gas
GAS_FEE = "gas_fee", // может добавляться к cost basis
UNCLASSIFIED = "unclassified",
}
Движок классификации
class TransactionClassifier {
async classify(tx: UnifiedTransaction, userContext: UserContext): Promise<ClassificationResult> {
// Применяем правила в порядке приоритета
const rules = this.getRulesForContext(userContext);
for (const rule of rules) {
const result = await rule.apply(tx, userContext);
if (result.matched) {
return {
category: result.category,
confidence: result.confidence,
ruleId: rule.id,
metadata: result.metadata,
};
}
}
return {
category: TaxCategory.UNCLASSIFIED,
confidence: 0,
requiresManualReview: true,
};
}
}
// Правила классификации
const CLASSIFICATION_RULES: ClassificationRule[] = [
{
id: "SELF_TRANSFER",
priority: 100,
apply: async (tx, ctx) => {
if (tx.fromAddress && tx.toAddress) {
const [from, to] = await Promise.all([
ctx.isUserAddress(tx.fromAddress),
ctx.isUserAddress(tx.toAddress),
]);
if (from && to) return { matched: true, category: TaxCategory.TRANSFER, confidence: 0.95 };
}
return { matched: false };
},
},
{
id: "WRAPPED_TOKEN",
priority: 90,
apply: async (tx) => {
const wrappedPairs = [
["ETH", "WETH"], ["BTC", "WBTC"], ["SOL", "SOL"], // WSOL
["MATIC", "WMATIC"],
];
const isWrap = wrappedPairs.some(
([native, wrapped]) =>
(tx.assetIn === native && tx.assetOut === wrapped) ||
(tx.assetIn === wrapped && tx.assetOut === native)
);
if (isWrap) return {
matched: true,
category: tx.assetIn.startsWith("W") ? TaxCategory.WRAPPED_TOKEN_BURN : TaxCategory.WRAPPED_TOKEN_MINT,
confidence: 0.95
};
return { matched: false };
},
},
{
id: "STAKING_REWARD_PATTERN",
priority: 85,
apply: async (tx) => {
// Staking reward: получаем токен без отдачи эквивалентного
if (tx.type === "receive" && !tx.assetOut && tx.source === "staking") {
return { matched: true, category: TaxCategory.STAKING_REWARD, confidence: 0.90 };
}
// Признаки staking reward от on-chain data
const isStakingContract = await isKnownStakingContract(tx.fromAddress);
if (tx.type === "receive" && isStakingContract) {
return { matched: true, category: TaxCategory.STAKING_REWARD, confidence: 0.80 };
}
return { matched: false };
},
},
{
id: "AIRDROP_PATTERN",
priority: 80,
apply: async (tx) => {
if (tx.type === "receive" && !tx.assetOut) {
// Маленький размер, новый токен, много получателей в том же блоке
const isMassDistribution = await checkMassDistribution(tx.txHash, tx.assetIn);
if (isMassDistribution) {
return { matched: true, category: TaxCategory.AIRDROP, confidence: 0.75 };
}
}
return { matched: false };
},
},
{
id: "CRYPTO_SWAP",
priority: 50,
apply: async (tx) => {
// Меняем одну крипту на другую (не стейблкоин на фиат)
if (tx.assetIn && tx.assetOut &&
!isFiat(tx.assetIn) && !isFiat(tx.assetOut) &&
tx.assetIn !== tx.assetOut) {
return { matched: true, category: TaxCategory.SWAP, confidence: 0.85 };
}
return { matched: false };
},
},
];
ML модель для неизвестных паттернов
# Для транзакций которые не покрываются rules-based системой
from sklearn.ensemble import RandomForestClassifier
import numpy as np
class TransactionMLClassifier:
def predict(self, tx_features):
# features: [amount, is_eoa_sender, is_contract_sender,
# contract_type, value_in/out_ratio, time_since_last_tx, ...]
features = self.extract_features(tx_features)
prediction = self.model.predict([features])[0]
confidence = max(self.model.predict_proba([features])[0])
return { "category": prediction, "confidence": confidence }
Batch классификация и review queue
// Запрос на ручную проверку неклассифицированных транзакций
async function processUnclassifiedTransactions(userId: string) {
const unclassified = await db.getUnclassified(userId, { limit: 50 });
for (const tx of unclassified) {
// Предлагаем вероятные варианты на основе контекста
const suggestions = await classifier.getSuggestions(tx, { topN: 3 });
await db.updateTransactionSuggestions(tx.id, suggestions);
}
// Уведомляем пользователя что есть транзакции для review
if (unclassified.length > 0) {
await notifyUserReviewNeeded(userId, unclassified.length);
}
}
Система классификации с rules engine, ML fallback и manual review queue — 2-4 недели разработки.







