You sit down on a Saturday to knock out your crypto taxes. Coinbase account, a MetaMask wallet, a few Uniswap swaps. Two hours, maybe three. You've done regular taxes before. How different can it be?
About 18 hours different.
What takes so long
The first thing you hit is data collection. Coinbase exports a CSV in one format. Kraken uses a different one. Your on-chain wallet history lives on Etherscan and has to be pulled by address. If you used Arbitrum or Base, that's separate exports from separate explorers. None of these files share column names, timestamp formats, or transaction ID conventions. Before you've calculated a single gain, you've spent two hours just getting records into the same room.
Then come the transfers. You sent 0.5 BTC from Coinbase to your Ledger in July. Coinbase records a withdrawal. Your Ledger records a deposit. Different timestamps, different transaction IDs, no shared reference linking the two. If your tax software doesn't match them as the same movement, it treats the Coinbase withdrawal as a sale and the Ledger deposit as a new purchase. Suddenly you have a phantom capital gain on BTC you never sold. This happens with ETH too, and SOL, and every other asset you've ever moved between a custodial exchange and a self-custody wallet.
Cost basis compounds the problem. You bought 2 ETH on Coinbase at $1,800 each. You sent 1 ETH to MetaMask. You swapped 0.5 of that ETH for USDC on Uniswap when ETH was at $2,400. The $900 cost basis ($1,800 divided by 2) has to follow that ETH from Coinbase to MetaMask to Uniswap, or your gain calculation is wrong. Tax software that loses the thread anywhere in that chain either overstates your gain or understates it. Both are problems.
DeFi adds a layer most people don't anticipate. Providing liquidity to a Uniswap v3 pool creates an entry event, ongoing fee accrual, and an exit event. Staking on Lido generates stETH with its own basis. Bridging USDC from Ethereum to Base via the native bridge creates two on-chain events with no counterparty report tying them together. Each of these interactions creates taxable events that no 1099 covers, with no guidance from the protocol on how to classify them. You're on your own figuring out what happened.
After all of that, you run your tax software and find 40 misclassified transactions. Transfers marked as trades. LP withdrawals interpreted as income. A bridge transaction flagged as two separate disposals. Each one has to be reviewed manually. At two minutes per transaction, that's over an hour just on cleanup, and that assumes you catch every error.
What goes wrong when people skip this
A lot of people don't go through all of it. They export what they have, run it through software, accept the output, and file. The result is a Form 8949 with phantom gains from unmatched transfers, missing transactions from unconnected wallets, and wrong basis numbers that compound across every subsequent trade.
An accountant working from that 8949 can't fix it without the underlying transaction data. They're auditing your software's output, not your actual activity. If the IRS issues a notice because your reported proceeds don't match what exchanges reported on 1099-DAs, the reconciliation work still has to happen, just under deadline and at higher stakes.
How it should work
A system built for this problem pulls from exchanges via API, ingests wallet history by address across every chain, matches transfers automatically using timestamp windows and amount matching, and maintains cost basis as assets move between venues. It surfaces the ambiguous cases, the transactions it can't match or classify with confidence, and asks you to resolve those specifically rather than dumping everything in your lap.
Moonscape is built around this reconciliation layer. The idea is that the software handles the mechanical matching so the 20 hours collapses into the hour or two of review that actually requires judgment.
The real breakdown
The filing takes 20 minutes. The reconciliation takes 20 hours. That's the actual crypto tax problem, and software that skips it just moves the error somewhere less visible.