FetchRate is a historical currency and cryptocurrency exchange rate tool. Given an amount, a source currency or crypto symbol, and a date, it returns the equivalent value in a target currency using rates from that specific day.
It provides three interfaces for the same service: a CLI, a REST API, and a web UI.
Stack: Java 17 · Spring Boot 4 · SQLite · Thymeleaf · Maven
- Convert any fiat currency or cryptocurrency to EUR (default)
- Convert to any other fiat currency supported by the ECB (e.g. USD, GBP, JPY) using
--to - Convert to another cryptocurrency (e.g. ETH, SOL) using
--exchange - All conversions are historical; rates are looked up for the exact date provided
Requirements (local build): Java 17+, Maven 3.9+
mvn package -DskipTestsCLI:
java -jar target/FetchRate-0.5.jar convert --amount 100 --input-currency USD --date 2024-01-15HTTP server (web UI at http://localhost:8000, REST API at /convert):
java -jar target/FetchRate-0.5.jar start_http_server
# Custom port:
java -jar target/FetchRate-0.5.jar start_http_server --port 9090The HTTP server binds to
127.0.0.1(loopback only) by default. To expose it on the network (for example, behind a reverse proxy), setserver.address=0.0.0.0infetchrate.properties.
Docker:
docker compose upRate data is persisted to ./data/ on the host. The container binds to 127.0.0.1 by default; to expose it on the network, override the port binding in your compose file. To pass a crypto API key:
FETCHRATE_API_KEY=your_key docker compose upThe image also supports CLI usage. Build the image first, then run:
docker compose build
docker run --rm -v ./data:/app/data fetchrate convert -a 100 -c USD -d 2024-01-15Docker API key note: The
config --set-keycommand writes tofetchrate.propertiesinside the container, which is not persisted across restarts. When running via Docker, set the API key using theFETCHRATE_API_KEYenvironment variable or through the web UI (the web UI stores it in the database volume, which is persisted).
For a full list of commands and options:
java -jar target/FetchRate-0.5.jar --helpjava -jar FetchRate-0.5.jar convert -a <amount> -c <symbol> -d <YYYY-MM-DD> [--to <symbol>] [--exchange <symbol>]
| Flag | Short | Description |
|---|---|---|
--amount |
-a |
Amount to convert. Commas and underscores accepted as thousand separators (e.g. 1,000 or 1_000). |
--input-currency |
-c |
Source currency or crypto symbol (e.g. USD, BTC). Case-insensitive. |
--date |
-d |
Date in YYYY-MM-DD format. Must not be in the future. |
--to |
-t |
(Optional) Target fiat currency (e.g. USD, GBP, JPY). Defaults to EUR. Cannot be combined with --exchange. |
--exchange |
-e |
(Optional) Target cryptocurrency (e.g. ETH, SOL). Cannot be combined with --to. |
Examples:
# Convert to EUR (default)
java -jar FetchRate-0.5.jar convert -a 100 -c USD -d 2024-01-15
# Convert to a different fiat currency
java -jar FetchRate-0.5.jar convert -a 100 -c USD -d 2024-01-15 --to GBP
java -jar FetchRate-0.5.jar convert -a 1 -c BTC -d 2024-01-15 --to JPY
# Exchange for another cryptocurrency
java -jar FetchRate-0.5.jar convert -a 1 -c BTC -d 2024-01-15 --exchange ETH
java -jar FetchRate-0.5.jar convert -a 100 -c USD -d 2024-01-15 --exchange SOLAll results are printed to stdout as JSON. Errors are also returned as JSON.
Default output (EUR):
{
"input": {
"amount": "100",
"currencySymbol": "USD",
"date": "2024-01-15"
},
"output": {
"inEuro": "91.37"
}
}With --to or --exchange:
{
"input": {
"amount": "100",
"currencySymbol": "USD",
"date": "2024-01-15"
},
"output": {
"amount": "78.65",
"currency": "GBP"
}
}Fiat output is rounded to 2 decimal places. Cryptocurrency output (--exchange) is rounded to 8 decimal places.
java -jar FetchRate-0.5.jar config --set-key YOUR_API_KEY # Save crypto data provider API key
java -jar FetchRate-0.5.jar config --set-url https://... # Override crypto data provider URL
java -jar FetchRate-0.5.jar config --add-symbol XRP # Add symbol to daily update list
java -jar FetchRate-0.5.jar config --remove-symbol DOGE # Remove symbol from daily update list
java -jar FetchRate-0.5.jar config --list-symbols # Show current tracked symbol listGET /convert?amount=<n>&input_currency=<symbol>&date=<YYYY-MM-DD>[&output_currency=<symbol>]
Returns a JSON response on success, or an error field with an appropriate HTTP status on failure.
The optional output_currency parameter accepts any ECB-tracked fiat currency or cryptocurrency symbol. When omitted or set to EUR, the response uses the default inEuro format. When set to any other currency, the response uses the amount + currency format (same as the CLI --to / --exchange output).
GET /health
Returns {"status": "ok"}.
GET /settings
Returns the current runtime settings: apiKeyConfigured (boolean), providerUrl (string or null), trackedSymbols (array), and trackedSymbolsCustomized (boolean).
POST /settings
Content-Type: application/json
Updates one or more runtime settings. Accepted fields: apiKey, providerUrl, addSymbol, removeSymbol. Returns {"status":"saved"} on success or {"error":"..."} on validation failure. This is the programmatic equivalent of the web UI settings panel.
Once the HTTP server is running, a browser interface is available at /.
The web UI supports selecting an output currency via a dropdown (any ECB fiat currency or tracked cryptocurrency). The default output is EUR.
Rates are sourced from the European Central Bank. All 30 ECB-tracked currencies are supported as both input and output:
USD JPY BGN CZK DKK GBP HUF PLN RON SEK CHF ISK NOK TRY AUD BRL CAD CNY HKD IDR ILS INR KRW MXN MYR NZD PHP SGD THB ZAR
The ECB publishes rates on business days only. Requests on a weekend return an error with the nearest Friday suggested. Requests on a public holiday return a not-found error.
Default tracked symbols: BTC, LTC, DOGE, SOL, USDT.
Crypto rates require a compatible data provider API. An API key is required for automatic daily updates and on-demand fetching; without one, only rates loaded from local CSV files are available. Additional symbols can be added via config --add-symbol. Unlike fiat, crypto rates are available for every calendar day including weekends.
The application ships with LiveCoinWatch pre-configured as the default endpoint because it currently offers a free API tier. FetchRate is not affiliated with or endorsed by LiveCoinWatch; it is simply one example of a compatible provider. Any service implementing the same API contract can be used instead (see Custom Provider below).
When the output currency is not EUR, FetchRate uses EUR as an intermediate pivot:
input amount → EUR → output currency
This applies to both --to (fiat output) and --exchange (crypto output).
For fiat output, the ECB rate for the output currency is used. If the requested date falls on a weekend or holiday, the most recent available business day rate is used automatically.
For crypto output, the stored EUR-equivalent rate for that coin on the requested date is used.
Note for tax purposes: This two-step EUR-pivot methodology is consistent with guidance from major tax authorities:
- US (IRS): IRS Notice 2014-21 explicitly permits converting via an intermediate currency: "converted into U.S. dollars (or into another real currency which in turn can be converted into U.S. dollars)".
- UK (HMRC): No mandatory source is specified; consistent methodology and record-keeping are required.
- Germany (BMF): ECB is the official EU rate source; the 2025 BMF circular accepts daily pricing from recognised sources.
- Japan (NTA): Japanese taxpayers should note that the NTA standard instrument is the TTM rate published by a Japanese bank. ECB rates may qualify as an "other reasonable market rate applied continuously", but users filing Japanese taxes are advised to confirm with a local tax advisor.
The small rounding difference that may arise from the two-step pivot (versus a direct market rate) is considered immaterial under the "reasonable and consistently applied methodology" standard accepted by all of the above authorities.
The application maintains a local SQLite database in the data/ directory.
| Table | Columns | Purpose |
|---|---|---|
fiat_rates |
date, currency, rate |
ECB daily rates (1 EUR = N foreign units) |
crypto_rates |
date, symbol, rate |
Crypto rates in EUR per 1 coin |
meta |
key, value |
Runtime state: last update timestamps, API key, provider URL |
tracked_symbols |
symbol |
Custom crypto symbol list (empty = default list in effect) |
All bulk writes use INSERT ... ON CONFLICT DO UPDATE (upsert), so re-running an update is always safe. The schema is created automatically on first startup.
Rates are refreshed once per day on the first request of the day:
- Fiat: fetched from the ECB. The appropriate feed is selected automatically (full history, 90-day, or daily) based on how long ago the database was last updated.
- Crypto: if an API key is configured, the last 31 days of rates are fetched for all tracked symbols. Fiat and crypto updates are independent; a failure in one does not prevent the other.
If all sources fail (e.g. no network), the timestamp is not advanced and the next request retries.
If a crypto rate for the requested date is not in the database, the application fetches it on demand and returns the result immediately. The fetched rate is cached in the local database for future requests. If the on-demand fetch also fails (e.g. no API key configured, symbol not found), an error is returned.
Place .csv files in data/crypto/ to seed historical crypto rates without using API credits. The filename must match the coin symbol (e.g. BTC.csv). The supported format is the export from CoinCodex.
FetchRate ships with LiveCoinWatch pre-configured as the default endpoint. If you want to use it, free API keys are available at livecoinwatch.com/tools/api. To use a different provider instead, see Custom Provider below; only the API key and endpoint URL need to change.
Option 1 - Properties file (recommended): create fetchrate.properties next to the jar:
fetchrate.api-key=your_api_key_here
Option 2 - CLI:
java -jar FetchRate-0.5.jar config --set-key your_api_key_here
java -jar FetchRate-0.5.jar config --set-url https://your-provider/endpointOption 3 - Environment variable:
export FETCHRATE_API_KEY=your_api_key_hereWhen running in HTTP mode, the API key and provider URL can also be configured from the web UI under ⚙ API Settings.
CLI vs HTTP settings: The
configcommand and properties file write values that take effect on the next startup. The web UI (HTTP mode) stores values in the local database and they take effect immediately without a restart. If both are configured, the database value takes priority.
The provider URL is fully configurable, so you can swap LiveCoinWatch for any other service or self-hosted proxy, as long as it implements the same API contract:
- Method:
POST - Auth:
x-api-keyheader - Request body:
{"currency":"EUR","code":"BTC","start":<epoch_ms>,"end":<epoch_ms>,"meta":false} - Response:
{"code":"BTC","history":[{"date":<epoch_ms>,"rate":<eur_price>}, ...]}
Set the endpoint via any of the methods above (--set-url, fetchrate.provider-url, or the web UI). The API key field accepts whatever credential the replacement service requires.
The daily update fetches rates for the default set: BTC, LTC, DOGE, SOL, USDT. This list can be customised:
java -jar FetchRate-0.5.jar config --list-symbols
java -jar FetchRate-0.5.jar config --add-symbol XRP
java -jar FetchRate-0.5.jar config --remove-symbol DOGEThe first add or remove seeds the list from the current defaults, so no existing symbols are lost. In HTTP mode the list is also manageable from the web UI.
Copyright (c) 2026 Simon D. All rights reserved. No permission is granted to use, copy, modify, or distribute this project without a written license.
Contact: simon.d.dev@proton.me
