Data source
An unauthenticated monthly endpoint returns per-division, per-day counts:payload.availability[division].date_availability[date].remaining > 0. The endpoint is monthly, so a window spans one call per month. Division ids come from GET /api/permitcontent/{permit} — never guessed.
Targets
Configured intargets.yaml. Division ids are resolved and pinned:
| River | Permit | Division | Window |
|---|---|---|---|
| Middle Fork Salmon | 234623 | 377 | Jun 1 – Jul 20 |
| Main Salmon | 234622 | 376 | Jun 20 – Sep 20 |
| Selway | 234624 | 378 | Jun 1 – Jul 15 |
| Dinosaur – Green (Gates of Lodore) | 250014 | 380 | May 15 – Sep 1 |
| Dinosaur – Yampa (Deerlodge) | 250014 | 371 | May 15 – Jul 15 |
250014 also exposes a 1250014 “Combined Yampa/Green” division — a combined-trip artifact, not one of the two launch segments, so it’s excluded.
Behavior
- Edge-triggered. State is keyed
permit:division:datein./data/state.json. A date alerts once when it goes open and re-arms when it closes, so a later reopen alerts again. - Close-debounce. A reopened date only re-alerts after it has read closed continuously for
reopen_debounce_minutes(default 20). This absorbs transient flicker — a scarce date briefly zeroes while two spots sit in someone’s checkout cart — without muting a genuine book-then-cancel. New openings are never delayed; only re-alerts are gated. Transitions are logged (CLOSE/FLAP/ARMED) so real open/closed durations can be measured. - Grouped notifications. Newly-open dates are merged into calendar-consecutive runs — one bookable stretch is one notification (title
Main Salmon: Sep 8-20), not one ping per day. A gap splits the run; a run that crosses a month boundary readsAug 30 - Sep 2. Titles carry the river and date span only — no emoji, no remaining-spot count. - Cold start alerts. On first run (no state file) every currently-open date inside a window alerts once — the bot proves it’s working by surfacing what’s open now, not just future changes. After that, state suppresses re-alerts until a date closes and reopens.
- Polling. ~120s interval with ±25% jitter. Permit months are fetched once and shared across that permit’s divisions; calls are spaced a few seconds apart to stay polite to recreation.gov’s Akamai edge. A real browser User-Agent is required.
- Fault tolerance. A cycle never crashes the loop. A
403/429block self-alerts (hourly cooldown) — silence there would look identical to “nothing available.”
Operations
- Image: built locally, pushed to
192.168.1.100:5000/riverbot:latest. Rebuild withbash scripts/rebuild-images.sh riverbot. - Logs:
docker service logs riverbot_riverbot(or Dozzle). - Re-baseline: delete
riverbot/data/state.jsonand restart — the next cycle re-alerts every currently-open in-window date. - Add a river: resolve its division from
permitcontent/{id}, add an entry totargets.yaml, restart. - Swap the notifier:
notify()inriverbot.pyis the only ntfy-aware seam.