Overview

Torn Travel is a self-hosted assistant for abroad travel in Torn City. It watches shop stock per destination, learns how long items stay in stock and how long restocks take, then predicts whether you will find stock on arrival.

Currently tracked: Hawaii (Shark Fin, Turtle Shell), Switzerland (Neumune Tablet), China (Pangolin Scales), and Canada (Xanax).

Flight times — Hawaii (regular / with book): Standard 2:07 / 1:35, Airstrip 1:29 / 1:07, WLT 1:04 / 0:48, Business 0:38 / 0:29 · Switzerland (regular / with book): Standard 2:55 / 2:12, Airstrip 2:03 / 1:32, WLT 1:28 / 1:06, Business 0:53 / 0:39 · China (regular / with book): Standard 3:49 / 2:52, Airstrip 2:40 / 2:00, WLT 1:54 / 1:26, Business 1:09 / 0:52 · Canada (regular / with book): Standard 0:39 / 0:29, Airstrip 0:27 / 0:20, WLT 0:19 / 0:15, Business 0:12 / 0:09 · Poll interval: 30 s · Data file: Data/stock-history.db

Data Sources

The app does not use your Torn API key. Stock data comes from two public community aggregators, polled in parallel every 30 seconds:

Source Endpoint Role
Prometheus Bot prombot.co.uk:8443/api/travel Primary source. Also provides NextRestock timestamps used in live predictions.
YATA yata.yt/api/v1/travel/export/ Secondary source for cross-checking. If YATA disagrees with a transition, the event is ignored.

Both APIs are fed by players visiting abroad shops (userscripts report stock anonymously). They are not official Torn endpoints and can occasionally return stale or noisy values.

Real-Time Collection

TravelStateService runs as a background service. On each refresh it:

  1. Fetches Prombot and YATA in parallel
  2. Stores a snapshot per watched item (quantity, timestamp, optional API next-restock)
  3. Runs StockTransitionTracker to detect restock and depletion events
  4. Updates departure recommendations via TravelPredictor

What is a snapshot?

A point-in-time record: destination, item, quantity, and when it was observed. Snapshots are kept for 30 days (rolling window) and are the raw material used by the delayed validator to replay history.

What is a cycle?

A restock cycle is one sell-out window:

restock (qty > 0) → depletion (qty = 0) → [optional] next restock time

From this we derive two learned metrics:

  • Sell-out duration — minutes from restock to depletion
  • Restock delay — minutes from depletion until the next restock

Transition Detection (Live)

A single poll can lie (API flicker). The tracker therefore requires 2 consecutive polls before recording an event:

Event Rule
Restock confirmed 2 polls with qty > 0 after being out of stock. YATA must not show 0 on the confirming poll.
Depletion confirmed 2 polls with qty = 0 during an active sell-out. YATA must not show stock on the confirming poll.
Flicker ignored If qty hits 0 for one poll then rebounds above 50% of the previous quantity, the zero is treated as noise.

New cycles start as Provisional until the validator reviews them.

Delayed Validator

CycleReconciliationService runs every 10 minutes. Cycles older than 1 hour(s) are replayed using stored snapshots with stricter rules — no new API calls, no Torn key required.

Replay algorithm

  1. Load all snapshots in a window around the cycle (restock − 5 min → next restock + 2 h)
  2. Re-detect transitions with the same 2-poll confirmation and flicker filtering
  3. Match the replayed cycle to the recorded one (restock time within 15 min)
  4. Reject restock delays shorter than 10 minutes (physically implausible)
  5. Update timestamps or mark the cycle rejected

Validation statuses

Status Meaning Used for learning
Provisional Recorded live, not yet reviewed Sell-out yes · Restock delay no
Validated Confirmed by snapshot replay Sell-out yes · Restock delay yes (if delay ≥ 10 min)
Rejected Could not be confirmed (e.g. single-poll flicker, no matching replay) Excluded entirely

Why reject 1-minute sell-outs with qty = 2? The validator replays the timeline: if stock appears on only one poll then returns to zero for 30+ minutes, it is classified as API noise — not a real rush buy. Short sell-outs with sustained stock across multiple polls can still be validated when the replay finds a matching cycle.

Learning Engine

StockStore computes hourly profiles per item. For each UTC hour (0–23) it tracks average sell-out and restock delay.

Sample priority

  1. Hour exact — ≥ 3 samples in that UTC hour
  2. Hour neighbors — combine hour − 1, hour, hour + 1 (≥ 3 total)
  3. Global — all validated samples (≥ 2)
  4. Default — TornPDA fallback values until enough data exists
Item Default sell-out Default restock delay
Shark Fin 20 min 97 min
Turtle Shell 20 min 63 min
Neumune Tablet 10 min 516 min
Pangolin Scales 20 min 30 min
Xanax 45 min 493 min (8:13)

Arrival Predictions

StockTimelineSimulator simulates stock between now and your arrival time, stepping through predicted restock and depletion events using learned durations.

Anchored on reality, not averages alone

  • The current cycle uses the observed restock time (when it actually happened), not the observation time — stock that has been on the shelf for 15 minutes is treated as 15 minutes old.
  • When enough recent polls exist, the depletion time of the current stock is estimated from the live sell rate (slope of the quantity), which beats historical averages.
  • Predicted restocks snap to Torn's :00 / :15 / :30 / :45 UTC slots, and the API's announced NextRestock takes priority when present.

Safety margins

Every predicted event carries an accumulated timing uncertainty (more predicted cycles = more uncertainty). An item only counts as "in stock on arrival" when the arrival clears both window edges — comfortably after the predicted restock and comfortably before the predicted sell-out. Arrivals that land too close to an edge are rejected as too risky.

Check the Accuracy page to see how these predictions perform against reality.

Two separate predictions on the Home page

Block Question answered
Recommendation (top) When should you leave? Scans wait = 0…360 min for the first departure where at least one item is in stock on arrival. Includes why wait X min reasons comparing leave-now vs recommended departure.
If You Leave Now What happens if you depart immediately? Uses now + selected flight time. When the recommendation is green, a departure window bar shows time remaining, urgency, and how long the window has been open.

Restock times in simulation use Prombot's NextRestock API field when available, otherwise the learned hourly delay (snapped to the next 15-min slot), then defaults.

Prediction Accuracy Tracking

Every 10 minutes, PredictionAccuracyService logs — for each destination, item, and flight duration — whether the simulator predicts stock at the corresponding arrival time. Once that time has passed, the nearest recorded snapshot (within 3 min) reveals what actually happened.

The Accuracy page shows the hit rate per flight duration, and highlights the painful case specifically: predicted in stock, but actually empty on arrival.

Architecture Diagram

Every 30 s
  ├── PrombotClient ──┐
  └── YataClient ─────┤
                      ▼
            TravelStateService
              ├── StockStore (snapshots + cycles → SQLite)
              ├── StockTransitionTracker (live events)
              └── TravelPredictor → Home UI

Every 10 min (+ startup retro-pass)
  └── CycleReconciliationService
        └── CycleTimelineAnalyzer (replay snapshots → validate / correct / reject)

Persistence

All learned data lives in Data/stock-history.db:

  • Cycles — permanent history of restock → depletion events
  • RecentSnapshots — last 30 days of raw polls (purged automatically)

The validator only needs local snapshots — it never re-fetches historical data from Prombot or YATA.

Display timezone

All internal logic — restock slots, hourly learning, predictions, and stored timestamps — uses UTC, because Torn restocks at :00, :15, :30, and :45 UTC.

The UI displays times in your chosen offset (e.g. 26/08 05:41 UTC+2). When the offset is not UTC, the reference UTC time appears in parentheses where useful. Change the offset on the Home page.

  • Setting is stored in your browser localStorage (per device, not on the server)
  • Current offset: UTC+2 (default UTC+2 until you change it)
  • Changing the offset does not affect predictions, learned hourly profiles, or cycle data
  • Plane type and travel book choices per destination are also saved in localStorage

Dirty bombs & travel blocks

Torn City occasionally enters a state of emergency or faction dirty-bomb events that block all departures from Torn City for a period (often several hours, roughly once a month).

Torn Travel does not detect these events. Recommendations assume you can leave at any minute within the scanned window (now → +6 hours).

  • During a travel block, “Leave now” or “Leave in X min” may be impossible even if stock abroad looks good
  • Abroad stock timing on the chart may still be useful — only your departure timing is wrong
  • Use your own judgment when travel is blocked; do not rely on departure advice until flights reopen

Why no auto-detect? The app uses community stock APIs only (no Torn API key). Travel-block status would require manual input or an optional API integration not implemented today.

Known Limitations

  • Hawaii, Switzerland, China, and Canada are supported today (5 items total)
  • Community APIs can flicker or lag; cross-validation and delayed replay mitigate but do not eliminate noise
  • Very fast sell-outs (< 60 s) may be rejected by replay because confirmation requires 2 polls (90 s apart)
  • Hourly profiles need several days of coverage before all 24 UTC hours are reliable
  • Predictions are probabilistic — they improve as validated cycle count grows
  • Dirty bombs / travel blocks are not modeled — see section above
An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.