FuelBoard Relay API
Keyless proxy for the official UK Fuel Finder API (the OAuth-protected
GOV.UK service). Sibling of fuelboard-relay (which reads the public CSV
mirror): this one talks to the real API, joins the station-info and fuel-price
endpoints on node_id, and normalises to the same output shape so the
FuelBoard iOS app and widget work against either relay unchanged.
Holds the GOV.UK OAuth 2.0 client credentials server-side so the app never embeds a secret (secrets inside an IPA are extractable by anyone).
Architecture
FuelBoard app / widget
│ GET http://192.168.1.131:8789/api/v1/stations?fuel=e10&lat=..&lng=..
▼
FuelBoard Relay API (this repo, runs on the Mac Mini)
│ POST /api/v1/oauth/generate_access_token (token cached ~1h)
│ GET /api/v1/pfs?batch-number=N (station info, 500/page)
│ GET /api/v1/pfs/fuel-prices?batch-number=N (prices, 500/page)
▼
UK Fuel Finder API (official gov.uk open data)
- Token refreshed automatically before expiry (60s buffer); reused across the two batched loops (docs: reuse tokens until near expiry).
- Both endpoints paginated 500 records/page via
batch-numberuntil a short page is returned; pages are spacedFUEL_API_BATCH_SLEEP(default 4s) apart to stay inside the documented 30 rpm sequential-only rate limit. - Prices arrive as decimal strings in pence (
"0120.0000"= 120.0p); some stations report pounds (values < 2.0 are multiplied by 100). Out-of-band values are dropped (50–500p sane band) so a pounds-denominated column can't poison the nationwide cheapest reference. - Fuel types mapped:
E5/E10→ same,B7/B7_STANDARD/B7S/B7P/B7_PREMIUM/B10→DIESEL. Others (HVO,SDV, …) are ignored — the app tracks exactly E5/E10/DIESEL. - Station snapshot cached
FUEL_CACHE_TTL(default 300s); a stale cache is refreshed in the background on the next request and stale data is served in the meantime, so a multi-minute sync never blocks the app's 5s timeout. - Base URL is
https://www.fuel-finder.service.gov.uk(NOTapi.fuelfinder...— that host doesn't resolve). Overridable per-endpoint via env. - No credentials are exposed to clients — the app calls the relay keyless.
Setup
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install pytest # only needed for the test suite
cp .env.example .env # then fill in FUEL_API_CLIENT_ID / FUEL_API_CLIENT_SECRET
./scripts/run.sh # serves on FUEL_RELAY_PORT (default 8789)
Without credentials, FUEL_SOURCE=auto falls back to the public hourly CSV
mirror (same parser as fuelboard-relay), so the full relay→app path is
testable end-to-end before GOV.UK One Login creds exist. /health reports the
active source (csv vs api).
Endpoints
| Endpoint | Description |
|---|---|
GET /health |
Cache status, active source, last sync, failure counters |
GET /api/v1/stations |
All stations with prices (keyless) |
GET /api/v1/stations?fuel=e10&lat=53.72&lng=-1.85&radius=10&limit=10 |
Filter by fuel, radius around a point, sorted nearest-first |
GET /api/v1/stations?fuel=diesel (no lat/lng) |
Sorted cheapest-first |
Fuel grades: e10, e5, diesel. Output contract identical to
fuelboard-relay — the app can switch relays by changing the base URL only.
Getting credentials
- Sign in at https://developer.fuel-finder.service.gov.uk/get-started-ifr/onelogin with GOV.UK One Login.
- Register an application → get
client_id+client_secret. - Put them in
.env(never in git, never in the app).
Fair use notes (from the Fuel Finder developer guideline)
- Refresh at least every 5 minutes — we do (cron-style refresh on request + TTL cache).
- Serve data unmodified — this relay only filters/sorts, never alters prices.
- Keep timestamps —
price_updatedis passed through untouched. - Rate limits: ~100 rpm documented — batched loops issue ~2 requests per page (info + prices), comfortably inside the limit on a 5-minute cadence.
Tests
python -m pytest tests/ -q
Covers price-string parsing (pence, pounds, out-of-band), the node_id join, fuel-type mapping, and CSV-fallback parsing — against the documented API shapes, no network required.
Switching the app
Point Shared/FuelPriceProvider.swift's RelayFuelProvider.baseURL at
http://192.168.1.131:8789 (API relay) instead of :8788 (CSV relay). No
other app change needed — the station JSON is identical in shape.