Система обліку NFT-операцій для податків
Оподаткування NFT — спірна область з юрисдикційними відмінностями. У США IRS трактує NFT як власність (capital asset) — продаж = приріст/збиток капіталу. Мінтинг з колекції — cost basis = комісія газу + ціна мінту. Royalties = звичайний дохід. Безплатні drops — дохід за справедливою вартістю при отриманні.
Специфіка обліку NFT
Відмінність від fungible токенів: кожен NFT унікальний. Cost basis для конкретного #1234 з колекції — ціна саме цього токена, не середня для колекції.
Floor price проти ціни продажу: податкова база при отриманні безплатного NFT — справедлива ринкова вартість. Зазвичай це floor price на час мінту/отримання. Проблема: floor price волатильна, а для рідких ознак реальна вартість вище floor.
Wash trading: покупка та продаж NFT самому собі для штучного завищення ціни — податкове шахрайство.
Схема даних
interface NFTTaxRecord {
tokenAddress: string;
tokenId: string;
collectionName: string;
// Набуття
acquiredAt: Date;
acquiredFrom: string; // адреса або "mint"
acquisitionType: "MINT" | "PURCHASE" | "AIRDROP" | "GIFT" | "TRANSFER_IN";
acquisitionPrice: number; // у ETH
gasAtAcquisition: number;
costBasisUSD: number; // acquisitionPrice + газ (у USD за курсом)
// Розпорядження
disposedAt?: Date;
disposedTo?: string;
dispositionType?: "SALE" | "GIFT" | "BURN" | "TRANSFER_OUT";
salePrice?: number;
royaltyPaid?: number; // royalty комісія творцю
gasAtDisposition?: number;
proceedsUSD?: number; // salePrice - royalty - gas (у USD)
// P&L
realizedGainUSD?: number; // proceedsUSD - costBasisUSD
isLongTerm?: boolean;
// Royalties отримані (якщо власник — творець)
royaltiesReceived?: RoyaltyPayment[];
}
Імпорт NFT транзакцій
class NFTTransactionImporter {
async importNFTHistory(walletAddress: string): Promise<NFTTaxRecord[]> {
// Використовуємо Moralis / Alchemy для історії передач NFT
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";
// Отримуємо ціну з value транзакції або marketplace подіяю
const { price, royalty } = await this.extractPriceFromTx(transfer.txHash);
// Отримуємо FMV для безплатного мінту/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 {
// Безплатний мінт/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;
}
}
Облік Royalties для творців
async function trackRoyaltyIncome(creatorAddress: string): Promise<RoyaltyIncome[]> {
// Знаходимо всі платежі royalty ERC-2981 з подій
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, // звичайний дохід
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 дані | Moralis + Alchemy NFT API |
| Floor prices | Reservoir Protocol API |
| Виявлення продажів | Seaport подіїї + Blur подіїї |
| Історія цін | CoinGecko ETH |
| Сховище | PostgreSQL |
Система обліку NFT податків з імпортом, відстеженням floor price, доходом royalties та мультиюрисдикційними звітами: 4-6 тижнів розробки.







