Files
Måns SivertssonandClaude Opus 5.5 9caff094ac Export, Import and Reset; JSON editor removed
- Export downloads one file with the saved config, all match data and
  history, the flight cache and view settings
- Import restores such a file after a confirmation; older config-only
  .json files are accepted and keep existing match data
- Reset (with a warning) deletes everything and starts over with new
  random example watches; replaces "Reset state"
- The JSON config section is gone
- Old ntfy targets no longer stop the script: they are ignored at
  runtime and on import, shown as "no longer supported" in the editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:08:47 +02:00

8.3 KiB
Raw Permalink Blame History

SAS bonus watch

A Tampermonkey / Violentmonkey userscript that watches SAS EuroBonus award availability and notifies you when seats you care about appear. It runs inside your own browser, so requests to sas.se are ordinary same-origin browser requests and it keeps working behind sas.se's Cloudflare protection.

  • Any number of watches: origins, destinations, one travel window, cabins (Economy / Premium / Business), seats, max stops
  • Finds outbound and return flights inside the window; optionally only alerts when both legs exist, with min/max nights
  • Gentle on sas.se: about one request a minute, a full sweep at most every 15 minutes, backs off when refused
  • Alerts via Zulip, Slack, Discord, any webhook, and desktop notifications
  • Dashboard shows what is bookable now and what you missed: seat drops and sold-out flights

Install

  1. Install Tampermonkey (Firefox, Chrome, Edge, Safari) or Violentmonkey.
  2. Open the raw script and let the extension install it: https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js (or: Tampermonkey → Create a new script → paste the file → save).
  3. Open https://www.sas.se/robots.txt and keep that tab open. It turns into the dashboard. Pin the tab so the browser doesn't unload it.

Chrome / Edge: userscripts need Developer mode enabled on chrome://extensions (or "Allow user scripts" in the Tampermonkey extension details) since Manifest V3.

Set up

On first start the script creates two example watches from ARN with a random theme, a destination that suits it and dates at its next occurrence – e.g. Christmas in Rovaniemi (20–30 Dec), Getaway weekend in Prague (a Friday → Monday a few weeks out), Ski week in Innsbruck (sportlov), Valentine's escape to Paris. Keep, edit or delete them.

In the Config section of the dashboard:

  1. Watches – each watch is two lines of fields:

    Column Meaning
    On Pause a watch without deleting it
    From / To Airport codes, comma-separated (ARN, CPH). Every From × To pair is checked
    Earliest / Latest Travel window. Outbound and return flights departing in it are found
    Cabins Economy, Premium (premium economy), Business
    Seats Seats needed on the same flight
    Stops Any, direct only, or at most 1 / 2 stops
    Both legs Only alert when an outbound has a later return in the window (default on). Off = alert for either direction on its own
    Nights Optional min / max stay, measured between the two departure dates (turns on Both legs)
  2. Notifications – add one or more targets:

    Type URL
    zulip Zulip's Slack-compatible webhook: https://<org>.zulipchat.com/api/v1/external/slack_incoming?api_key=<bot key>&stream=<channel>. Each watch posts to a topic named after it, unless you add &topic=…
    slack An incoming-webhook URL
    discord A channel webhook URL
    webhook Anything; receives POST JSON {title, body, watch, matches: [...]}

    The first time the script posts to a host, Tampermonkey asks you to allow it — choose Always allow domain.

  3. Press Save & restart, then Test notification.

  4. Optional: Baseline (no alerts) records what is available right now, so you are only told about new seats.

Unsaved edits are colour-coded until you save or discard: yellow = changed field, green = new row, red = row that will be removed on save (click ↺ to keep it).

Export, Import, Reset

The buttons at the top right:

  • Export downloads one .json file with everything: the saved config, all tracked matches (including drops and sold-out flights), recent changes, the flight cache, and view settings (collapsed watches, sort order). Unsaved edits in the config tables are not included.
  • Import restores such a file after a confirmation and replaces the current config and data. Config-only .json files from older versions are accepted too; they replace just the config and keep your matches and history.
  • Reset deletes everything, after a warning, and starts over with new random example watches. Export first if you want a backup.

Dashboard

  • Current matches per watch, sorted By departure or Newest first (most recently appeared seats on top). Click a watch's header or summary row to collapse it; this is remembered across browser restarts:
    • summary: number of flights, bookable seats per cabin with how many were lost in red (Economy ×30 −6), and when the most recent flight appeared
    • Economy ×8 −2 – 8 seats left now; the red number is how many were taken since the highest count seen (10)
    • red, struck through, gone 12 okt 13:42 – sold out (or below your seat count); kept for two weeks or until departure
    • grey, no matching return – still has seats, but its return disappeared (with Both legs)
  • Recent changes: + new / back, ▲ more seats, ▼ fewer seats, − gone, ○ lost its return
  • Log: every request and sweep

You are notified only when seats are new or come back. Drops and sold-out flights are shown, not pushed.

How it polls

  1. Calendar – /bff/award-finder/destinations/v1?origin=ARN&destinations=HND&passengers=N: one request per route pair returns every date with seats in both directions. With passengers=N it only counts flights that have N free seats.
  2. Details – /bff/award-finder/routes/v1?origin=…&destination=…&departureDate=…: only for dates in your window that have seats in your cabins, and only when that date's calendar entry changed (or every 6 h).

Requests are at least 40 s apart (+0–20 s random) and a sweep starts at most every 15 minutes; both are adjustable under Request pacing (minimum 20 s). If sas.se refuses requests, the script backs off exponentially (up to an hour), the badge turns red (blocked – reload this tab) and you get a notification. Reload the tab and complete the check if sas.se shows one.

Only one tab polls at a time, so opening the dashboard twice does not double the rate.

Limitations

  • Runs only while the browser is open and the computer awake.
  • Firefox / Chrome may unload background tabs under memory pressure; pinning the tab usually prevents it.
  • Config, matches and history live in the userscript manager's storage on that browser only. Use Export / Import to back up or move everything to another browser.

Auto-update

The script header points Tampermonkey at this repo:

// @version      1.5.0
// @updateURL    https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js
// @downloadURL  https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js

Tampermonkey periodically downloads @updateURL (default once a day; Dashboard → Settings → Script Update, or Check for userscript updates in the extension menu to force it) and installs the new file only if its @version is higher. Settings and history are kept across updates.

Requirements:

  • The raw URL must be readable without logging in. Tampermonkey cannot send Gitea credentials, so the repo (or at least this file) must be public. Check by opening the raw URL in a private window. If the repo has to stay private, publish just the .user.js somewhere public (a public mirror repo, or a public Gitea release asset) and point both URLs there.
  • The script must be installed from that URL (or have the header above) for updates to be picked up.
  • @name and @namespace must never change, or Tampermonkey treats it as a different script and the new copy starts with an empty config.

Releasing a change

  1. Edit sas-bonus-watch.user.js.
  2. Run the tests: node --test (Node 20+).
  3. Bump @version (e.g. 1.5.0 → 1.5.1). Without this, nobody gets the update.
  4. Commit and push to main.

Development

  • sas-bonus-watch.user.js – the script. The pure logic (config validation, parsing, matching, state diffing) is exported to Node when loaded with require, the browser part only runs in the userscript manager.
  • sas-bonus-watch.test.cjs – tests for the logic using response shapes captured from sas.se. Run node --test.

Nothing is sent anywhere except sas.se and the notification targets you configure.