Correction, 12 September 2026. This post says below that average cost is the treatment most tax regimes apply to a coin-to-coin trade. That is wrong for the United States, where average cost is not a permitted method for crypto. Since 1 January 2025 the accepted methods are first in first out, which is the default, and specific identification with records made at the time of the trade. The tracker is moving to first in first out. The full correction is in The Most Dangerous Number. The rest of this post, on why a swap has to move basis at all, still stands.
We mine one coin and hold others. Payouts land in one asset, and some of them get swapped into the assets we actually want to own for the long run. Until this morning our open-source wallet tracker had no way to say that. It knew about purchases and mining payouts, and a swap is neither. The mined coin sat in the ledger as income with nothing on the other side, and the coin it became showed up in a wallet with no basis at all.
That is fixed. This post is about how, because the accounting is the interesting part and the code is short.
A swap is two things at once
A swap is a disposal and an acquisition in the same moment. You give up some units of one asset and receive some units of another, and at that moment the trade has a dollar value. Three numbers, plus a date and a venue. That is the whole record:
What you gave up, what you got, and what it was worth. Nothing else is needed.
What the ledger does with those three numbers is where people usually get it wrong, so here is the rule it follows, and then the arithmetic.
Average cost, and why
The tracker uses average cost. The units you give up leave at the average cost of everything you held of that asset. The difference between that and the swap's dollar value is realized. The units you receive arrive with the swap's dollar value as their basis.
Concretely. Say you bought 0.03 BTC in two lots for a combined $2,727, fees included. Your average cost is $909 per 0.01 BTC. Now you swap 0.01 BTC for 5 SOL, and the trade is worth $1,000 at the time.
| Before | After | |
|---|---|---|
| BTC units | 0.03 | 0.02 |
| BTC basis | $2,727 | $1,818 |
| SOL units | 0 | 5 |
| SOL basis | $0 | $1,000 |
| Realized | $0 | $91 |
$909 of basis left with the BTC. $1,000 arrived with the SOL. The $91 gap is a realized gain, because you disposed of something worth more than you paid for it. If BTC had been below your average cost, the same arithmetic gives a realized loss.
This is one recognised way to treat a coin-to-coin trade, though see the correction at the top: it is not a permitted one in the United States. The reason it appealed to us is that it keeps the word basis honest. Basis is what a position cost you. After a swap, the SOL cost you $1,000 of BTC. Nothing about that changes because the BTC was mined, or was cheap once, or is up today.
Two edge cases the code handles on purpose:
- Swapping out of something with no basis. Mined coins that were never priced have a basis of zero. Swap them and the whole swap value is realized, because that is what happened: something you got for nothing became something worth money.
- Swapping more than the ledger knows you hold. Maybe you swapped coins that were bought elsewhere and never logged. The units and the basis stop at zero rather than going negative. A negative basis is a number that means nothing, so the tracker refuses to produce one.
The twelve lines
The ledger module is dependency-free and pure, so this is the entire implementation of the rule above. Events arrive in chronological order, which matters, because a swap's average cost depends on what was held at that moment.
if (e.kind === "swap" && e.from) {
const f = get(e.from.asset, e.date);
const basis = f.usd + f.fees;
const units = Math.min(e.from.qty, f.qty);
const removed = f.qty > 0 ? Math.min(basis, (basis / f.qty) * units) : 0;
const share = basis > 0 ? removed / basis : 0;
f.usd -= f.usd * share;
f.fees -= f.fees * share;
f.qty = Math.max(0, f.qty - e.from.qty);
f.swappedOut += e.usd;
f.realized += e.usd - removed;
const t = get(e.asset, e.date);
t.qty += e.qty; t.usd += e.usd; t.fees += e.fees; t.swappedIn += e.usd;
continue;
}
Fees are tracked separately from the rest of basis, and they leave proportionally, so a position's fee share stays accurate after a partial swap. It is a small thing that would be annoying to get wrong later.
The worked example above is a unit test. So are the two edge cases. They run in plain Node with no framework:
npm test
A swap is never dollars in
The dashboard has a tile called Dollars in. It is purchases plus mining income, and it answers one question: how much new money or new value has entered this book. A swap enters nothing. It moves value from one column to another. So swaps are excluded from that tile and from the dollars-in-by-month chart, and get their own tile instead: Realized, the sum of every swap's gain or loss, with the count of swaps under it.
It would have been easy to let a swap inflate dollars in. Every swap would look like a $1,000 purchase and the number would climb on its own. That is the kind of chart that feels good and tells you nothing. The composition chart already shows the swap the honest way: one band shrinks and another grows on the same day.
What we do with it
Mining pays out in one coin. We swap a share of each payout into a small set of assets we intend to hold, and log each swap the day it happens. The ledger then shows, per asset, what the position cost, what it is worth now, and what we have realized along the way. Value now is only what the tracked wallets hold; anything sitting on an exchange shows basis with nothing against it, and the page says so under the table.
That is as much as we will say about strategy. The point of the tool is that the numbers are ours to check, at any time, against public chain data, with the arithmetic in the open.
Try it
git clone https://github.com/Gpatrickj7/tofte-wallet-tracker-.git
cd tofte-wallet-tracker-
npm install
DASH_USER=me DASH_PASS=me DEMO_MODE=1 npm run dev
Swaps need storage, so point MONGODB_URI at a free Atlas cluster to log real ones. The demo page shows the charts without any. The code is MIT at github.com/Gpatrickj7/tofte-wallet-tracker-, and the previous post, Balances Have No Memory, covers the ledger and charts this builds on.