Lesson 74Crypto-fiat boundaryAdvanced

Tax-lot tracking: FIFO vs LIFO vs HIFO

You bought BTC at three different prices. Which 'cost basis' did you sell?

By Solomon Ajayi · Free to read, no signup

Crypto and FX positions have a cost-basis problem. A user buys 0.1 BTC at ₦60M/BTC, 0.1 BTC at ₦70M/BTC, and 0.1 BTC at ₦80M/BTC. Now they sell 0.15 BTC. Which lots did they sell? Tax outcomes depend entirely on the COST BASIS METHOD: FIFO (sell the oldest lots first) reports lower gain in a rising market; LIFO (sell the newest first) reports higher gain; HIFO (sell highest-cost first) minimizes taxable gain. Many crypto-fiat fintechs let users PICK the method (with tax-form generation reflecting the choice); regulators in some jurisdictions force FIFO. The ledger needs to track lots per user per asset: opening lot, lots consumed by each sale, remaining lot balance. This lesson posts a sale under FIFO and shows the per-lot consumption logic.

When a user holds the same asset bought at different prices, a sale does not have one obvious cost. They bought 0.1 BTC at three different prices, so selling 0.15 BTC could mean selling the cheap coins, the expensive coins, or some blend. That choice is not cosmetic: it decides how much realised gain the user owes tax on. The proceeds are fixed by the sale price, but the cost basis you subtract from them is a policy decision, and the gain is the difference.

FIFO consumes the oldest lots first, which in a rising market means the cheapest, so it reports the largest gain. HIFO consumes the highest-cost lots first, which shrinks the gain and the tax bill. LIFO sits in between. Same coins, same sale price, three different tax outcomes, all defensible if you can show which lots you consumed and in what order. That is why the entry here debits the bank for proceeds, credits BTC Custody for the exact cost basis of the lots consumed, and credits Realised Gain for the difference.

This is why the ledger entry alone is not enough. The three-line journal entry is trivial, but it only balances correctly if a lot-tracking table sitting beside the ledger tells you the exact cost basis consumed. One row per buy event, with quantity, cost basis, and acquired-at, and a sell-time job that draws down lots in the configured order. Get the per-lot bookkeeping wrong and the gain you post is wrong, which means the tax form you hand the user is wrong.

Worked example, step by step

State: user has 0.3 BTC across three lots

Lot A: 0.1 BTC bought at ₦60M/BTC = cost basis ₦6M. Lot B: 0.1 BTC at ₦70M = ₦7M. Lot C: 0.1 BTC at ₦80M = ₦8M. Total custody: 0.3 BTC, total cost basis: ₦21M.

State: 3 BTC lots, ₦21M total cost basis
AccountDebitCredit
BTC Custody (per-lot) (1900)₦21,000,000.00
NGN Bank Account (1200)₦21,000,000.00

BTC Custody UP ₦21M (debit, the sum of cost bases). NGN Bank UP ₦21M (debit), wait, the cash flowed OUT to buy the BTC. So Bank goes DOWN. Reframe: this is a STATE snapshot. We use the cost-basis representation: BTC Custody at total cost = ₦21M. Bootstrap with a contra (user wallet liability) for balance.

User sells 0.15 BTC at current price ₦90M/BTC (FIFO method)

Sale proceeds: 0.15 × ₦90M = ₦13.5M. Under FIFO, you consume from oldest lot first. Lot A is 0.1 BTC fully consumed (cost ₦6M); Lot B contributes 0.05 BTC partially (cost ₦3.5M). Total cost basis consumed: ₦9.5M. Realised gain: ₦13.5M proceeds − ₦9.5M cost = ₦4M.

FIFO sale 0.15 BTC at ₦90M: gain ₦4M
AccountDebitCredit
NGN Bank Account (1200)₦13,500,000.00
BTC Custody (per-lot) (1900)₦9,500,000.00
Realised Gain on Crypto Sale (4800)₦4,000,000.00

NGN Bank UP ₦13,500,000 (debit, cash in). BTC Custody DOWN ₦9,500,000 (credit, cost basis of lots consumed). Realised Gain UP ₦4,000,000 (credit, P&L). Three lines, balanced: 13,500,000 = 9,500,000 + 4,000,000. The remaining 0.15 BTC sits at: Lot B 0.05 BTC (cost ₦3.5M) + Lot C 0.1 BTC (cost ₦8M) = 0.15 BTC at ₦11.5M cost.

Takeaway

Tax-lot tracking is critical for any custodial crypto product. The CHOICE OF METHOD (FIFO/LIFO/HIFO/Specific-ID) materially changes user tax liability and your platform's reporting obligations. Build the lot-tracking table EARLY: one row per buy event with (user, asset, quantity, cost_basis, acquired_at), and a sell-time consumption job that reduces lots in the configured order, posting the realised gain entry per sale. The journal entries are simple; the per-lot bookkeeping is what makes the year-end tax form generation tractable for your users. Without it, you're shipping users to 1099-misery and your CPA-call volume spikes every January.

The code behind it

FIFO lot drawdown for a 0.15 BTC sale, reproducing the lesson's NGN 4M realised gain (and HIFO's NGN 2M).

from decimal import Decimal as D

# (qty BTC, unit_cost NGN/BTC, acquired_at) -> Lot A/B/C from the lesson
lots = [(D("0.1"), D("60_000_000"), "A"),
        (D("0.1"), D("70_000_000"), "B"),
        (D("0.1"), D("80_000_000"), "C")]

def realised_gain(lots, sell_qty, sell_price, method="FIFO"):
    # FIFO: oldest first. HIFO: highest unit cost first.
    order = lots if method == "FIFO" else sorted(lots, key=lambda l: -l[1])
    remaining, cost_basis = sell_qty, D(0)
    for qty, unit_cost, _ in order:
        if remaining <= 0:
            break
        take = min(qty, remaining)              # partial lot allowed
        cost_basis += take * unit_cost          # 1900 BTC Custody credit
        remaining -= take
    assert remaining == 0, "not enough BTC across lots"
    proceeds = sell_qty * sell_price            # 1200 NGN Bank debit
    return proceeds, cost_basis, proceeds - cost_basis  # 4800 Realised Gain

for m in ("FIFO", "HIFO"):
    proceeds, basis, gain = realised_gain(lots, D("0.15"), D("90_000_000"), m)
    print(f"{m}: proceeds={proceeds:,} cost_basis={basis:,} realised_gain={gain:,}")
# FIFO: proceeds=13,500,000.00 cost_basis=9,500,000.00 realised_gain=4,000,000.00
# HIFO: proceeds=13,500,000.00 cost_basis=11,500,000.00 realised_gain=2,000,000.00

Practice this on a real ledger

Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.

More in this section

Search lessons

Type to find any of the 85 lessons. Press Enter to open.