Three scripts that run. They are short because the API already returns the numbers worth deciding on: balance, burn rate, runway, quota.
All of them assume FFXF_TOKEN in the environment and
jq installed.
An ephemeral runner
The textbook case: a machine born for one job, destroyed when it ends. Real cost of an hour-long job on a Nano: 0.018 CAD.
#!/usr/bin/env bash
set -euo pipefail
API=https://api.ffxf.net/v1
AUTH="Authorization: Bearer $FFXF_TOKEN"
NAME="runner-$(date +%s)"
# Order. The idempotency key protects against a double send on a network retry.
VM=$(curl -sf "$API/vms" -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: $NAME" \
-d "{\"plan\":\"nano\",\"region\":\"montreal\",\"image\":\"debian-13\",
\"hostname\":\"$NAME\",\"billing\":\"hourly\",\"password_delivery\":\"none\",
\"ssh_keys\":[\"$FFXF_SSH_FINGERPRINT\"]}" | jq -r .data.vm.id)
# Wait for delivery: provisioning is asynchronous.
until [ "$(curl -sf "$API/vms/$VM" -H "$AUTH" | jq -r .data.status)" = "running" ]; do
sleep 5
done
IP=$(curl -sf "$API/vms/$VM" -H "$AUTH" | jq -r .data.ipv4)
ssh -o StrictHostKeyChecking=accept-new "debian@$IP" "./my-job.sh"
# Destroy. The hostname confirms: without it, the API refuses.
curl -sf -X DELETE "$API/vms/$VM?confirm=$NAME" -H "$AUTH"
Note password_delivery: none: no machine is safer than one
whose password is sitting in a mailbox. It requires at least one SSH key.
Shutting down before running dry
The API says how long the balance lasts at the current burn. A script reading that number never gets cut off by surprise.
#!/usr/bin/env bash
# Run hourly. Under 12 hours of runway, shut the disposable machines down so
# what is left of the credit serves the ones that matter.
API=https://api.ffxf.net/v1
AUTH="Authorization: Bearer $FFXF_TOKEN"
RUNWAY=$(curl -sf "$API/account" -H "$AUTH" | jq -r '.data.hourly.runway_hours // 9999')
if (( $(echo "$RUNWAY < 12" | bc -l) )); then
curl -sf "$API/vms" -H "$AUTH" \
| jq -r '.data[] | select(.hostname | startswith("runner-")) | .id' \
| while read -r vm; do
curl -sf -X POST "$API/vms/$vm/actions" -H "$AUTH" \
-H "Content-Type: application/json" -d '{"type":"shutdown"}'
done
fi
A powered-off machine is still billed: the hour runs as long as the machine
exists, on or off. To stop the meter you must destroy it. This script buys
time, it saves nothing — swap shutdown for deletion if saving is
what you are after.
An inventory that costs what it says
The per-machine ledger answers the only question that matters at month end: which one is expensive.
curl -sf "https://api.ffxf.net/v1/usage?group_by=vm" -H "Authorization: Bearer $FFXF_TOKEN" \
| jq -r '.data[] | [.hostname // "(destroyed)", .hours, .amount] | @tsv' \
| sort -k3 -rn
Destroyed machines appear with their name: what they cost does not vanish with them.
Handling refusals properly
A script that reads the code rather than the message survives
rewordings and translations.
response=$(curl -s -w '\n%{http_code}' "$API/vms" -H "$AUTH" \
-H "Idempotency-Key: $NAME" -H "Content-Type: application/json" -d "$BODY")
body=$(echo "$response" | head -n -1); code=$(echo "$response" | tail -n1)
case "$(echo "$body" | jq -r '.error.code // "ok"')" in
ok) echo "ordered" ;;
insufficient_credit) echo "top up $(echo "$body" | jq -r .error.details.missing) CAD" ;;
quota_reached) echo "cap reached: destroy one, or ask support" ;;
rate_limited) sleep 60; exec "$0" "$@" ;;
*) echo "refused: $body"; exit 1 ;;
esac