NFT Operations Tax Accounting System Development
NFT taxation is a controversial area with jurisdictional differences. In the US, the IRS treats NFT as property (capital asset) — sale = capital gain/loss. Minting from collection — cost basis = gas fee + mint price. Royalties = ordinary income. Free drops — income at FMV when received.
NFT Accounting Specifics
Difference from fungible tokens: each NFT is unique. Cost basis for specific #1234 from collection is the price of that exact token, not the average for collection.
Floor price vs sale price: tax basis when receiving free NFT is fair market value. Usually floor price at mint/receipt time. Problem: floor price is volatile, and for rare traits actual value is higher than floor.
Wash trading: buying and selling NFT to yourself to artificially inflate price is tax fraud.
Data Schema
interface NFTTaxRecord {
tokenAddress: string;
tokenId: string;
collectionName: string;
// Acquisition
acquiredAt: Date;
acquiredFrom: string; // address or "mint"
acquisitionType: "MINT" | "PURCHASE" | "AIRDROP" | "GIFT" | "TRANSFER_IN";
acquisitionPrice: number; // in ETH
gasAtAcquisition: number;
costBasisUSD: number; // acquisitionPrice + gas (in USD at rate)
// Disposition
disposedAt?: Date;
disposedTo?: string;
dispositionType?: "SALE" | "GIFT" | "BURN" | "TRANSFER_OUT";
salePrice?: number;
royaltyPaid?: number; // royalty fee to creator
gasAtDisposition?: number;
proceedsUSD?: number; // salePrice - royalty - gas (in USD)
// P&L
realizedGainUSD?: number; // proceedsUSD - costBasisUSD
isLongTerm?: boolean;
// Royalties received (if owner is creator)
royaltiesReceived?: RoyaltyPayment[];
}
NFT Transaction Import
class NFTTransactionImporter {
async importNFTHistory(walletAddress: string): Promise<NFTTaxRecord[]> {
// Use Moralis / Alchemy for 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) {
// Receiving NFT
const record = await this.processNFTReceive(transfer, walletAddress);
records.push(record);
}
if (isSend) {
// Transfer/sale of 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> {
// Determine type of receipt
const isMint = transfer.from === "0x0000000000000000000000000000000000000000";
// Get price from transaction value or marketplace event
const { price, royalty } = await this.extractPriceFromTx(transfer.txHash);
// Get FMV for free 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 {
// Free mint/airdrop — FMV at 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 Sources
class NFTFloorPriceService {
async getFloorPriceAtTime(collectionAddress: string, timestamp: Date): Promise<number> {
// Reservoir Protocol for historical 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 Accounting for Creators
async function trackRoyaltyIncome(creatorAddress: string): Promise<RoyaltyIncome[]> {
// Find all ERC-2981 royalty payments from events
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,
};
}));
}
Summary Reporting
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,
};
}
Stack
| Component | Technology |
|---|---|
| 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 system with import, floor price tracking, royalty income and multi-jurisdictional reports: 4-6 weeks development.







