You hold Bitcoin in self-custody. Good. You buy through multiple brokers. Even better. And now the tax authority asks which coins you bought when, which of them are tax-free and exactly how high your taxable gain is.
For exactly this problem there is the BTC Tax Tool β an open-source tool I developed myself for my own tax return (CLI, web version and app). It reads your CSV exports from BitBox, Sparrow, Electrum, Trezor Suite, Ledger Wallet, 21bitcoin, Bison, Swissquote, Strike, Pocket and Bisq (other exchanges via a CoinTracking or Blockpit export) and automatically calculates what has to go to the tax office and what remains tax-free.
"Don't Trust, Verify β including your accountant. If you understand how the calculation works, you're not at anyone's mercy."
πΈ The Web Version β drop your CSVs, get your report. Free.
No Python, no terminal: the tax tool also runs as a web app β free, no access link, no account. Drag your CSV exports into the browser, the app detects every broker automatically and computes all tax years at once. Your files never leave your machine β the entire calculation runs locally in your browser via WebAssembly. No upload, no account, no tracking. Same open-source calculation engine as the CLI tool.
Note: the web app interface switches between German and English (language button in the app). The reports themselves are made for the German tax system, Β§ 23 EStG.
β Open the web version (fictional sample data included to try it out)
β The guide β step by step with screenshots
β As an app for Android and Linux desktop
β Web version, app or CLI β which fits you?
1. The Foundation: What German Tax Law Says
Bitcoin is classified in Germany as a private disposal transaction (Β§ 23 EStG). The rules:
| Situation | Tax treatment |
|---|---|
| BTC held for one year or less, then sold | Profit is a taxable event β tax is due once the exemption threshold is reached |
| BTC held for more than one year, then sold | Profit tax-free |
| Transfer between your own wallets/accounts | The transferred holdings: not a taxable event |
| Network/withdrawal fee paid in bitcoin | Disposal of the fee portion at the daily rate (BMF circular of 6 March 2025, paras. 54, 60) β usually cents, the tool includes them |
| Bitcoin given away or donated | Not a disposal, but the coins leave your holdings β acquisition date and cost carry over to the recipient (Sec. 23 (1) sentence 3 EStG) |
| Profit under 600 EUR/year (until 2023) | Exemption threshold β not a single cent in tax |
| Profit under 1,000 EUR/year (from 2024) | Exemption threshold β not a single cent in tax |
The method is called FiFo β First In, First Out. That means: what you buy first, you also sell first. Sounds simple, but gets complicated quickly when you buy from five brokers and your BTC moves between multiple wallets. On top of that, the German Federal Ministry of Finance circular of 6 March 2025 requires in para. 62 a wallet-based view β FiFo applies per wallet, not across everything at once. Since October 2026 the tool calculates exactly that way and also treats every exchange account as a wallet of its own.
The Exemption Threshold Is a Trap
Many people think: "I'm under 1,000 EUR, so it's irrelevant." Wrong. The exemption threshold is not a tax allowance. If your gain reaches or exceeds it, the entire taxable profit becomes taxable β not just the part above it. A profit of 1,001 EUR is therefore fully taxable β not just that one euro.
2. The Problem with Self-Custody
Tax apps like CoinTracking or Blockpit work well for exchanges that record everything. For genuine self-custody holders with hardware wallets, problems arise:
- You transfer BTC from your BitBox to an exchange for a sale β this looks like a purchase.
- You have multiple wallets β the cost basis travels with them.
- Swissquote buys in USD β the historical exchange rate must be correctly factored in.
- You enter your complete financial data into a cloud app β that contradicts the whole point of self-custody.
The BTC Tax Tool runs locally on your machine. No cloud, no registration, no third-party servers. Your data stays with you.
From here on: the path for technical users (free CLI tool)
Sections 3β5 below describe the free CLI tool from GitHub β for everyone
who likes working in a terminal.
Using the web version or the app? Then you
need none of this. No Python, no terminal β drop or pick your CSVs,
done. Skip straight to section 6: What you submit to the tax office.
3. Installing the CLI tool (one-time β GitHub version only)
You need Python 3.10 or newer. Many Linux systems ship it; check the version with python3 --version (Windows: python --version). On macOS and Windows you may need to install it first (python.org).
Mac / Linux:
git clone https://github.com/Alien-Investor/btc-steuertool.git cd btc-steuertool python3 -m venv .venv .venv/bin/pip install -r requirements.txt
Windows:
git clone https://github.com/Alien-Investor/btc-steuertool.git cd btc-steuertool python -m venv .venv .venv\Scripts\pip install -r requirements.txt
That's it. No database, no server, no subscription.
4. Place Your CSV Files (CLI)
Export your CSV files from the respective apps and place them in the corresponding folders.
The folders bitbox/ and Broker/ don't exist yet after cloning β
they are listed in .gitignore so your financial data never accidentally ends up on GitHub.
You need to create them once yourself:
Mac / Linux:
mkdir -p bitbox Broker
Windows:
mkdir bitbox mkdir Broker
| Source | Folder | Filename |
|---|---|---|
| BitBox (hardware wallet) | bitbox/ |
any name, all *.csv files are loaded |
| Sparrow, Electrum, Trezor Suite, Ledger Wallet (since v1.4) | wallets/ (noKYC: wallets/nokyc/) |
any name, the program is detected from the contents; the file name becomes the wallet's name (for Ledger the account names count, every account is a wallet of its own). Format derived from the programs' source code, not yet confirmed against real exports β the report warns |
| 21bitcoin | Broker/ |
must start with 21bitcoin |
| Bison | Broker/ |
Bison-CSV-Gesamt.csv |
| Swissquote | Broker/ |
Swissquote_CSV-Gesamt.csv |
| Strike | Broker/ |
strike_2024.csv, strike_2025.csv etc. |
Broker/ |
must start with Pocket |
|
| Bisq (noKYC) | Broker/ |
must start with bisq: Bisq saves as tradeHistory.csv, rename it e.g. to bisq.csv. Export with Bisq set to German or English (since v1.3) |
| CoinTracking / Blockpit (aggregate import) | Broker/ |
must start with cointracking, blockpit or sammelimport; with nokyc in the file name everything in it counts as noKYC. Every account in the file becomes a wallet of its own, and the account name appears as the source in the tax evidence β so name it neutrally. Format derived from the documentation, not yet confirmed against real exports β the report warns, please check amounts, fees and time zone |
| noKYC purchases without CSV (Robosats, P2P, cash) | Project folder | manual_buys.csv β optional, create yourself |
| Private sales (P2P without broker) | Project folder | manual_sales.csv β optional, create yourself |
Your data stays local
The folders bitbox/ and Broker/ as well as the files
manual_buys.csv and manual_sales.csv are listed in
.gitignore β they will never accidentally be checked into Git
or put online. Your financial data stays on your machine.
noKYC Purchases and Private Sales
Anyone who buys BTC without a KYC broker β via Robosats, HodlHodl, directly person-to-person or for cash β
has no broker CSV (the tool reads Bisq exports directly, see the table above). For this there is manual_buys.csv: a simple text file
in the project folder where you enter each purchase with date, BTC amount and EUR amount.
date,btc_amount,eur_amount,note,kyc 2024-07-22,0.00500000,280.00,Robosats trade, 2024-11-05,0.00250000,148.50,P2P cash, 2024-12-01,0.00200000,190.00,Coinbase purchase (broker without parser),ja
The kyc column is optional. Empty means noKYC (default). With ja you mark a purchase
from a broker that has no import of its own yet: it then counts towards the KYC holdings and appears in the report for the tax office.
Same principle for private sales without a broker: manual_sales.csv with the same first four columns. The optional fifth column is called no_kyc there and works the other way round: ja = sale from the noKYC holdings, internal report only. Empty = normal sale, appears in the report for the tax office.
The tool reads both files automatically and processes them like any other purchase or sale.
On the legal situation: A tax report with correct acquisition costs is usually enough. You do not have to hand over wallet addresses unprompted. In individual cases, however, the tax office can request addresses, transaction hashes and holdings (BMF letter of 06.03.2025, para. 101, 104). So keep your records, including for P2P purchases.
β‘ Update October 2026 (v1.5): maintenance release
- Desktop: browser engine updated to Electron 44.7.2 (Chromium 152) β with the fixes backported since 44.5.1 from Chromium and its components (including V8, ANGLE, Skia, PDFium, WebRTC). The app still has no network access.
- Android: menus and dialogs show Android's slim scrollbar instead of a fixed neon bar; the neon bar now appears only with a mouse or trackpad.
- Version number in small print at the bottom of the app; the web version shows the date of its current state.
- Desktop: links (donations, online guide, bug report by e-mail) stay clickable even after the system clock has been set back.
- Bug report: long file type names wrap in the selection menu on narrow screens.
- Check before upload: the release script only uploads the APK and the Flatpak if their source files match the published Git tag, both are unchanged since the final check, and the APK is signed with the release certificate and the Flatpak checksum with the GPG release key.
β‘ Update October 2026 (v1.4): Sparrow, Electrum, Trezor, Ledger β and error messages that get through
- Wallet exports from Sparrow, Electrum, Trezor Suite and Ledger Wallet (formerly Ledger Live): the tool detects the program from the header line, older versions included. Every wallet is a holding of its own, in Ledger every account; transfers are matched by transaction ID. In the documents for the tax office they are only called "Sparrow-Wallet", "Trezor-Wallet 1" and so on. The formats are derived from the programs' source code, not from real exports β the tax report says so for every file. Other hardware wallets (e.g. Coldcard, Jade, SeedSigner) are added to Sparrow and exported there.
- Error messages get through: since version 1.2, the web version and the apps showed only "Files or classification changed during the calculation" on every abort. Now the actual message appears.
- Stricter noKYC separation: if noKYC coins arrive in a KYC wallet, the calculation now stops in more cases (fee missing in the export, wallets with the same name, pooled withdrawal, gift to your own wallet). A year with noKYC activity only no longer produces an empty tax report. Wallets loaded twice (yearly and full export, the same wallet in the BitBoxApp and Sparrow) are detected (abort if they share an outgoing transaction, otherwise a notice).
- Diagnosis for bug reports: in the "Report a bug" dialog the app can create a diagnosis β detected file types, the structure of the first line of unrecognized files and the messages of the last calculation, without file names, amounts, exact dates, addresses or wallet names (years and line numbers remain). You see all of it before sending.
β‘ Update October 2026 (v1.3): Hardened Parsers, Bisq in English, Aggregate Import
An audit of the CSV parsers found a silent failure: a renamed column or a byte-order mark at the start of the file (as Excel writes it when saving as "CSV UTF-8") turned a column into an unnoticed blank β whole Pocket, Bison or Swissquote files dropped out of the calculation without a word, Strike purchases showed up with 0 BTC. Since version 1.3 such a mark is skipped, and the tool checks every file for its required columns before reading: if one is missing, it stops with file, line and column instead of quietly carrying on.
- Loud instead of quiet: rows with the wrong number of fields, an unclosed quotation mark (which used to swallow the rest of the file), timestamps without a time zone, ambiguous numbers such as "2,500", negative transfer amounts and empty currency columns are now hard errors with a clear message.
- Bisq in German or English: the Bisq export is read in both interface languages, including the date forms of the common English regional settings (UK, US, Canada, Australia, India, New Zealand, Denmark, Finland and others); an unknown form stops with a message instead of being misread. Trades on altcoin markets (XMR/BTC for instance) are reported as a disposal or acquisition of BTC (without valuing them) instead of merely being counted as skipped.
- Aggregate import from CoinTracking/Blockpit: if you already keep your exchanges there, load an export; every account in it becomes a wallet of its own. The formats come from the documentation and published sample files, not from real exports β the report shows a warning. Swaps into other cryptocurrencies, inflows (mining, rewards, staking) and payments are only taken over with a euro value from the export; if it is missing, the tool reports them instead of estimating. Gifted coins are always reported, because the donor's acquisition data applies to them.
- Byte-identical: the CSV files from the app and the web version are now identical to those of the command line.
Your broker is missing? Send the header line of the export plus one row of made-up values to kontakt@alien-investor.org β a real export is the release criterion for every new parser.
β‘ Update October 2026: FiFo per Wallet β as the BMF Circular Requires
The German Federal Ministry of Finance circular of 6 March 2025 states in para. 62: a wallet-based view applies. So FiFo does not run across all holdings together, but per wallet. Until now the tool calculated one pooled holding across all sources. Since version 1.2 every BitBox wallet is a FiFo holding of its own, and the tool treats every broker account as a wallet of its own as well (paras. 20 and 90 name the trading platform as a separate unit) β the only method used in the official documents, no toggle.
- Transfers carry their history: on a transfer between your own wallets, the sending wallet gives up its oldest units; purchase date and cost travel into the receiving wallet and are sorted in there by purchase date. The transfer itself remains a non-disposal.
- Automatic matching: outgoing and incoming transfers are linked by transaction ID, otherwise by amount (with or without fee) and time within 48 hours. Ambiguous cases are not linked but reported; special cases such as pooled withdrawals are solved with the optional file
transfer_zuordnung.csv. - Nothing silently becomes a sale: an outgoing transfer without a matching incoming one counts as a transfer to one of your wallets that was not loaded, and is reported. So: load the exports of all your wallets.
- The reports explain the method: tax report and tax evidence state the method as FiFo per wallet under paras. 61 f. and list the transfers between your own wallets; the tax report additionally shows the holdings per wallet at year end. Your BitBox wallets appear there only as "BitBox-Wallet", with several wallets "BitBox-Wallet 1", "BitBox-Wallet 2" β¦ β file names and real labels appear solely in the new internal file
wallet_abgleich_intern_YEAR.txt, together with the comparison to the former pooled calculation. - What may change: depending on which units sat in which wallet, a sale may now be matched to a different purchase than before β with a different holding period and gain. With my own data the differences were single-digit euro amounts per year; yours may look different. The internal comparison file shows the difference per year.
How acquisition data travels on a transfer is not spelled out in the BMF circular. The tool chooses the obvious reading β the sending wallet gives up units by FiFo, purchase date and cost are preserved β and discloses it in the evidence document. No tax advice.
β‘ Update August 2026: Fees Paid in Bitcoin and Gifts Are Now Included
Until now the tool treated transfers between your own wallets as tax-neutral, full stop. That is true for the transferred holdings β but not for the network fee you pay in bitcoin along the way. You hand it over in exchange for a service, and under the logic of the German Federal Ministry of Finance circular of 6 March 2025 (paras. 33, 54, 60) that is a small disposal of the fee portion. The tool now books it β which also means the reported holdings finally match your wallet.
- Fees in BTC: miner fees on BitBox sends (including wallet-internal ones), 21bitcoin withdrawal fees, Bisq trading fees β each is matched to a lot via FiFo, valued at the daily closing price and listed with gain/loss in the report (section "GEBΓHREN IN BITCOIN"). The gain counts towards the exemption threshold. In practice this amounts to cents to a few euros per year.
- Prices offline: the daily closing prices (Bitstamp BTC/EUR since 2017) ship as a table inside the tool. No price service is queried β your transaction data still never leaves the browser, and a report generated tomorrow is identical to one generated today.
- Gifts and donations: put a word like "Spende", "Geschenk", "donation" or "gift" into the transaction note in the BitBox app. The tool then books it as a gratuitous transfer: no disposal, no gain β but the coins leave your holdings, and the report shows purchase date and cost basis per lot, because both carry over to the recipient (Sec. 23 (1) sentence 3 EStG).
- Transparency: the tax evidence document explains the method itself, citing the paragraph numbers. Anyone who prefers a different treatment of the fees can see exactly which lines are affected.
Why no tax-advisor opinion on this? Because there is no explicit administrative statement and no court ruling β only the logic of the BMF circular and the practice of the major crypto tax tools, which do the same. The tool picks the traceable, documented method and lays it open. This is not tax advice.
β‘ Update June 2026: Bisq Direct Import + Strict noKYC Separation
The tool now supports direct import of the Bisq trade history CSV. Simply place the export from Bisq Classic ("Portfolio β History β Export to CSV"; since v1.3 with Bisq set to German or English) as bisq.csv in the Broker/ folder β the tool automatically recognises all completed purchases.
- Bisq direct import: No more manual entry. The parser reads price, BTC amount, EUR amount and fees directly from the Bisq export.
- Dedicated noKYC file: noKYC purchases do not appear in any tax-office document. They go into a separate file
nokyc_intern_YEAR.txtβ with a clear warning header at the top so it can never accidentally be handed in along with the official documents. - Separate FiFo pools: KYC and noKYC holdings live in two strictly separated FiFo pools. A broker sale can never be matched against a Bisq purchase as its cost basis β the official proof document is guaranteed to stay internally consistent.
- noKYC wallets: Running a dedicated wallet just for noKYC sats? Place its CSV in the subfolder
bitbox/nokyc/β all movements appear exclusively in the internal report. - noKYC sales:
manual_sales.csvnow has an optionalno_kyccolumn. P2P sales from your noKYC holdings are calculated internally with FiFo matching, holding period and gain β but only ever appear in the internal file.
Also: the holding period is now calculated calendar-exact (anniversary date instead of a rigid 365-day count) β correct across leap years too.
For Robosats, HodlHodl and other P2P purchases without a CSV export, manual_buys.csv remains the right approach β also with noKYC separation.
Bisq is currently the only noKYC platform with a dedicated parser. The tool is MIT-licensed β anyone wanting to contribute an importer for Robosats, HodlHodl or other platforms is very welcome.
5. Your First Report (CLI)
Output a report for a specific year:
.venv/bin/python src/main.py --year 2024
All years at once:
.venv/bin/python src/main.py --all
With formal documentation for your accountant or tax office:
.venv/bin/python src/main.py --year 2024 --csv --nachweis
The result lands in reports/: a readable tax report, a print-ready
formal proof of calculation and optionally CSV tables for Excel.
6. What You Submit to the Tax Office (Web, App & CLI)
The proof alone is not enough. The tax office wants to know that the purchase data is genuine. So always submit two parts:
- The tax proof β what the tool generates (
steuernachweis_YEAR.txt). - The original broker CSVs β as evidence that the purchases actually took place.
The tool shows in the proof under "Source" exactly which broker provided which lot β so you can trace every single sale without gaps.
7. What This Tool Is Not
Not a replacement for a tax advisor. If you have taxable gains, have the results reviewed β especially in the first year. The tool calculates correctly according to FiFo, but you are responsible for the accuracy of your inputs.
What it is: a tool that informs and prepares you β instead of leaving you blindly trusting a cloud app you don't control.
As an app: Android and Linux desktop
Since version 1.0 (2 October 2026) the tax tool is also available as an app for Android (GrapheneOS) and as a Flatpak for the Linux desktop β free and open source like everything else. Interface and calculation core are the same as in the web version, with one difference: after installation the app downloads nothing. Pyodide, the calculation core, the ECB exchange rates and the BTC daily closing prices are built in. The reports are byte-identical to those of the CLI tool.
- Android: no internet permission (the only permission in the manifest is the AndroidX-generated signature permission of the app itself, which grants nothing), screenshots and app switcher blocked (FLAG_SECURE), no backup of app data. You pick CSVs and ZIPs with the file picker and save reports through the Android save dialog.
- Linux desktop: Flatpak without network permission and without file-system access β files come in through the file dialog or drag & drop and go out through the save dialog.
- In-app guide behind the β?β button (German and English). The reports stay in German, they are meant for the German tax office.
Android: how to get it
Deliberately not on the Google Play Store, but as signed releases from our own download address api.alien-investor.org/downloads/steuertool/. Easiest via Obtainium, which reports every update automatically (setup in the sheet below), or from the Zapstore, the app store on Nostr. Without either: download the APK from the download page and install it. Every release is mirrored on GitHub.
Set up Obtainium (values to copy)
On a phone with Obtainium installed it takes one tap; all settings are prefilled and Obtainium asks before adding:
Open in Obtainium
Or by hand, in Obtainium :
-
https://api.alien-investor.org/downloads/steuertool/ - Under :
btc-steuertool-([0-9]+(\.[0-9]+)+)\.apk$ -
$1 -
26:C5:E0:94:A8:B7:65:0C:A6:65:01:4D:0C:3A:8B:A6:82:B1:7C:05:F5:3C:6C:DE:F0:55:D0:7C:E9:84:BF:0B - Tap the to add, then install. Obtainium reports updates automatically.
Why the RegEx? On a download page Obtainium reads the version number from the file name only with it. Without it, Obtainium only has a substitute identifier and cannot compare against the installed version. The certificate hash is a hard lock: Obtainium will not install an APK signed with a different key.
Signing fingerprint (SHA-256) to verify authenticity with AppVerifier, identical for every version:
26:C5:E0:94:A8:B7:65:0C:A6:65:01:4D:0C:3A:8B:A6:82:B1:7C:05:F5:3C:6C:DE:F0:55:D0:7C:E9:84:BF:0B
apksigner: 26c5e094a8b7650ca665014d0c3a8ba682b17c05f53c6cdef055d07ce984bf0b
For the Linux desktop
The same interface, packaged with Electron as a Flatpak (x86_64). The file is in the
GitHub release, together with
SHA256SUMS and the signature SHA256SUMS.asc. The same three files are on our
own download address.
Not on Flathub, no automatic update. Step by step: Flatpak guide (for all desktop apps).
Requirement: Flatpak with the Flathub remote (for the runtime org.freedesktop.Platform 25.08, which Flatpak fetches during installation):
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
1. Download three files: btc-steuertool-1.5-linux-x86_64.flatpak, SHA256SUMS, SHA256SUMS.asc.
2. Check the signature. The checksum file is signed with the Alien Investor GPG release key β the same key as for Alien Pass, Sachwert-Tresor and Alien Notes: alien-investor-release-key.asc. Compare the fingerprint through a second channel:
100F 9E25 BFAE A807 DBC3 57D7 50C0 D785 83BF CB81
gpg --import alien-investor-release-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS # "Good signature from "Alien Investor (Release-Signatur) β¦""
sha256sum -c SHA256SUMS # "β¦flatpak: OK"
3. Install and start:
flatpak install --user btc-steuertool-1.5-linux-x86_64.flatpak
flatpak run org.alieninvestor.steuertool
Update: a new bundle cannot be installed over the existing one (Flatpak 1.14). Download and check the new version, then
flatpak uninstall --user org.alieninvestor.steuertool and install again. The app stores no tax data, only language and theme.
Check for yourself that the app has no network:
flatpak info --user --show-permissions org.alieninvestor.steuertool
Expected, exactly:
[Context]
shared=ipc;
sockets=wayland;fallback-x11;
devices=dri;
No network, no filesystem.
Limits, stated honestly
- Internal review, not an independent audit. Before the release an internal security audit covered the app shells and the latest changes to the calculation core; all findings were fixed before publication.
- No screenshot protection on the desktop β only the Android app blocks screenshots.
- Rate tables have a cut-off date. If a purchase or sale in USD/CHF lies after the end of the built-in ECB table, the calculation stops with an explanation instead of using an old rate. For fees paid in bitcoin after the end of the BTC price table (as of v1.2: 14 August 2026) the report marks the gain as βnot determinedβ and warns. New rates come with an app update, at the latest in January for the past year.
- No tax advice. The app calculates; responsibility for the tax return stays with you.
Web version, app or CLI β which fits you?
The web version is free: no access link, no account, no paywall. Open api.alien-investor.org/steuertool/, drop your CSV exports, done. Fictional sample data is included to try it out. Since 2 October 2026 there is no paid access any more β the tool is free in every form.
Frankly: the web version fetches its code from my server on every visit. So you have to trust that exactly the published code is sitting there β you can verify it, but it is not convenient. If you want maximum control, use the CLI from the repo: once cloned it runs fully offline, with bundled exchange-rate tables and not a single network connection. In between sit the apps for Android and the Linux desktop: installed and verified once (signing fingerprint or GPG-signed checksum), then offline. All three run the same code.
β‘ Open the web version π Guide
What the server sees when you open the app β and what it does not: privacy notice for the app.
Open Source on GitHub
The complete code is public, free and open-source. You can verify how every line of the calculation works. Fictional sample data for all supported brokers is included as well β so you can test the tool before loading your real data.