Разработка системы учета NFT-операций для налогов
NFT налогообложение — спорная область с юрисдикционными различиями. В США IRS трактует NFT как property (capital asset) — продажа = capital gain/loss. Minting из collection — cost basis = gas fee + mint price. Royalties = ordinary income. Бесплатные drops — income по FMV при получении.
Специфика NFT учёта
Отличие от fungible tokens: каждый NFT уникален. Cost basis для конкретного #1234 из collection — цена именно этого токена, не средняя по collection.
Floor price vs sale price: налоговая база при получении бесплатного NFT — fair market value. Это обычно floor price на момент mint/receipt. Проблема: floor price волатилен, и для редких трейтов реальная стоимость выше floor.
Wash trading: покупка и продажа NFT самому себе для искусственного завышения цены — налоговое мошенничество.
Схема данных
interface NFTTaxRecord {
tokenAddress: string;
tokenId: string;
collectionName: string;
// Acquisition
acquiredAt: Date;
acquiredFrom: string; // address или "mint"
acquisitionType: "MINT" | "PURCHASE" | "AIRDROP" | "GIFT" | "TRANSFER_IN";
acquisitionPrice: number; // в ETH
gasAtAcquisition: number;
costBasisUSD: number; // acquisitionPrice + gas (в USD по курсу)
// Disposition
disposedAt?: Date;
disposedTo?: string;
dispositionType?: "SALE" | "GIFT" | "BURN" | "TRANSFER_OUT";
salePrice?: number;
royaltyPaid?: number; // royalty fee для creator
gasAtDisposition?: number;
proceedsUSD?: number; // salePrice - royalty - gas (в USD)
// P&L
realizedGainUSD?: number; // proceedsUSD - costBasisUSD
isLongTerm?: boolean;
// Royalties received (если owner является creator)
royaltiesReceived?: RoyaltyPayment[];
}
Импорт NFT транзакций
class NFTTransactionImporter {
async importNFTHistory(walletAddress: string): Promise<NFTTaxRecord[]> {
// Используем Moralis / Alchemy для NFT transfer history
const nftTransfers = await this.moralis.getNFTTransfers(walletAddress);
const records: NFTTaxRecord[] = [];
for (const transfer of nftTransfers) {
const isReceive = transfer.to.toLowerCase() === walletAddress.toLowerCase();
const isSend = transfer.from.toLowerCase() === walletAddress.toLowerCase();
if (isReceive) {
// Получение NFT
const record = await this.processNFTReceive(transfer, walletAddress);
records.push(record);
}
if (isSend) {
// Передача/продажа NFT
const existingRecord = await this.db.getNFTRecord(
transfer.tokenAddress, transfer.tokenId, walletAddress
);
if (existingRecord) {
await this.processNFTDisposal(existingRecord, transfer);
}
}
}
return records;
}
private async processNFTReceive(
transfer: NFTTransfer,
walletAddress: string
): Promise<NFTTaxRecord> {
// Определяем тип получения
const isMint = transfer.from === "0x0000000000000000000000000000000000000000";
// Получаем цену из transaction value или marketplace event
const { price, royalty } = await this.extractPriceFromTx(transfer.txHash);
// Получаем FMV для бесплатных mint/airdrop
let costBasisUSD: number;
if (price > 0) {
const ethPrice = await this.priceService.getHistoricalPrice("ETH", transfer.timestamp);
costBasisUSD = price * ethPrice + transfer.gasUsed * transfer.gasPrice * ethPrice / 1e18;
} else {
// Бесплатный mint/airdrop — FMV по floor price
const floorPrice = await this.getFloorPriceAtTime(transfer.tokenAddress, transfer.timestamp);
costBasisUSD = floorPrice;
}
return {
tokenAddress: transfer.tokenAddress,
tokenId: transfer.tokenId,
collectionName: transfer.collectionName,
acquiredAt: transfer.timestamp,
acquiredFrom: transfer.from,
acquisitionType: isMint ? "MINT" : price > 0 ? "PURCHASE" : "AIRDROP",
acquisitionPrice: price,
gasAtAcquisition: transfer.gasUsed * transfer.gasPrice / 1e18,
costBasisUSD,
};
}
}
Floor price источники
class NFTFloorPriceService {
async getFloorPriceAtTime(collectionAddress: string, timestamp: Date): Promise<number> {
// Reservoir Protocol для исторических floor prices
const response = await fetch(
`https://api.reservoir.tools/collections/${collectionAddress}/floor-ask?timestamp=${timestamp.getTime() / 1000}`,
{ headers: { "x-api-key": RESERVOIR_API_KEY } }
);
const data = await response.json();
const ethPrice = await this.priceService.getHistoricalPrice("ETH", timestamp);
return (data.price?.amount?.native ?? 0) * ethPrice;
}
}
Royalty учёт для creators
async function trackRoyaltyIncome(creatorAddress: string): Promise<RoyaltyIncome[]> {
// Находим все ERC-2981 royalty payments из событий
const royaltyLogs = await getERC2981RoyaltyPayments(creatorAddress);
return Promise.all(royaltyLogs.map(async log => {
const ethPrice = await priceService.getHistoricalPrice("ETH", log.timestamp);
return {
timestamp: log.timestamp,
collection: log.tokenAddress,
tokenId: log.tokenId,
amountETH: log.royaltyAmount / 1e18,
valueUSD: (log.royaltyAmount / 1e18) * ethPrice,
taxCategory: TaxCategory.ROYALTY_INCOME, // ordinary income
txHash: log.txHash,
};
}));
}
Сводная отчётность
async function generateNFTTaxSummary(
userId: string,
taxYear: number
): Promise<NFTTaxSummary> {
const [sales, royalties] = await Promise.all([
db.getNFTSales(userId, taxYear),
db.getNFTRoyalties(userId, taxYear),
]);
const shortTermGains = sales.filter(s => !s.isLongTerm)
.reduce((sum, s) => sum + s.realizedGainUSD, 0);
const longTermGains = sales.filter(s => s.isLongTerm)
.reduce((sum, s) => sum + s.realizedGainUSD, 0);
const royaltyIncome = royalties.reduce((sum, r) => sum + r.valueUSD, 0);
return {
taxYear,
nftSalesCount: sales.length,
shortTermGains,
longTermGains,
royaltyIncome,
totalTaxableEvents: shortTermGains + longTermGains + royaltyIncome,
saleDetails: sales,
royaltyDetails: royalties,
};
}
Стек
| Компонент | Технология |
|---|---|
| NFT data | Moralis + Alchemy NFT API |
| Floor prices | Reservoir Protocol API |
| Sales detection | Seaport events + Blur events |
| Price history | CoinGecko ETH |
| Storage | PostgreSQL |
NFT tax accounting система с import, floor price tracking, royalty income и мультиюрисдикционными отчётами: 4-6 недель разработки.







