diff --git a/bin/Makefile b/bin/Makefile
index 499fe8c..6aba9dc 100644
--- a/bin/Makefile
+++ b/bin/Makefile
@@ -16,7 +16,7 @@ PREFIX = $(HOME)
MANPREFIX = $(PREFIX)/local
BIN = ZZZ browser credentials fetch flac2ogg imgresize invidious \
nyaasearch plumb pstsrv rbucmd rfcopen screenshot sekrit \
- w3m-copy-link
+ w3m-copy-link xchg-rates
MAN1 = sekrit.1
BROWSER_LINKS = tor-browser
diff --git a/bin/xchg-rates.sh b/bin/xchg-rates.sh
new file mode 100644
index 0000000..1b7e9a6
--- /dev/null
+++ b/bin/xchg-rates.sh
@@ -0,0 +1,103 @@
+#!/bin/sh
+# xchg-rates
+# Written in 2023 by Lucas
+# CC0 1.0 Universal/Public domain - No rights reserved
+#
+# To the extent possible under law, the author(s) have dedicated all
+# copyright and related and neighboring rights to this software to the
+# public domain worldwide. This software is distributed without any
+# warranty. You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# .
+
+usage()
+{
+ cat - <&2
+Usage:
+ ${0##*/} amount from to
+ ${0##*/} -l
+
+Rates By Exchange Rate API - https://www.exchangerate-api.com
+EOF
+ exit 1
+}
+
+err()
+{
+ printf "%s: %s\n" "${0##*/}" "$*" >&2
+ exit 1
+}
+
+warn()
+{
+ printf "%s: %s\n" "${0##*/}" "$*" >&2
+}
+
+date_gt()
+{
+ expr "X$1" ">" "X$2" >/dev/null
+}
+
+RATES_API=https://open.er-api.com/v6/latest/USD
+fetch_rates()
+{
+ ftp -o "$RATES_JSON" "$RATES_API"
+}
+
+cachedir=${XDG_CACHE_HOME:-~/.cache}/xchg-rates
+mkdir -p "$cachedir"
+
+: ${RATES_JSON:=$cachedir/rates.json}
+
+do_list=false
+while getopts l flag; do
+ case $flag in
+ l) do_list=true ;;
+ *) usage ;;
+ esac
+done
+shift $(($OPTIND - 1))
+
+if [ ! -f "$RATES_JSON" ]; then
+ fetch_rates
+fi
+
+next_update_unix=$(jq -er .time_next_update_unix "$RATES_JSON")
+if [ $? -ne 0 ]; then
+ fetch_rates
+else
+ next_update=$(date -r "$next_update_unix" +%Y%m%dT%H%M%SZ)
+ now=$(date +%Y%m%dT%H%M%SZ)
+ if date_gt "$now" "$next_update"; then
+ fetch_rates
+ fi
+fi
+
+eol=$(jq -r '.time_eol_unix' "$RATES_JSON")
+if [ X"$eol" != X"0" ]; then
+ warn "EOL set for $(date -j "$eol" +%Y%m%dT%H%M%SZ)"
+fi
+
+if $do_list; then
+ jq -r '.rates | to_entries | .[].key' "$RATES_JSON" | sort
+ exit $?
+fi
+
+if [ $# -ne 3 ]; then
+ usage
+fi
+amount=$1
+from=$(echo "$2" | tr a-z A-Z)
+to=$(echo "$3" | tr a-z A-Z)
+
+if ! jq --arg k "$from" -er '.rates | has($k)' "$RATES_JSON" >/dev/null; then
+ err "no rate for $from"
+fi
+if ! jq --arg k "$to" -er '.rates | has($k)' "$RATES_JSON" >/dev/null; then
+ err "no rate for $to"
+fi
+jq -r --arg from "$from" --arg to "$to" \
+ '.rates | [.[$from], .[$to]] | @tsv' "$RATES_JSON" | {
+ read -r from to
+ echo "$amount / $from * $to" | bc -l
+}