How It Works
Architecture, data collection, validation, and predictions
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).
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:
- Fetches Prombot and YATA in parallel
- Stores a snapshot per watched item (quantity, timestamp, optional API next-restock)
- Runs
StockTransitionTrackerto detect restock and depletion events - 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
- Load all snapshots in a window around the cycle (restock − 5 min → next restock + 2 h)
- Re-detect transitions with the same 2-poll confirmation and flicker filtering
- Match the replayed cycle to the recorded one (restock time within 15 min)
- Reject restock delays shorter than 10 minutes (physically implausible)
- 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
- Hour exact — ≥ 3 samples in that UTC hour
- Hour neighbors — combine hour − 1, hour, hour + 1 (≥ 3 total)
- Global — all validated samples (≥ 2)
- 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
NextRestocktakes 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.
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)
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