
Crypto Tax Export
- 198 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
crypto-tax-export is a Claude Code skill that exports trade history and tax calculations in formats compatible with Koinly, CoinTracker, CoinLedger, TokenTax, and IRS Form 8949.
About
crypto-tax-export is a Claude Code skill that exports crypto trade history and tax calculations in formats for Koinly, CoinTracker, CoinLedger, TokenTax, TurboTax/TaxAct, and IRS Form 8949. It classifies Solana-specific transactions (Jupiter swaps, LP operations, staking, airdrops, migrations) and excludes failed transactions. A developer uses it when building tax-reporting output for a crypto trading system. It supports FIFO, LIFO, HIFO, and Specific ID cost-basis methods.
- Exports CSVs for Koinly, CoinTracker, CoinLedger, TokenTax, TurboTax/TaxAct
- Generates IRS Form 8949 Part I and Part II with columns a through h
- Classifies Solana tx types: Jupiter swaps, LP ops, staking, airdrops, migrations, failed tx
Crypto Tax Export by the numbers
- 198 all-time installs (skills.sh)
- Ranked #474 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
crypto-tax-export capabilities & compatibility
Free for export; on-chain reconciliation optionally needs a Solana RPC or Helius API key.
- Capabilities
- crypto tax export · form 8949 generation · transaction classification · cost basis calculation · csv export
- Use cases
- trading · data analysis · documentation
- Runs
- Runs locally
- Pricing
- Free
What crypto-tax-export says it does
Export trade history and tax calculations in formats compatible with Koinly, CoinTracker, CoinLedger, TokenTax, and IRS Form 8949
Failed Solana transactions (status: failed, or inner instruction errors) are **not** taxable events.
This skill provides data formatting and calculation tools only. It does not constitute tax advice.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill crypto-tax-exportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 198 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Export crypto trade history and tax calculations as CSVs for tax software and IRS Form 8949.
Who is it for?
Producing tax-software-ready CSVs and IRS Form 8949 output from Solana trade history.
Skip if: Providing tax advice; the skill states it is data-formatting only and to consult a tax professional.
When should I use this skill?
You need to export crypto trades and computed gains into tax software or IRS Form 8949 format.
What you get
Valid per-tool CSV exports and Form 8949 Parts I/II generated from classified Solana trade history.
By the numbers
- Six export formats plus IRS Form 8949
- Four cost-basis methods (FIFO, LIFO, HIFO, Specific ID)
- Ships demo CSVs for CoinTracker, Koinly, and Form 8949
Files
Crypto Tax Export
Export trade history and tax calculations for tax software and IRS filing. Handles Solana-specific transaction types (Jupiter swaps, LP operations, staking rewards, airdrops) and generates CSVs compatible with Koinly, CoinTracker, CoinLedger, TokenTax, TurboTax/TaxAct, and IRS Form 8949.
Disclaimer: This skill provides data formatting and calculation tools only. It does not constitute tax advice. Consult a qualified tax professional for guidance on reporting cryptocurrency transactions. Tax laws vary by jurisdiction and change frequently.
Prerequisites
- Python 3.10+
- Trade history data (from internal trade journal, on-chain history, or exchange exports)
- For on-chain reconciliation: Solana RPC or Helius API access (see
helius-apiskill) - For cost basis: historical price data (see
birdeye-apiorcoingecko-apiskills)
Capabilities
| Capability | Description |
|---|---|
| Multi-format CSV export | Koinly, CoinTracker, CoinLedger, TokenTax, TurboTax, TaxAct |
| IRS Form 8949 generation | Part I (short-term) and Part II (long-term), columns a through h |
| Solana tx classification | Jupiter swaps, multi-hop routes, LP deposits/withdrawals, staking, airdrops |
| Cost basis methods | FIFO, LIFO, HIFO, Specific Identification |
| Reconciliation | Match on-chain history against internal trade journal |
| Failed tx handling | Identify and exclude failed transactions (no taxable event) |
Supported Export Formats
Koinly CSV
Koinly expects a universal import format with these columns:
Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Fee Currency,Net Worth Amount,Net Worth Currency,Label,Description,TxHashLabels: swap, staking, airdrop, liquidity_in, liquidity_out, cost, gift, lost.
CoinTracker CSV
Date,Received Quantity,Received Currency,Sent Quantity,Sent Currency,Fee Amount,Fee Currency,TagTags: trade, staking_reward, airdrop, lp_deposit, lp_withdrawal.
CoinLedger CSV
Date (UTC),Type,Received Currency,Received Amount,Sent Currency,Sent Amount,Fee Currency,Fee Amount,Exchange/WalletTypes: Trade, Income, Gift Received, Mining, Staking Reward, Airdrop.
TokenTax CSV
Type,BuyAmount,BuyCurrency,SellAmount,SellCurrency,FeeAmount,FeeCurrency,Exchange,Group,Comment,DateTypes: Trade, Income, Staking, Airdrop, Spending.
TurboTax / TaxAct CSV
Both accept a simplified Form 8949 format:
Description of Property,Date Acquired,Date Sold,Proceeds,Cost Basis,Gain or LossIRS Form 8949
Part I — Short-term (held one year or less). Part II — Long-term (held more than one year).
Columns:
- (a) Description of property (e.g., "2.5 SOL")
- (b) Date acquired (MM/DD/YYYY)
- (c) Date sold or disposed of (MM/DD/YYYY)
- (d) Proceeds (sale price in USD)
- (e) Cost or other basis (purchase price in USD + fees)
- (f) Code, if any (per IRS instructions)
- (g) Adjustment amount
- (h) Gain or loss (d minus e, adjusted by g)
Check box: (A) if basis reported to IRS, (B) if not, (C) if Form 1099-B not received.
Solana Transaction Classification
Jupiter Swaps (Single-Hop)
A direct token-to-token swap. Classified as a disposal of the sent token and acquisition of the received token. Each side is a taxable event.
tx = {
"type": "swap",
"sent": {"amount": 1.5, "currency": "SOL", "usd_value": 225.00},
"received": {"amount": 50000, "currency": "BONK", "usd_value": 224.50},
"fee": {"amount": 0.000005, "currency": "SOL", "usd_value": 0.00075},
"timestamp": "2025-03-15T14:30:00Z",
"tx_hash": "5abc...def",
}Jupiter Swaps (Multi-Hop)
A routed swap through intermediate tokens (e.g., SOL -> USDC -> BONK). Only the initial send and final receive matter for tax purposes. Intermediate hops are not separate taxable events.
LP Deposits / Withdrawals
- Deposit: Sending tokens to an LP is generally treated as a disposal at fair market value.
- Withdrawal: Receiving tokens from an LP is an acquisition at fair market value.
- LP tokens received/burned may be tracked for cost basis continuity.
Staking Rewards
Staking rewards (SOL validator rewards, liquid staking yield) are income at fair market value when received. Cost basis equals the FMV at receipt.
Airdrops
Airdrops are income at fair market value when the recipient gains dominion and control. Some jurisdictions differ on when dominion is established.
Token Migrations
A 1:1 token migration (e.g., protocol upgrade) is generally not a taxable event. The new token inherits the cost basis and holding period of the old token.
Failed Transactions
Failed Solana transactions (status: failed, or inner instruction errors) are not taxable events. The transaction fee (SOL) may still be deductible as a cost of doing business in some jurisdictions. Always exclude failed txs from trade export.
Cost Basis Methods
from enum import Enum
class CostBasisMethod(Enum):
FIFO = "fifo" # First In, First Out (IRS default)
LIFO = "lifo" # Last In, First Out
HIFO = "hifo" # Highest In, First Out (minimizes gains)
SPEC_ID = "spec_id" # Specific Identification (requires lot tracking)
def compute_gain(
proceeds: float,
cost_basis: float,
adjustments: float = 0.0,
) -> float:
"""Compute gain or loss for Form 8949 column (h)."""
return proceeds - cost_basis + adjustmentsReconciliation
Reconciling on-chain history with an internal trade journal catches:
1. Missing trades — on-chain tx not in journal (manual entry missed) 2. Phantom trades — journal entry with no matching on-chain tx 3. Amount mismatches — journal amount differs from on-chain amount 4. Duplicate entries — same tx recorded twice
Reconciliation workflow:
def reconcile(
journal_trades: list[dict],
onchain_txs: list[dict],
tolerance: float = 0.001,
) -> dict:
"""Match journal entries to on-chain transactions.
Args:
journal_trades: Internal trade records with tx_hash field.
onchain_txs: Parsed on-chain transactions.
tolerance: Relative tolerance for amount matching.
Returns:
Dict with matched, missing_onchain, missing_journal,
mismatched lists.
"""
onchain_by_hash = {tx["tx_hash"]: tx for tx in onchain_txs}
matched, missing_onchain, missing_journal, mismatched = [], [], [], []
for trade in journal_trades:
tx = onchain_by_hash.pop(trade.get("tx_hash", ""), None)
if tx is None:
missing_onchain.append(trade)
elif abs(trade["amount"] - tx["amount"]) / max(tx["amount"], 1e-9) > tolerance:
mismatched.append({"journal": trade, "onchain": tx})
else:
matched.append({"journal": trade, "onchain": tx})
missing_journal = list(onchain_by_hash.values())
return {
"matched": matched,
"missing_onchain": missing_onchain,
"missing_journal": missing_journal,
"mismatched": mismatched,
"summary": {
"total_journal": len(journal_trades),
"total_onchain": len(onchain_txs),
"matched": len(matched),
"missing_onchain": len(missing_onchain),
"missing_journal": len(missing_journal),
"mismatched": len(mismatched),
},
}Quick Start
from scripts.tax_exporter import (
generate_demo_trades,
export_koinly_csv,
export_form_8949_csv,
)
# Generate sample trades
trades = generate_demo_trades()
# Export to Koinly format
export_koinly_csv(trades, "koinly_import.csv")
# Export to Form 8949 format
export_form_8949_csv(trades, "form_8949.csv")Run the demo directly:
python scripts/tax_exporter.py --demoUse Cases
End-of-Year Tax Filing
Export all trades from your journal, reconcile with on-chain history, generate Form 8949 line items, and import into TurboTax or hand to your CPA.
Tax-Loss Harvesting Review
Export with HIFO cost basis method to identify positions with unrealized losses that could offset gains before year-end.
Multi-Platform Consolidation
Combine trades from multiple wallets and DEXs into a single Koinly or CoinTracker import file for unified portfolio tax reporting.
Audit Preparation
Use reconciliation to verify completeness of your trade records against on-chain history. Produce a clean Form 8949 with supporting transaction hashes.
Files
| File | Description |
|---|---|
references/planned_features.md | Export format specs, Form 8949 mapping, Solana tx classification, reconciliation methodology |
scripts/tax_exporter.py | Demo script: generate trades, export to Koinly CSV and Form 8949 CSV, show format differences |
Related Skills
helius-api— Fetch and parse Solana transaction history for reconciliationbirdeye-api/coingecko-api— Historical price data for cost basis lookupssolana-onchain— Wallet transaction analysis and classificationtrade-journal— Internal trade record keeping
Date,Received Quantity,Received Currency,Sent Quantity,Sent Currency,Fee Amount,Fee Currency,Tag
01/15/2025 10:00:00,3.33,SOL,500,USDC,0.000005,SOL,trade
02/14/2025 10:00:00,1500000,BONK,1,SOL,0.000005,SOL,trade
03/01/2025 10:00:00,0.05,SOL,,,,SOL,staking_reward
03/16/2025 10:00:00,1000,JUP,,,,SOL,airdrop
04/15/2025 10:00:00,1,SOL-USDC-LP,1,SOL,0.000005,SOL,lp_deposit
08/13/2025 10:00:00,1.05,SOL,1,SOL-USDC-LP,0.000005,SOL,lp_withdrawal
07/14/2025 10:00:00,200,USDC,1500000,BONK,0.000005,SOL,trade
04/20/2026 10:00:00,5.5,SOL,1000,JUP,0.000005,SOL,trade
--- Part I: Short-Term Capital Gains and Losses ---
(a) Description of Property,(b) Date Acquired,(c) Date Sold,(d) Proceeds,(e) Cost Basis,(f) Code,(g) Adjustment,(h) Gain or Loss,Holding Period,Check Box
500 USDC,01/15/2025,01/15/2025,500.00,500.00,,0.00,0.00,0 days,(B)
1 SOL,01/15/2025,02/14/2025,165.00,150.15,,0.00,14.85,30 days,(B)
1500000 BONK,02/14/2025,07/14/2025,200.00,165.00,,0.00,35.00,150 days,(B)
--- Part II: Long-Term Capital Gains and Losses ---
(a) Description of Property,(b) Date Acquired,(c) Date Sold,(d) Proceeds,(e) Cost Basis,(f) Code,(g) Adjustment,(h) Gain or Loss,Holding Period,Check Box
1000 JUP,03/16/2025,04/20/2026,1100.00,850.00,,0.00,250.00,400 days,(B)
Summary
Short-term transactions: 3
Short-term net gain/loss: $49.85
Long-term transactions: 1
Long-term net gain/loss: $250.00
Total net gain/loss: $299.85
Cost basis method: FIFO
Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Fee Currency,Net Worth Amount,Net Worth Currency,Label,Description,TxHash
2025-01-15 10:00:00,500,USDC,3.33,SOL,0.000005,SOL,500.0,USD,swap,Buy SOL with USDC via Jupiter,4xKm9rTPqGn2b7VfZ8jHdR1nQwEsXpYcLmA3kF5vG6t
2025-02-14 10:00:00,1,SOL,1500000,BONK,0.000005,SOL,165.0,USD,swap,Swap SOL for BONK (multi-hop via USDC),5yLn0sUPrHo3c8WgA9kIeS2oRxFtYqDmB4nC6lG7wH8u
2025-03-01 10:00:00,,,0.05,SOL,,SOL,8.25,USD,staking,Staking reward from Marinade mSOL,6zMo1tVQsIp4d9XhB0lJfT3pSyGuZrEnC5oD7mH8xI9v
2025-03-16 10:00:00,,,1000,JUP,,SOL,850.0,USD,airdrop,JUP airdrop Season 2,7aNp2uWRtJq5e0YiC1mKgU4qTzHvAsF0D6pE8nI9yJ0w
2025-04-15 10:00:00,1,SOL,1,SOL-USDC-LP,0.000005,SOL,175.0,USD,liquidity_in,Add liquidity to SOL-USDC pool,8bOq3vXStKr6f1ZjD2nLhV5rUaIwBtG1E7qF9oJ0zK1x
2025-08-13 10:00:00,1,SOL-USDC-LP,1.05,SOL,0.000005,SOL,199.5,USD,liquidity_out,Remove liquidity from SOL-USDC pool,9cPr4wYTuLs7g2AkE3oMiW6sVbJxCuH2F8rG0pK1aL2y
2025-07-14 10:00:00,1500000,BONK,200,USDC,0.000005,SOL,200.0,USD,swap,Sell BONK for USDC via Jupiter,0dQs5xZUvMt8h3BlF4pNjX7tWcKyDvI3G9sH1qL2bM3z
2026-04-20 10:00:00,1000,JUP,5.5,SOL,0.000005,SOL,1100.0,USD,swap,Sell JUP for SOL (long-term holding),1eRt6yAVwNu9i4CmG5qOkY8uXdLzEwJ4H0tI2rM3cN4a
Crypto Tax Export — Planned Features Reference
Export Format Specifications
Koinly Universal CSV
Koinly accepts a universal CSV format for importing transactions from any source.
Required columns:
Date— ISO 8601 orYYYY-MM-DD HH:MM:SS UTCSent Amount— quantity sent (blank for income)Sent Currency— ticker symbolReceived Amount— quantity received (blank for disposal-only)Received Currency— ticker symbol
Optional columns:
Fee Amount/Fee Currency— transaction feeNet Worth Amount/Net Worth Currency— USD value at time of tx (helps Koinly price lookups)Label— transaction classificationDescription— free text noteTxHash— on-chain transaction signature
Koinly labels: swap, staking, airdrop, liquidity_in, liquidity_out, cost, gift, lost, fork, margin_fee, realized_gain.
Notes:
- For swaps, populate both Sent and Received columns.
- For income (staking, airdrops), populate only Received columns.
- For expenses/losses, populate only Sent columns.
- Koinly auto-detects duplicates by TxHash.
CoinTracker CSV
Columns: Date, Received Quantity, Received Currency, Sent Quantity, Sent Currency, Fee Amount, Fee Currency, Tag.
Tags: trade, staking_reward, airdrop, lp_deposit, lp_withdrawal, payment, gift.
Date format: MM/DD/YYYY HH:MM:SS (US format).
Notes:
- CoinTracker uses tags to classify income vs trades.
- Fees should be in the same currency as the sent side when possible.
CoinLedger CSV
Columns: Date (UTC), Type, Received Currency, Received Amount, Sent Currency, Sent Amount, Fee Currency, Fee Amount, Exchange/Wallet.
Types: Trade, Income, Gift Received, Mining, Staking Reward, Airdrop, Spending, Lost/Stolen.
Date format: YYYY-MM-DD HH:MM:SS.
Notes:
- The
Exchange/Walletfield should identify the source (e.g., "Jupiter", "Raydium", "Phantom Wallet"). - CoinLedger pairs each trade row into buy/sell internally.
TokenTax CSV
Columns: Type, BuyAmount, BuyCurrency, SellAmount, SellCurrency, FeeAmount, FeeCurrency, Exchange, Group, Comment, Date.
Types: Trade, Income, Staking, Airdrop, Spending, Gift, Lost, Mining.
Date format: YYYY-MM-DD HH:MM:SS UTC.
Group field: Optional grouping label for related transactions.
TurboTax / TaxAct CSV
Both accept a simplified Form 8949-style CSV.
Columns: Description of Property, Date Acquired, Date Sold, Proceeds, Cost Basis, Gain or Loss.
Date format: MM/DD/YYYY.
Notes:
- Proceeds and Cost Basis are in USD.
- Gain or Loss = Proceeds - Cost Basis.
- Short-term and long-term should be in separate sections or files.
Form 8949 Field Mapping
Column Definitions
| Column | Field | Source | Notes |
|---|---|---|---|
| (a) | Description of property | "{amount} {currency}" | e.g., "2.5 SOL" |
| (b) | Date acquired | Trade journal / on-chain timestamp | MM/DD/YYYY |
| (c) | Date sold or disposed of | Disposal timestamp | MM/DD/YYYY |
| (d) | Proceeds | USD value at disposal | Sale price minus exchange fees |
| (e) | Cost or other basis | USD value at acquisition | Purchase price plus fees |
| (f) | Code | IRS code if applicable | Usually blank for crypto |
| (g) | Adjustment amount | Wash sale or other adjustments | Usually 0 |
| (h) | Gain or loss | (d) - (e) + (g) | Negative = loss |
Check Box Rules
- (A) Basis reported to IRS — Rarely applies to crypto (no 1099-B from DEXs).
- (B) Basis NOT reported to IRS — Most common for DEX trades.
- (C) Form 1099-B not received — Use when no 1099-B was issued.
Part I vs Part II
- Part I — Short-term: acquired and disposed within 365 days.
- Part II — Long-term: held for more than 365 days.
- Holding period starts the day after acquisition.
Solana Transaction Type Classification
Classification Rules
| Tx Pattern | Tax Type | Sent | Received | Label |
|---|---|---|---|---|
| Jupiter swap (single-hop) | Trade | Input token | Output token | swap |
| Jupiter swap (multi-hop) | Trade | First input | Final output | swap |
| LP deposit (add liquidity) | Disposal | Token A + Token B | LP token | liquidity_in |
| LP withdrawal (remove liquidity) | Acquisition | LP token | Token A + Token B | liquidity_out |
| Staking reward claim | Income | — | Reward token | staking |
| Airdrop receipt | Income | — | Airdrop token | airdrop |
| Token migration (1:1) | Non-taxable | Old token | New token | migration |
| Failed transaction | Non-taxable | — | — | (exclude) |
| NFT purchase | Trade | SOL/token | NFT | swap |
| Rent recovery (account close) | Non-taxable | — | SOL (rent) | cost |
Multi-Hop Swap Handling
Jupiter routes through intermediate tokens for better pricing. For tax: 1. Parse the swap instruction to find the initial input and final output. 2. Ignore intermediate token transfers within the same transaction. 3. Record as a single trade: input token -> output token. 4. Transaction fee is the SOL fee on the outer transaction.
Identifying Failed Transactions
A Solana transaction is failed if:
meta.erris notnullin the transaction response.- The transaction status is
Err. - Inner instructions may have executed partially but the overall tx reverted.
Failed transactions still consume SOL for fees. The fee is not automatically deductible but may be claimed as a business expense if trading is a business activity.
Reconciliation Methodology
Step 1: Collect Sources
- Journal trades: Internal records with timestamps, amounts, tx hashes.
- On-chain history: Fetched via Helius
getSignaturesForAddress+ parsed transactions.
Step 2: Primary Match (by tx hash)
Match journal entries to on-chain transactions by tx_hash / signature. This is the strongest match.
Step 3: Secondary Match (by timestamp + amount)
For journal entries without tx hashes, attempt fuzzy matching:
- Timestamp within 60-second window.
- Amount within 0.1% relative tolerance.
- Same token pair.
Step 4: Classify Unmatched
- Journal-only entries: Potentially from off-chain venues (CEX) or manual errors.
- On-chain-only entries: Missed journal entries — need to be added.
- Amount mismatches: Possible slippage discrepancies or partial fills.
Step 5: Generate Report
Output a reconciliation report with:
- Match rate percentage.
- List of unmatched entries with suggested actions.
- List of mismatched amounts with differences.
- Summary statistics (total trades, total volume, date range).
Planned Enhancements
- Wash sale detection: Flag disposals where the same or substantially identical asset was repurchased within 30 days (note: IRS has not issued definitive guidance on wash sale rules for crypto as of 2025, but proposed regulations may apply starting 2026).
- Multi-wallet aggregation: Combine exports across multiple Solana wallets into a single tax report.
- CEX import parsing: Ingest CSVs from Coinbase, Kraken, Binance and merge with on-chain data.
- Lot tracking UI: Visual tool for specific identification lot selection.
- Schedule D summary: Auto-generate Schedule D totals from Form 8949 line items.
- International formats: Support for HMRC (UK), ATO (Australia), CRA (Canada) reporting requirements.
#!/usr/bin/env python3
"""Crypto tax export demo: generate sample trades and export to multiple CSV formats.
Demonstrates exporting trade history to Koinly CSV and IRS Form 8949 CSV formats.
Includes sample Solana transaction types: swaps, staking rewards, airdrops,
LP operations, and failed transactions.
Usage:
python scripts/tax_exporter.py --demo
python scripts/tax_exporter.py --demo --format koinly
python scripts/tax_exporter.py --demo --format form8949
python scripts/tax_exporter.py --demo --format all
Dependencies:
None (uses Python stdlib only: csv, json, datetime, argparse)
Environment Variables:
None required for demo mode.
"""
import argparse
import csv
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta, timezone
from enum import Enum
from io import StringIO
from typing import Optional
# ── Enums & Data Models ─────────────────────────────────────────────
class TxType(Enum):
"""Solana transaction type classification for tax purposes."""
SWAP = "swap"
STAKING_REWARD = "staking"
AIRDROP = "airdrop"
LP_DEPOSIT = "liquidity_in"
LP_WITHDRAWAL = "liquidity_out"
MIGRATION = "migration"
FAILED = "failed"
class CostBasisMethod(Enum):
"""Supported cost basis calculation methods."""
FIFO = "fifo"
LIFO = "lifo"
HIFO = "hifo"
SPEC_ID = "spec_id"
@dataclass
class TaxableEvent:
"""A single taxable event derived from a trade or transaction.
Attributes:
timestamp: When the event occurred (UTC).
tx_type: Classification of the transaction.
sent_amount: Quantity of asset sent (0 for income events).
sent_currency: Ticker of asset sent.
received_amount: Quantity of asset received (0 for disposal-only).
received_currency: Ticker of asset received.
fee_amount: Transaction fee amount.
fee_currency: Transaction fee currency.
usd_proceeds: USD value of the disposal side.
usd_cost_basis: USD cost basis of the asset disposed.
date_acquired: When the disposed asset was originally acquired.
tx_hash: On-chain transaction signature.
exchange: Source platform or wallet label.
description: Human-readable note.
is_failed: Whether the transaction failed on-chain.
"""
timestamp: datetime
tx_type: TxType
sent_amount: float = 0.0
sent_currency: str = ""
received_amount: float = 0.0
received_currency: str = ""
fee_amount: float = 0.0
fee_currency: str = "SOL"
usd_proceeds: float = 0.0
usd_cost_basis: float = 0.0
date_acquired: Optional[datetime] = None
tx_hash: str = ""
exchange: str = "Jupiter"
description: str = ""
is_failed: bool = False
@property
def holding_days(self) -> Optional[int]:
"""Number of days the asset was held before disposal."""
if self.date_acquired is None:
return None
return (self.timestamp - self.date_acquired).days
@property
def is_long_term(self) -> Optional[bool]:
"""Whether this is a long-term holding (> 365 days)."""
days = self.holding_days
if days is None:
return None
return days > 365
@property
def gain_or_loss(self) -> float:
"""Compute gain or loss (Form 8949 column h)."""
return self.usd_proceeds - self.usd_cost_basis
# ── Demo Data Generation ────────────────────────────────────────────
def generate_demo_trades() -> list[TaxableEvent]:
"""Generate a realistic set of demo Solana trades for export testing.
Returns:
List of TaxableEvent objects covering various transaction types.
"""
base = datetime(2025, 1, 15, 10, 0, 0, tzinfo=timezone.utc)
trades: list[TaxableEvent] = []
# 1. Buy SOL with USDC (swap)
trades.append(TaxableEvent(
timestamp=base,
tx_type=TxType.SWAP,
sent_amount=500.00,
sent_currency="USDC",
received_amount=3.33,
received_currency="SOL",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=500.00,
usd_cost_basis=500.00,
date_acquired=base,
tx_hash="4xKm9rTPqGn2b7VfZ8jHdR1nQwEsXpYcLmA3kF5vG6t",
description="Buy SOL with USDC via Jupiter",
))
# 2. Swap SOL for BONK (short-term, 30 days later)
t2 = base + timedelta(days=30)
trades.append(TaxableEvent(
timestamp=t2,
tx_type=TxType.SWAP,
sent_amount=1.0,
sent_currency="SOL",
received_amount=1500000,
received_currency="BONK",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=165.00,
usd_cost_basis=150.15,
date_acquired=base,
tx_hash="5yLn0sUPrHo3c8WgA9kIeS2oRxFtYqDmB4nC6lG7wH8u",
description="Swap SOL for BONK (multi-hop via USDC)",
))
# 3. Staking reward received
t3 = base + timedelta(days=45)
trades.append(TaxableEvent(
timestamp=t3,
tx_type=TxType.STAKING_REWARD,
received_amount=0.05,
received_currency="SOL",
usd_proceeds=8.25,
usd_cost_basis=8.25,
date_acquired=t3,
tx_hash="6zMo1tVQsIp4d9XhB0lJfT3pSyGuZrEnC5oD7mH8xI9v",
exchange="Marinade",
description="Staking reward from Marinade mSOL",
))
# 4. Airdrop received
t4 = base + timedelta(days=60)
trades.append(TaxableEvent(
timestamp=t4,
tx_type=TxType.AIRDROP,
received_amount=1000,
received_currency="JUP",
usd_proceeds=850.00,
usd_cost_basis=850.00,
date_acquired=t4,
tx_hash="7aNp2uWRtJq5e0YiC1mKgU4qTzHvAsF0D6pE8nI9yJ0w",
exchange="Jupiter",
description="JUP airdrop Season 2",
))
# 5. LP deposit (Raydium SOL-USDC)
t5 = base + timedelta(days=90)
trades.append(TaxableEvent(
timestamp=t5,
tx_type=TxType.LP_DEPOSIT,
sent_amount=1.0,
sent_currency="SOL",
received_amount=1,
received_currency="SOL-USDC-LP",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=175.00,
usd_cost_basis=150.15,
date_acquired=base,
tx_hash="8bOq3vXStKr6f1ZjD2nLhV5rUaIwBtG1E7qF9oJ0zK1x",
exchange="Raydium",
description="Add liquidity to SOL-USDC pool",
))
# 6. LP withdrawal (120 days after deposit)
t6 = t5 + timedelta(days=120)
trades.append(TaxableEvent(
timestamp=t6,
tx_type=TxType.LP_WITHDRAWAL,
sent_amount=1,
sent_currency="SOL-USDC-LP",
received_amount=1.05,
received_currency="SOL",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=199.50,
usd_cost_basis=175.00,
date_acquired=t5,
tx_hash="9cPr4wYTuLs7g2AkE3oMiW6sVbJxCuH2F8rG0pK1aL2y",
exchange="Raydium",
description="Remove liquidity from SOL-USDC pool",
))
# 7. Sell BONK for USDC (short-term)
t7 = base + timedelta(days=180)
trades.append(TaxableEvent(
timestamp=t7,
tx_type=TxType.SWAP,
sent_amount=1500000,
sent_currency="BONK",
received_amount=200.00,
received_currency="USDC",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=200.00,
usd_cost_basis=165.00,
date_acquired=t2,
tx_hash="0dQs5xZUvMt8h3BlF4pNjX7tWcKyDvI3G9sH1qL2bM3z",
description="Sell BONK for USDC via Jupiter",
))
# 8. Sell JUP for SOL (long-term, > 365 days after airdrop)
t8 = t4 + timedelta(days=400)
trades.append(TaxableEvent(
timestamp=t8,
tx_type=TxType.SWAP,
sent_amount=1000,
sent_currency="JUP",
received_amount=5.5,
received_currency="SOL",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=1100.00,
usd_cost_basis=850.00,
date_acquired=t4,
tx_hash="1eRt6yAVwNu9i4CmG5qOkY8uXdLzEwJ4H0tI2rM3cN4a",
description="Sell JUP for SOL (long-term holding)",
))
# 9. Failed transaction (not taxable, but fee spent)
t9 = base + timedelta(days=200)
trades.append(TaxableEvent(
timestamp=t9,
tx_type=TxType.FAILED,
sent_amount=2.0,
sent_currency="SOL",
received_amount=0,
received_currency="",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=0,
usd_cost_basis=0,
tx_hash="2fSu7zBWxOv0j5DnH6rPlZ9vYeMAFxK5I1uJ3sN4dO5b",
description="Failed swap — slippage exceeded (NOT taxable)",
is_failed=True,
))
# 10. Token migration (non-taxable)
t10 = base + timedelta(days=250)
trades.append(TaxableEvent(
timestamp=t10,
tx_type=TxType.MIGRATION,
sent_amount=100,
sent_currency="TOKEN_V1",
received_amount=100,
received_currency="TOKEN_V2",
fee_amount=0.000005,
fee_currency="SOL",
usd_proceeds=0,
usd_cost_basis=0,
date_acquired=base,
tx_hash="3gTv8aCXyPw1k6EoI7sPmA0wZfNBGyL6J2vK4tO5eP6c",
description="Token migration v1->v2 (non-taxable, basis carries over)",
is_failed=False,
))
return trades
# ── Export Functions ─────────────────────────────────────────────────
def _filter_taxable(trades: list[TaxableEvent]) -> list[TaxableEvent]:
"""Exclude failed and migration transactions from taxable output.
Args:
trades: All trade events including non-taxable.
Returns:
Filtered list containing only taxable events.
"""
excluded = {TxType.FAILED, TxType.MIGRATION}
return [t for t in trades if t.tx_type not in excluded]
def _fmt_date_iso(dt: Optional[datetime]) -> str:
"""Format datetime as YYYY-MM-DD HH:MM:SS UTC."""
if dt is None:
return ""
return dt.strftime("%Y-%m-%d %H:%M:%S")
def _fmt_date_us(dt: Optional[datetime]) -> str:
"""Format datetime as MM/DD/YYYY for US tax forms."""
if dt is None:
return ""
return dt.strftime("%m/%d/%Y")
def _fmt_amount(value: float) -> str:
"""Format a numeric amount, returning empty string for zero."""
if value == 0.0:
return ""
return f"{value:.8f}".rstrip("0").rstrip(".")
def _fmt_usd(value: float) -> str:
"""Format a USD amount to 2 decimal places."""
return f"{value:.2f}"
def export_koinly_csv(
trades: list[TaxableEvent],
output_path: Optional[str] = None,
) -> str:
"""Export trades to Koinly universal CSV format.
Args:
trades: List of taxable events to export.
output_path: File path to write CSV. If None, returns CSV string.
Returns:
CSV content as a string.
"""
taxable = _filter_taxable(trades)
headers = [
"Date", "Sent Amount", "Sent Currency",
"Received Amount", "Received Currency",
"Fee Amount", "Fee Currency",
"Net Worth Amount", "Net Worth Currency",
"Label", "Description", "TxHash",
]
buf = StringIO()
writer = csv.writer(buf)
writer.writerow(headers)
for t in taxable:
label = t.tx_type.value
net_worth = t.usd_proceeds if t.usd_proceeds > 0 else ""
net_worth_currency = "USD" if net_worth else ""
writer.writerow([
_fmt_date_iso(t.timestamp),
_fmt_amount(t.sent_amount),
t.sent_currency,
_fmt_amount(t.received_amount),
t.received_currency,
_fmt_amount(t.fee_amount),
t.fee_currency,
net_worth,
net_worth_currency,
label,
t.description,
t.tx_hash,
])
content = buf.getvalue()
if output_path:
with open(output_path, "w", newline="") as f:
f.write(content)
print(f"Koinly CSV written to {output_path} ({len(taxable)} events)")
return content
def export_form_8949_csv(
trades: list[TaxableEvent],
output_path: Optional[str] = None,
cost_basis_method: CostBasisMethod = CostBasisMethod.FIFO,
) -> str:
"""Export trades to IRS Form 8949 CSV format.
Separates short-term (Part I) and long-term (Part II) disposals.
Only includes events where an asset was disposed (sent_amount > 0
and has a cost basis).
Args:
trades: List of taxable events to export.
output_path: File path to write CSV. If None, returns CSV string.
cost_basis_method: Cost basis method label for reporting.
Returns:
CSV content as a string.
"""
taxable = _filter_taxable(trades)
# Only disposals with cost basis info qualify for Form 8949
disposals = [
t for t in taxable
if t.sent_amount > 0
and t.date_acquired is not None
and t.tx_type == TxType.SWAP
]
headers = [
"(a) Description of Property",
"(b) Date Acquired",
"(c) Date Sold",
"(d) Proceeds",
"(e) Cost Basis",
"(f) Code",
"(g) Adjustment",
"(h) Gain or Loss",
"Holding Period",
"Check Box",
]
short_term = [t for t in disposals if not t.is_long_term]
long_term = [t for t in disposals if t.is_long_term]
buf = StringIO()
writer = csv.writer(buf)
# Part I — Short-Term
writer.writerow(["--- Part I: Short-Term Capital Gains and Losses ---"])
writer.writerow(headers)
for t in short_term:
writer.writerow([
f"{_fmt_amount(t.sent_amount)} {t.sent_currency}",
_fmt_date_us(t.date_acquired),
_fmt_date_us(t.timestamp),
_fmt_usd(t.usd_proceeds),
_fmt_usd(t.usd_cost_basis),
"",
"0.00",
_fmt_usd(t.gain_or_loss),
f"{t.holding_days} days",
"(B)",
])
writer.writerow([])
# Part II — Long-Term
writer.writerow(["--- Part II: Long-Term Capital Gains and Losses ---"])
writer.writerow(headers)
for t in long_term:
writer.writerow([
f"{_fmt_amount(t.sent_amount)} {t.sent_currency}",
_fmt_date_us(t.date_acquired),
_fmt_date_us(t.timestamp),
_fmt_usd(t.usd_proceeds),
_fmt_usd(t.usd_cost_basis),
"",
"0.00",
_fmt_usd(t.gain_or_loss),
f"{t.holding_days} days",
"(B)",
])
# Summary
writer.writerow([])
total_st_gain = sum(t.gain_or_loss for t in short_term)
total_lt_gain = sum(t.gain_or_loss for t in long_term)
writer.writerow(["Summary"])
writer.writerow([f"Short-term transactions: {len(short_term)}"])
writer.writerow([f"Short-term net gain/loss: ${total_st_gain:.2f}"])
writer.writerow([f"Long-term transactions: {len(long_term)}"])
writer.writerow([f"Long-term net gain/loss: ${total_lt_gain:.2f}"])
writer.writerow([f"Total net gain/loss: ${total_st_gain + total_lt_gain:.2f}"])
writer.writerow([f"Cost basis method: {cost_basis_method.value.upper()}"])
content = buf.getvalue()
if output_path:
with open(output_path, "w", newline="") as f:
f.write(content)
print(f"Form 8949 CSV written to {output_path} "
f"({len(short_term)} short-term, {len(long_term)} long-term)")
return content
def export_cointracker_csv(
trades: list[TaxableEvent],
output_path: Optional[str] = None,
) -> str:
"""Export trades to CoinTracker CSV format.
Args:
trades: List of taxable events to export.
output_path: File path to write CSV. If None, returns CSV string.
Returns:
CSV content as a string.
"""
taxable = _filter_taxable(trades)
tag_map = {
TxType.SWAP: "trade",
TxType.STAKING_REWARD: "staking_reward",
TxType.AIRDROP: "airdrop",
TxType.LP_DEPOSIT: "lp_deposit",
TxType.LP_WITHDRAWAL: "lp_withdrawal",
}
headers = [
"Date", "Received Quantity", "Received Currency",
"Sent Quantity", "Sent Currency",
"Fee Amount", "Fee Currency", "Tag",
]
buf = StringIO()
writer = csv.writer(buf)
writer.writerow(headers)
for t in taxable:
# CoinTracker uses MM/DD/YYYY HH:MM:SS
date_str = t.timestamp.strftime("%m/%d/%Y %H:%M:%S")
writer.writerow([
date_str,
_fmt_amount(t.received_amount),
t.received_currency,
_fmt_amount(t.sent_amount),
t.sent_currency,
_fmt_amount(t.fee_amount),
t.fee_currency,
tag_map.get(t.tx_type, "trade"),
])
content = buf.getvalue()
if output_path:
with open(output_path, "w", newline="") as f:
f.write(content)
print(f"CoinTracker CSV written to {output_path} ({len(taxable)} events)")
return content
# ── Display & Comparison ────────────────────────────────────────────
def print_format_comparison(trades: list[TaxableEvent]) -> None:
"""Print a side-by-side comparison showing how formats differ.
Args:
trades: List of taxable events.
"""
print("\n" + "=" * 70)
print("FORMAT COMPARISON — Same trades, different CSV structures")
print("=" * 70)
# Show one swap trade in each format
swap_trades = [t for t in trades if t.tx_type == TxType.SWAP and not t.is_failed]
if not swap_trades:
print("No swap trades to compare.")
return
sample = swap_trades[0]
print(f"\nSample trade: {sample.description}")
print(f" Sent: {sample.sent_amount} {sample.sent_currency}")
print(f" Received: {sample.received_amount} {sample.received_currency}")
print(f" Date: {_fmt_date_iso(sample.timestamp)}")
# Koinly row
print("\n--- Koinly CSV row ---")
print("Date,Sent Amount,Sent Currency,Received Amount,Received Currency,"
"Fee Amount,Fee Currency,Net Worth Amount,Net Worth Currency,"
"Label,Description,TxHash")
print(f"{_fmt_date_iso(sample.timestamp)},{_fmt_amount(sample.sent_amount)},"
f"{sample.sent_currency},{_fmt_amount(sample.received_amount)},"
f"{sample.received_currency},{_fmt_amount(sample.fee_amount)},"
f"{sample.fee_currency},{sample.usd_proceeds},USD,swap,"
f"{sample.description},{sample.tx_hash}")
# Form 8949 row
if sample.date_acquired:
period = "Short-term" if not sample.is_long_term else "Long-term"
print(f"\n--- Form 8949 row ({period}) ---")
print("(a) Description,(b) Acquired,(c) Sold,(d) Proceeds,"
"(e) Cost Basis,(f) Code,(g) Adjustment,(h) Gain/Loss")
print(f"{_fmt_amount(sample.sent_amount)} {sample.sent_currency},"
f"{_fmt_date_us(sample.date_acquired)},"
f"{_fmt_date_us(sample.timestamp)},"
f"{_fmt_usd(sample.usd_proceeds)},"
f"{_fmt_usd(sample.usd_cost_basis)},,0.00,"
f"{_fmt_usd(sample.gain_or_loss)}")
# CoinTracker row
print("\n--- CoinTracker CSV row ---")
print("Date,Received Quantity,Received Currency,Sent Quantity,"
"Sent Currency,Fee Amount,Fee Currency,Tag")
ct_date = sample.timestamp.strftime("%m/%d/%Y %H:%M:%S")
print(f"{ct_date},{_fmt_amount(sample.received_amount)},"
f"{sample.received_currency},{_fmt_amount(sample.sent_amount)},"
f"{sample.sent_currency},{_fmt_amount(sample.fee_amount)},"
f"{sample.fee_currency},trade")
print()
def print_summary(trades: list[TaxableEvent]) -> None:
"""Print a summary of all generated trades.
Args:
trades: List of taxable events.
"""
print("\n" + "=" * 70)
print("TRADE SUMMARY")
print("=" * 70)
taxable = _filter_taxable(trades)
non_taxable = [t for t in trades if t.tx_type in {TxType.FAILED, TxType.MIGRATION}]
print(f"\nTotal events: {len(trades)}")
print(f"Taxable events: {len(taxable)}")
print(f"Non-taxable: {len(non_taxable)}")
by_type: dict[str, int] = {}
for t in trades:
label = t.tx_type.value
if t.is_failed:
label = "failed"
by_type[label] = by_type.get(label, 0) + 1
print("\nBy type:")
for tx_type, count in sorted(by_type.items()):
print(f" {tx_type:20s} {count}")
disposals = [
t for t in taxable
if t.sent_amount > 0
and t.date_acquired is not None
and t.tx_type == TxType.SWAP
]
if disposals:
short_term = [t for t in disposals if not t.is_long_term]
long_term = [t for t in disposals if t.is_long_term]
st_gain = sum(t.gain_or_loss for t in short_term)
lt_gain = sum(t.gain_or_loss for t in long_term)
print(f"\nDisposals for Form 8949: {len(disposals)}")
print(f" Short-term: {len(short_term)} (net: ${st_gain:.2f})")
print(f" Long-term: {len(long_term)} (net: ${lt_gain:.2f})")
print(f" Total net: ${st_gain + lt_gain:.2f}")
print()
# ── Main ─────────────────────────────────────────────────────────────
def run_demo(fmt: str = "all") -> None:
"""Run the full demo: generate trades and export to selected formats.
Args:
fmt: Export format — "koinly", "form8949", "cointracker", or "all".
"""
print("Crypto Tax Export — Demo Mode")
print("=" * 70)
print("Generating sample Solana trades...\n")
trades = generate_demo_trades()
print_summary(trades)
if fmt in ("koinly", "all"):
print("-" * 70)
koinly_csv = export_koinly_csv(trades, "demo_koinly_export.csv")
print("Preview (first 5 lines):")
for line in koinly_csv.strip().split("\n")[:5]:
print(f" {line[:100]}{'...' if len(line) > 100 else ''}")
print()
if fmt in ("form8949", "all"):
print("-" * 70)
form_csv = export_form_8949_csv(trades, "demo_form_8949.csv")
print("Preview (first 12 lines):")
for line in form_csv.strip().split("\n")[:12]:
print(f" {line[:100]}{'...' if len(line) > 100 else ''}")
print()
if fmt in ("cointracker", "all"):
print("-" * 70)
ct_csv = export_cointracker_csv(trades, "demo_cointracker_export.csv")
print("Preview (first 5 lines):")
for line in ct_csv.strip().split("\n")[:5]:
print(f" {line[:100]}{'...' if len(line) > 100 else ''}")
print()
if fmt == "all":
print_format_comparison(trades)
print("=" * 70)
print("Demo complete. Files written:")
if fmt in ("koinly", "all"):
print(" - demo_koinly_export.csv")
if fmt in ("form8949", "all"):
print(" - demo_form_8949.csv")
if fmt in ("cointracker", "all"):
print(" - demo_cointracker_export.csv")
print()
print("NOTE: This is demo data only. Not financial or tax advice.")
print("Consult a qualified tax professional for your specific situation.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Crypto Tax Export — generate trade CSVs for tax software",
)
parser.add_argument(
"--demo",
action="store_true",
help="Run in demo mode with sample Solana trades",
)
parser.add_argument(
"--format",
choices=["koinly", "form8949", "cointracker", "all"],
default="all",
help="Export format (default: all)",
)
args = parser.parse_args()
if not args.demo:
print("Use --demo to run with sample data.")
print("Usage: python scripts/tax_exporter.py --demo [--format koinly|form8949|cointracker|all]")
sys.exit(0)
run_demo(args.format)
Related skills
FAQ
Which tax formats does it export?
Koinly, CoinTracker, CoinLedger, TokenTax, TurboTax/TaxAct CSVs, and IRS Form 8949 Parts I and II.
How are failed Solana transactions handled?
Failed transactions are identified and excluded because they are not taxable events, though the fee may still be deductible.