bank tranfer fix
All checks were successful
Deploy / build-deploy (push) Successful in 3m11s

This commit is contained in:
Jorijn van der Graaf 2026-08-20 23:33:50 +02:00
commit aaa7a8ce99
10 changed files with 543 additions and 8 deletions

View file

@ -351,6 +351,38 @@ BUNQ_API_KEY=… TRANSFER_IBAN=NL.. \
# -> pulled 2 new credit(s) into /var/lib/catcrafts/orders.jsonl.transfer-credits.jsonl
```
**The key is IP-restricted, and that decides the shape.** Verified the hard way
on 2026-08-20: the bunq API key permits one address (the owner's home
connection), and **bunq enforces that on every request, not only at device
registration**. Registering the server's address in the device's
`permitted_ips` does not help — the device registered fine from home with both
addresses listed, and every read from the server still came back
`"Incorrect API key or IP address"`. So `BUNQ_API_KEY` on the web host cannot
work unless the key's allowlist is widened in the bunq app.
That constraint happens to enforce the right design, so it is now the supported
one: **`tools/pull-and-ship-credits.sh`**, run by a systemd USER timer on the
machine bunq permits (`deploy/catcrafts-credits.{service,timer}`, install
instructions in the service file). It pulls, keeps an accumulating local copy,
and ships **incoming credits only** to the file the rail reads. Outgoing lines
are supplier payments and card spending: the matcher ignores negative amounts,
so shipping them would put the business's outgoing payment history on a
public-facing host for no settlement benefit. The script refuses to overwrite
the remote with an empty file, so an upstream parse failure leaves the server
settling from the last good copy rather than from nothing.
Latency, so nobody wonders: five minutes for the timer plus the reconciler's own
60 s cadence, so a fresh order confirms within about six minutes of the money
landing. Orders more than two hours old back off to a ten-minute poll, so an
older one can take that long.
Two operational notes. The timer only runs while that machine is awake, so
`Persistent=true` makes it pull once on waking rather than silently skipping
every window it missed. And if the home address changes, both the key's
allowlist (in the bunq app) and settlement stop working until it is updated —
the failure mode is silent from the shop's side, so the credits file's mtime is
worth glancing at.
**Read this before deciding where to put the key.** A bunq API key can
**initiate payments**, and bunq offers no read-only scope, so there is no such
thing as a key that can only read. That gives two deployment shapes, and they

View file

@ -0,0 +1,30 @@
# Read the bank and hand the incoming credits to the server.
#
# A USER unit, on the machine whose IP the bunq API key permits — NOT on the
# web host. The web host cannot call bunq at all (the key is IP-restricted at
# the key level and bunq enforces it on every request), and it should not be
# able to: a bunq key can initiate payments, because bunq offers no read-only
# scope. So the key stays here and the server only ever reads a file.
#
# Install:
# mkdir -p ~/.config/systemd/user
# cp deploy/catcrafts-credits.{service,timer} ~/.config/systemd/user/
# systemctl --user daemon-reload
# systemctl --user enable --now catcrafts-credits.timer
# loginctl enable-linger "$USER" # so it runs when nobody is logged in
#
# Watch: journalctl --user -u catcrafts-credits -f
[Unit]
Description=Pull bunq credits and ship them to catcrafts.net
Documentation=file:deploy/README.md
# Pointless without a route to the bank or to the server.
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=%h/repos/catcrafts.net
ExecStart=%h/repos/catcrafts.net/tools/pull-and-ship-credits.sh
# A failed pull must not look like a successful one. The script already refuses
# to overwrite the remote with an empty file, so a failure here leaves the
# server settling from the last good copy rather than from nothing.
SuccessExitStatus=0

View file

@ -0,0 +1,23 @@
# Every five minutes. Two things set the floor and the ceiling:
#
# * bunq rate-limits reads to roughly 3 GET per 3 seconds per method, so one
# call per five minutes is nowhere near it. The cost of going faster is
# nothing technical; it is just noise.
# * a donor watching the order page wants confirmation while they still care.
# Five minutes here plus the reconciler's own 60 s cadence means a fresh
# order confirms within about six minutes of the money landing.
#
# Persistent=true so a laptop that was asleep pulls once on waking instead of
# silently skipping every window it missed — which is exactly when a donation
# would otherwise sit unacknowledged overnight.
[Unit]
Description=Pull bunq credits for catcrafts.net every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true
AccuracySec=30s
[Install]
WantedBy=timers.target