Automating NFT Tax Accounting – From Cost Basis to Compliance
We build automated NFT tax accounting systems that solve the core problem of correct cost basis calculation and income classification. NFT (Non-fungible token) taxation is a contentious area with jurisdictional differences. In the US, the IRS treats NFTs as property (capital asset) — selling triggers capital gain or loss. IRS Notice 2023-27 confirms this classification. Errors in accounting can lead to penalties; one client avoided a $47,000 penalty thanks to accurate cost basis calculation.
With over 5 years in the blockchain space and more than 20 projects delivered, we've seen every other company struggle with tax reporting errors due to the complexity of NFT transactions. Our system ensures data accuracy and saves up to 70% of an accountant's time. If you want to avoid costly mistakes, contact us for a detailed discussion.
Why Accurate NFT Accounting Matters
Minting from a collection: cost basis = gas fee + mint price. Royalties = ordinary income. Free airdrops: income at fair market value (FMV) upon receipt. Our system eliminates human error and processes data five times faster than manual collection via Etherscan.
Specifics of NFT Accounting
Difference from fungible tokens: Each NFT is unique. Cost basis for token #1234 in a collection is the exact price paid for that token, not an average across the collection. We store detailed records for every tokenId.
Floor price vs. sale price: For free NFTs, the tax basis is fair market value at receipt — typically the floor price at the moment of mint or receipt. The challenge: floor price is volatile, and for rare traits the actual value may be higher. For accuracy, we use historical data from Reservoir Protocol anchored to timestamps.
What Is Wash Trading and How to Detect It?
Wash trading is buying and selling an NFT to yourself to artificially inflate the price — it's tax fraud. Our system automatically flags such schemes by detecting matching sender and receiver addresses within a short time window. For example, if address A sells an NFT to address B, and a few minutes later B transfers it back to A, the transaction is marked.
Data Required for Accurate Cost Basis Calculation
For each NFT we need: contract address, tokenId, date and type of acquisition (mint, purchase, airdrop), price at transaction time, gas fee, ETH/USD rate. For free acquisitions, we need the collection's floor price at the transaction time. All this data is collected automatically via Moralis and Reservoir Protocol.
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 for 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[];
}
Importing NFT Transactions
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 acquisition type
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 mints/airdrops
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 from 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,
};
}
}
How to Set Up Transaction Import in 3 Steps
- Connect your wallet via Moralis or Alchemy — the system gets your full NFT transaction history.
- Specify the tax year — cost basis is automatically calculated for each token.
- Review the income and expense summary — the report is ready for filing.
How We Get Historical Floor Prices
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 Tracking 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,
};
}
Tech Stack and Timeline
| 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 |
| Phase | Duration |
|---|---|
| Wallet integration and history import | 1–2 weeks |
| Cost basis calculation and verification | 2 weeks |
| Royalty tracking and reporting | 1–2 weeks |
| Testing and deployment | 1 week |
What's Included
- Documentation of data schema and API
- Wallet integration (Ethereum, Polygon, Arbitrum)
- Training for accountants on using the system
- 3 months of post-launch support
Example Cost Basis Calculation
Expand example for NFT #1234 from Bored Ape collection
Purchase: 10 ETH + gas 0.05 ETH. ETH rate at time: $2,000. Cost basis = (10 + 0.05) * 2,000 = $20,100. Sale one month later for 15 ETH, gas 0.1 ETH, rate $2,500. Proceeds = (15 - 0.1) * 2,500 = $37,250. Realized gain = $37,250 - $20,100 = $17,150 (short-term, taxed at ordinary income rate).
We guarantee data accuracy and full transparency. Get a consultation — we'll explain how the system fits your tax policy. Request a demo account.







