A Go CLI-based personal finance tracking and management tool. Currently solely used for tracking cost basis of financial assets using Google Sheets as the data source. Supports FIFO and average cost methods, multiple asset types, and persists data locally in SQLite.
In the future, this CLI could help in more general personal finance tracking and tax preparation use-cases. It could bubble up reports and integrate with LLMs to offer unofficial financial management advice.
perfi replaces manual spreadsheet-based cost basis tracking with a single CLI that:
- Pulls transaction data from a Google Sheet into a local SQLite database
- Calculates cost basis using FIFO or average cost methods
- Pushes locally-recorded transactions and formatted results (gain/loss, holding period) back to the sheet
It's designed for personal tax reporting on crypto and other asset transactions.
- Go 1.23+ — install instructions
- Google Cloud CLI (
gcloud) — install instructions - A Google Sheet containing your transaction data
Run the interactive setup command:
perfi initThis will:
- Create or select a GCP project
- Create or select a service account
- Enable the Google Sheets API
- Grant your account permission to impersonate the service account
You can also provide an existing project and/or service account via flags:
# Use an existing project, create a new service account
perfi init --project my-existing-project
# Use both an existing project and service account
perfi init --project my-project --service-account perfi-sheets@my-project.iam.gserviceaccount.comAfter running perfi init, share your Google Sheet with the service account email printed at the end of setup. Open your sheet, click Share, and add the service account email as an Editor.
If you haven't already, authenticate with Google Cloud:
gcloud auth application-default loginperfi authenticates by impersonating the service account using your local credentials — no service account key files are needed.
Open your target Google Sheet in a browser and copy the Sheet ID from the URL:
https://docs.google.com/spreadsheets/d/SHEET_ID_HERE/edit
You'll need this for the configuration file.
git clone https://github.com/bradlet/perfi.git
cd perfi
go build -o perfi .Optionally move the binary to your PATH:
mv perfi ~/go/bin/go install github.com/bradlet/perfi@latestCreate a .perfi.yaml file in your home directory or the directory where you run the CLI:
# Google Sheet ID (from the URL)
sheet_id: "1ABCdef123456789"
# Default asset to operate on
default_asset: SOL
# Cost basis method: "fifo" or "average"
method: fifo
# SQLite database path
db_path: "./perfi.db"
# Per-asset Google Sheets ranges
assets:
SOL:
# Range to read transaction data from (columns: Source, Date, Quantity, Price, Total)
read_range: "Solana Cost Basis 2024!A:E"
# Range to write calculated results to
write_range: "Solana Cost Basis 2024!K1"
ETH:
read_range: "ETH Cost Basis 2024!A:E"
write_range: "ETH Cost Basis 2024!K1"All config values can be overridden via CLI flags or environment variables with the perfi_ prefix (e.g., perfi_SHEET_ID).
Your Google Sheet should have these columns in order:
| Column | Content | Example |
|---|---|---|
| A | Source (exchange name) | Coinbase |
| B | Date (Excel serial number or MM/DD/YYYY) |
45292 (= 2024-01-01) or 01/01/2024 |
| C | Quantity (positive = buy, negative = sell) | 12.15 or -3.5 |
| D | Price per unit | 82.25 |
| E | Transaction total (USD) | 1000.00 |
Header rows are automatically detected and skipped.
When you run perfi push, the results are written back to your Google Sheet as a table with these columns:
| Column | Content | Example |
|---|---|---|
| A | Date of the sale | 06/15/2024 |
| B | Quantity sold | 5.50000000 |
| C | Total proceeds (USD) | 550.00 |
| D | Total cost basis (USD) | 400.00 |
| E | Gain/Loss (USD) | 150.00 |
| F | Holding period | Long-term or Short-term |
Where it gets written: The write_range in your config specifies the starting cell. For example, "Solana Cost Basis 2024!K1" writes the header row to column K, and data rows below it. The output is written with a header row, followed by one row per sell transaction.
Per-asset configuration: Each asset has its own write_range in your config, since you may want results on different sheets or columns:
assets:
SOL:
write_range: "Solana Cost Basis 2024!K1" # Results for SOL here
ETH:
write_range: "ETH Cost Basis 2024!K1" # Results for ETH hereOverride the write location: Use the --range flag to write to a different location:
perfi push --range "Results!A1" # Override write location
perfi run --asset SOL --write-range "Summary!A1" # Override for the full pipelineThese flags are available on all commands:
| Flag | Description | Default |
|---|---|---|
--config |
Config file path | $HOME/.perfi.yaml |
--asset |
Asset type (e.g., SOL, ETH) | From config default_asset |
--verbose |
Enable verbose output | false |
Reads transaction data from the configured Google Sheet range and upserts it into the local SQLite database. Existing transactions with matching (asset, source, date, quantity) keys are updated rather than duplicated.
# Pull default asset
perfi pull
# Pull a specific asset
perfi pull --asset ETH
# Override the sheet range
perfi pull --range "My Sheet!A2:E500"
# Reset local database to match the sheet exactly
perfi pull --fresh| Flag | Description | Default |
|---|---|---|
--sheet-id |
Google Sheet ID | From config |
--range |
Sheet range to read | From config (read_range) |
--db |
SQLite database path | From config |
--fresh |
Delete all sheet-origin transactions for the asset before pulling | false |
By default, pull upserts — it updates existing rows and adds new ones, but does not remove transactions that were deleted from the sheet. Use --fresh to clear and re-import from the sheet. Unsynced local transactions (from perfi sell) are preserved.
Loads transactions from the local SQLite database and runs cost basis calculation. Results are saved back to the database for later retrieval or pushing to a sheet.
# Calculate using the configured method (default: FIFO)
perfi calc
# Use average cost method
perfi calc --method average
# Verbose output shows per-sale details
perfi calc --verbose| Flag | Description | Default |
|---|---|---|
--method |
Cost basis method: fifo, average |
From config |
--db |
SQLite database path | From config |
Appends any locally-recorded transactions (from perfi sell) to the transaction log in the Google Sheet, then writes the latest cost basis results to the configured output range.
# Preview what would be written
perfi push --dry-run
# Write local transactions and results to the configured ranges
perfi push
# Override the output target range
perfi push --range "Results!A1"| Flag | Description | Default |
|---|---|---|
--sheet-id |
Google Sheet ID | From config |
--range |
Target range for output | From config (write_range) |
--db |
SQLite database path | From config |
--dry-run |
Print output without writing to sheet | false |
Records a sell transaction in the local SQLite database and immediately runs cost basis calculation. The transaction will be appended to the Google Sheet transaction log on the next perfi push.
# Sell 5 SOL at $150/unit (total defaults to quantity × price)
perfi sell --quantity 5 --price 150
# Specify total proceeds explicitly
perfi sell --quantity 5 --total 750
# Specify asset, date, and source
perfi sell --asset ETH --quantity 2 --price 3000 --date 2024-06-15 --source Coinbase
# Override cost basis method
perfi sell --quantity 5 --price 150 --method averageEither --price or --total must be provided.
| Flag | Description | Default |
|---|---|---|
--quantity |
Quantity sold (required, positive number) | — |
--price |
Price per unit at sale time | — |
--total |
Total sale proceeds (optional; defaults to quantity × price) |
— |
--source |
Source label for the transaction | manual |
--date |
Sale date in YYYY-MM-DD format |
Today |
--method |
Cost basis method: fifo, average |
From config |
--db |
SQLite database path | From config |
Executes the full pipeline: pulls transaction data from Google Sheet, runs cost basis calculation, and writes local transactions and results back to the sheet. Equivalent to running pull, calc, and push sequentially.
# Full pipeline
perfi run
# Full pipeline with dry-run (doesn't write to sheet)
perfi run --dry-run
# Full pipeline for a specific asset and method
perfi run --asset ETH --method average
# Full pipeline with a fresh pull from the sheet
perfi run --fresh| Flag | Description | Default |
|---|---|---|
--sheet-id |
Google Sheet ID | From config |
--read-range |
Sheet range to read | From config (read_range) |
--write-range |
Target range for output | From config (write_range) |
--method |
Cost basis method: fifo, average |
From config |
--db |
SQLite database path | From config |
--dry-run |
Print output without writing to sheet | false |
--fresh |
Delete all sheet-origin transactions for the asset before pulling | false |
The default method. When you sell an asset, the earliest purchased lots are consumed first. This is the most common method for tax reporting.
Example: Buy 10 SOL at $100, then buy 10 SOL at $200. Sell 15 SOL — the cost basis is (10 × $100) + (5 × $200) = $2,000.
Each sale's cost basis is the weighted average price of all units held at the time of sale. The average is recalculated after each sale.
Example: Buy 10 SOL at $100 ($1,000), then buy 10 SOL at $200 ($2,000). Average cost = $3,000 / 20 = $150/unit. Sell 15 SOL — cost basis = 15 × $150 = $2,250.
Both methods classify gains as long-term (held > 365 days) or short-term. For FIFO, this is based on the specific lots consumed. For average cost, the earliest lots are used for holding period determination.
perfi/
├── main.go # Entry point
├── cmd/ # Cobra CLI commands
│ ├── root.go # Root command + Viper config
│ ├── init.go # GCP project + service account setup
│ ├── pull.go # Pull sheet → SQLite
│ ├── calc.go # Run cost basis calculation
│ ├── push.go # Write local txns + results → sheet
│ ├── run.go # Combined workflow
│ └── sell.go # Record a sell transaction locally
├── internal/
│ ├── config/ # Viper config struct
│ ├── engine/ # Cost basis calculators (FIFO, average)
│ ├── sheets/ # Google Sheets API client + parser + writer
│ ├── storage/ # SQLite persistence layer
│ └── workflow/ # Pipeline orchestrator
└── testdata/ # Test fixtures
# All tests
go test ./...
# Verbose output
go test ./... -v
# Specific package
go test ./internal/engine/ -v
# With coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.outshopspring/decimalfor all monetary values — neverfloat64for moneymodernc.org/sqlite(pure Go) — no CGO dependency, simplifies cross-compilation- Strategy pattern for cost basis methods — add new methods by implementing the
Calculatorinterface - All monetary values stored as TEXT in SQLite to preserve decimal precision
- Interface-based design for sheets client and storage — enables testing without live API calls
- Create
internal/engine/yourmethod.goimplementingCalculator - Add a case to
NewCalculator()instrategy.go - Add tests in
internal/engine/yourmethod_test.go