diff --git a/Cargo.lock b/Cargo.lock
index 72e7089cb8c6d625f2ede39bacc35cd6346979cc..d9294e6658dbf52bbc981a9b09841fa131e5e185 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1649,6 +1649,7 @@ dependencies = [
  "kayak_font",
  "kayak_ui",
  "kira",
+ "lazy_static",
  "ldtk_rust",
  "log",
  "micro_asset_io",
diff --git a/assets/trade_manifests/whitestone.manifest.toml b/assets/trade_manifests/whitestone.manifest.toml
new file mode 100644
index 0000000000000000000000000000000000000000..dec88c9b36c865d7f0cebc0464a02459205dea46
--- /dev/null
+++ b/assets/trade_manifests/whitestone.manifest.toml
@@ -0,0 +1,169 @@
+["Spare Wheel"]
+# It takes 5 world ticks to produce a new spare wheel
+tick_decay = 0.2
+# Spare wheels will be produced up to a natural maximum of 2.
+# When under this number, the undersupply_trend will be applied
+# to the tick_decay. When equal or greater, the oversupply_trend
+# will be applied
+natural_limit = 2
+# Each entry lists the cost multiplier from the previous amount
+# up to and including the specified quantity (left part).
+# E.g. (1, 1.4) means that if the town has 0 or 1 wheels, the base
+# price is multiplied by 1.4.
+cost_multipliers = [
+	[1, 1.4],
+	[2, 1.3],
+	[3, 1.2],
+	[99999, 1],
+]
+# When there are more wheels than the natural limit (e.g. player sells a lot of wheels)
+# This strategy will be applied to the inventory. "Halt" means nothing will be done
+oversupply_trend = "halt"
+
+[Armour]
+# It takes 10 world ticks to consume one unit of armour.
+# 10 ticks = 50km
+tick_decay = 0.1
+natural_limit = 10
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+oversupply_trend = "halt"
+
+[Weapons]
+tick_decay = 0.1
+natural_limit = 10
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+["Luxury Goods"]
+tick_decay = 0.05
+natural_limit = 5
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+oversupply_trend = "halt"
+
+[Ale]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Milk]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Cheese]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Berries]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Corn]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Tomato]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Chili]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Grapes]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
+
+[Wheat]
+tick_decay = 0.25
+natural_limit = 0
+cost_multipliers = [
+	[5, 1.4],
+	[9, 1.25],
+	[10, 1],
+	[15, 0.8],
+	[30, 0.7],
+	[99999, 0.6]
+]
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 69a077322cae5ff33d77054fa96257bc61f2cfce..47be554c36dbddbff0fc7432e2a0cbaa38fa01bf 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -38,6 +38,7 @@ kayak_font.workspace = true
 fake = "2.5.0"
 directories = "4.0.1"
 bevy_prototype_lyon = "0.7.2"
+lazy_static = "1.4.0"
 
 #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
 
diff --git a/game_core/src/assets/asset_types/mod.rs b/game_core/src/assets/asset_types/mod.rs
index fc46ae21f63dbdec25ed5b559b89c5ebb3ae6276..ecb2c415973c934243541dcee86696b6eaaa967d 100644
--- a/game_core/src/assets/asset_types/mod.rs
+++ b/game_core/src/assets/asset_types/mod.rs
@@ -1 +1,2 @@
 pub mod ldtk_project;
+pub mod trade_manifest;
diff --git a/game_core/src/assets/asset_types/trade_manifest.rs b/game_core/src/assets/asset_types/trade_manifest.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e31a42f40d8d60127e62575542e8a067e88e5384
--- /dev/null
+++ b/game_core/src/assets/asset_types/trade_manifest.rs
@@ -0,0 +1,45 @@
+use std::collections::HashMap;
+
+use anyhow::Error;
+use bevy::asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset};
+
+use crate::world::{RosterEntry, TradeManifest};
+
+pub struct TradeManifestJsonLoader;
+pub struct TradeManifestTomlLoader;
+
+impl AssetLoader for TradeManifestJsonLoader {
+	fn load<'a>(
+		&'a self,
+		bytes: &'a [u8],
+		load_context: &'a mut LoadContext,
+	) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
+		Box::pin(async move {
+			let manifest: TradeManifest = serde_json::from_slice(bytes)?;
+			load_context.set_default_asset(LoadedAsset::new(manifest));
+			Ok(())
+		})
+	}
+
+	fn extensions(&self) -> &[&str] {
+		&["manifest.json"]
+	}
+}
+
+impl AssetLoader for TradeManifestTomlLoader {
+	fn load<'a>(
+		&'a self,
+		bytes: &'a [u8],
+		load_context: &'a mut LoadContext,
+	) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
+		Box::pin(async move {
+			let manifest: TradeManifest = toml::from_slice(bytes)?;
+			load_context.set_default_asset(LoadedAsset::new(manifest));
+			Ok(())
+		})
+	}
+
+	fn extensions(&self) -> &[&str] {
+		&["manifest.toml"]
+	}
+}
diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs
index cb7994a826dfd21513e9eecfcc7faf6334ae6719..01abd6336892755a98e4896a4ea57341332fbc9a 100644
--- a/game_core/src/assets/loader.rs
+++ b/game_core/src/assets/loader.rs
@@ -10,6 +10,7 @@ use micro_musicbox::prelude::AudioSource;
 
 use crate::assets::asset_types::ldtk_project::LdtkProject;
 use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig};
+use crate::world::TradeManifest;
 
 #[derive(SystemParam)]
 pub struct AssetTypeLoader<'w, 's> {
@@ -62,6 +63,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 	load_basic_type!(load_apack, APack => apacks);
 	load_basic_type!(load_ldtk, LdtkProject => ldtk);
 	load_basic_type!(load_kayak_font, KayakFont => kayak_fonts);
+	load_basic_type!(load_trade_manifest, TradeManifest => trade_manifests);
 
 	pub fn load_spritesheet(
 		&mut self,
diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs
index 33fdbf7251811e5f0c007bd560621207ea1c92d2..bd42743e9bf00651ded6a64ae54284c076f4e670 100644
--- a/game_core/src/assets/mod.rs
+++ b/game_core/src/assets/mod.rs
@@ -14,7 +14,11 @@ pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, Sprit
 
 use crate::assets::apack_handler::handle_apack_process_events;
 use crate::assets::asset_types::ldtk_project::handle_ldtk_project_events;
+use crate::assets::asset_types::trade_manifest::{
+	TradeManifestJsonLoader, TradeManifestTomlLoader,
+};
 use crate::system::flow::AppState;
+use crate::world::TradeManifest;
 
 pub struct AssetsPlugin;
 impl Plugin for AssetsPlugin {
@@ -23,7 +27,10 @@ impl Plugin for AssetsPlugin {
 			.init_resource::<LevelIndex>()
 			.init_resource::<TilesetIndex>()
 			.add_asset::<LdtkProject>()
+			.add_asset::<TradeManifest>()
 			.add_asset_loader(LdtkLoader)
+			.add_asset_loader(TradeManifestJsonLoader)
+			.add_asset_loader(TradeManifestTomlLoader)
 			.add_enter_system(AppState::Preload, startup::start_preload_resources)
 			.add_enter_system(AppState::Preload, startup::start_load_resources)
 			.add_system(handle_apack_process_events)
diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs
index 459d6ef797b678516f144b19112efa4610ba0ea7..ba4ed40f931b47a75f7e3cab1c53e4e7e4f3b35b 100644
--- a/game_core/src/assets/resources.rs
+++ b/game_core/src/assets/resources.rs
@@ -6,6 +6,7 @@ use micro_musicbox::prelude::AudioSource;
 use micro_musicbox::utilities::{SuppliesAudio, TrackType};
 
 use crate::assets::asset_types::ldtk_project::LdtkProject;
+use crate::world::TradeManifest;
 
 #[derive(Copy, Clone, Debug)]
 pub struct SpriteSheetConfig {
@@ -44,6 +45,7 @@ pub struct AssetHandles {
 	pub apacks: HashMap<String, Handle<APack>>,
 	pub ldtk: HashMap<String, Handle<LdtkProject>>,
 	pub kayak_fonts: HashMap<String, Handle<KayakFont>>,
+	pub trade_manifests: HashMap<String, Handle<TradeManifest>>,
 }
 
 macro_rules! fetch_wrapper {
@@ -76,6 +78,7 @@ impl AssetHandles {
 	fetch_wrapper!(apack, APack => apacks);
 	fetch_wrapper!(ldtk, LdtkProject => ldtk);
 	fetch_wrapper!(kayak_font, KayakFont => kayak_fonts);
+	fetch_wrapper!(trade_manifest, TradeManifest => trade_manifests);
 }
 
 impl SuppliesAudio for AssetHandles {
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index 8fd402053625ec50868410678ac35078e500cb1e..e98c997db710626dfb77125003d066a99d9184b6 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -15,6 +15,7 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) {
 	loader.load_images(&[("splash.png", "splash")]);
 	loader.load_audio(&[("splash_sting.mp3", "splash_sting")]);
 	loader.load_apack(&[("resources.apack", "resources")]);
+	loader.load_trade_manifest(&[("trade_manifests/whitestone.manifest.toml", "whitestone")]);
 }
 
 pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) {
diff --git a/game_core/src/const_data/mod.rs b/game_core/src/const_data/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..94d05aff269bf82edc98981a196fbcd1178a8de7
--- /dev/null
+++ b/game_core/src/const_data/mod.rs
@@ -0,0 +1,25 @@
+//! Data embedded in the game for the sake of convenience. Future iterations
+//! should load this info as assets, to make it easier to customise & reload
+
+use std::collections::HashMap;
+
+use lazy_static::lazy_static;
+
+use crate::world::TradeGood;
+
+lazy_static! {
+	pub static ref TRADE_GOODS: HashMap<String, TradeGood> =
+		toml::from_slice(include_bytes!("../../../raw_assets/trade_goods.toml")).unwrap();
+}
+
+/// Convert the name of a trade good into its representation.
+/// Will panic if the name is incorrect; usually useful in a
+/// static context
+pub fn get_goods_from_name(name: impl ToString) -> TradeGood {
+	(*TRADE_GOODS)[&name.to_string()].clone()
+}
+/// Convert the name of a trade good into its representation.
+/// Preferred in a dynamic context where the `Option` can be checked
+pub fn get_goods_from_name_checked(name: impl ToString) -> Option<TradeGood> {
+	(*TRADE_GOODS).get(&name.to_string()).cloned()
+}
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index 1d04c0833aae12242d6d22dcf48034e0d1e772a0..0bc9aa3fe44e4e076d97eb29670a3ea1a36e7604 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1,6 +1,7 @@
 #![feature(result_option_inspect)]
 
 pub mod assets;
+pub mod const_data;
 pub mod multiplayer;
 pub mod persistance;
 pub mod splash_screen;
diff --git a/game_core/src/persistance/save_file.rs b/game_core/src/persistance/save_file.rs
index a4096406ebe6278a94bfb70166606f1d2c9ae979..681702d2351b8eb64bcc2d15f5f6dcf8ebbce591 100644
--- a/game_core/src/persistance/save_file.rs
+++ b/game_core/src/persistance/save_file.rs
@@ -1,12 +1,9 @@
+use std::collections::HashMap;
 use std::fs;
 use std::fs::File;
-use std::io::Write;
-use std::ops::Deref;
-use std::path::Path;
 
 use bevy::math::Vec3;
 use bevy::prelude::*;
-use bevy::utils::HashMap;
 use iyes_loopless::state::NextState;
 use serde::{Deserialize, Serialize};
 
@@ -14,12 +11,15 @@ use crate::persistance::fs_utils::{get_root_save_dir, AUTOSAVE_NAME};
 use crate::states::Player;
 use crate::system::flow::AppState;
 use crate::world::{
-	CurrentResidence, DistanceTravelled, EncounterState, HungerState, PendingLoadState,
-	TradingState, TravelPath, TravelTarget,
+	ActiveLevel, CurrentResidence, DistanceTravelled, EncounterState, HungerState,
+	PendingLoadState, TownName, TradeManifestTickState, TradingState, TravelPath, TravelTarget,
 };
 
+pub type TownPersistenceData = HashMap<String, (TradingState, HungerState, TradeManifestTickState)>;
+
 #[derive(Serialize, Deserialize, Debug, Resource)]
 pub struct PersistenceState {
+	pub map_name: String,
 	pub player_location: Vec3,
 	pub player_inventory: TradingState,
 	pub player_hunger: HungerState,
@@ -31,7 +31,7 @@ pub struct PersistenceState {
 	pub player_total_distance: DistanceTravelled,
 	pub previous_location: CurrentResidence,
 	pub encounter_state: EncounterState,
-	pub town_states: HashMap<String, (TradingState, HungerState)>,
+	pub town_states: TownPersistenceData,
 }
 
 #[derive(Clone, Debug)]
@@ -75,8 +75,25 @@ pub fn sync_state_to_persistence(
 	mut commands: Commands,
 	mut state: Option<ResMut<PersistenceState>>,
 	player_query: PlayerInfoQuery,
+	town_query: Query<(
+		&TownName,
+		&TradingState,
+		&HungerState,
+		&TradeManifestTickState,
+	)>,
 	encounters: Res<EncounterState>,
+	active_map: Res<ActiveLevel>,
 ) {
+	let town_states = town_query
+		.iter()
+		.map(|(name, trading, hunger, manifest)| {
+			(
+				name.0.clone(),
+				(trading.clone(), hunger.clone(), manifest.clone()),
+			)
+		})
+		.collect::<TownPersistenceData>();
+
 	match state {
 		Some(mut state) => {
 			if let Some((
@@ -90,6 +107,7 @@ pub fn sync_state_to_persistence(
 			)) = player_query.iter().next()
 			{
 				*state = PersistenceState {
+					map_name: active_map.map.clone(),
 					player_location: transform.translation,
 					player_inventory: trading.clone(),
 					player_hunger: hunger.clone(),
@@ -98,7 +116,7 @@ pub fn sync_state_to_persistence(
 					travel_target: maybe_target.cloned(),
 					previous_location: residence.clone(),
 					encounter_state: encounters.clone(),
-					town_states: Default::default(),
+					town_states,
 				};
 			}
 		}
@@ -114,6 +132,7 @@ pub fn sync_state_to_persistence(
 			)) = player_query.iter().next()
 			{
 				commands.insert_resource(PersistenceState {
+					map_name: active_map.map.clone(),
 					player_location: transform.translation,
 					player_inventory: trading.clone(),
 					player_hunger: hunger.clone(),
@@ -122,7 +141,7 @@ pub fn sync_state_to_persistence(
 					travel_target: maybe_target.cloned(),
 					previous_location: residence.clone(),
 					encounter_state: encounters.clone(),
-					town_states: Default::default(),
+					town_states,
 				})
 			}
 		}
diff --git a/game_core/src/system/utilities.rs b/game_core/src/system/utilities.rs
index 1c5028cbd538c766c7bc5c3165e783dbcf9c8464..93566302b274511019715442d6146117a187e0ad 100644
--- a/game_core/src/system/utilities.rs
+++ b/game_core/src/system/utilities.rs
@@ -144,6 +144,9 @@ where
 	}
 }
 
+pub fn format_distance(distance: f32) -> f32 {
+	distance / 10.0
+}
 pub fn format_ui_distance(distance: f32) -> String {
 	format!("{:.0} km", distance / 10.0)
 }
diff --git a/game_core/src/ui/components/inset_icon.rs b/game_core/src/ui/components/inset_icon.rs
index e05e25dbdb4d33edc7297a4563dfe351a4c52121..3d080d95feabed5a7534a811ca9474a68f4cb3cb 100644
--- a/game_core/src/ui/components/inset_icon.rs
+++ b/game_core/src/ui/components/inset_icon.rs
@@ -8,6 +8,7 @@ use crate::parent_widget;
 use crate::ui::prelude::*;
 
 #[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
 pub enum IconContent {
 	Image(String),
 	Atlas(String, usize),
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
index 91dbefcd4547fddabc1298d87249b311cf90c048..971f5d44bf368db28e93286880057cc363aa1bc3 100644
--- a/game_core/src/world/mod.rs
+++ b/game_core/src/world/mod.rs
@@ -14,14 +14,18 @@ mod world_query;
 
 pub use encounters::{EncounterState, WorldZones};
 pub use spawning::PendingLoadState;
-pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget};
-pub use trading::{HungerState, ItemName, TradeGood, TradingState};
+pub use towns::{CurrentResidence, PathingResult, TownName, TownPaths, TravelPath, TravelTarget};
+pub use trading::{
+	HungerState, ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState,
+	TradeRoster, TradingState,
+};
 pub use travel::DistanceTravelled;
+pub use utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked};
 pub use world_query::{CameraBounds, MapQuery};
 
 use crate::system::flow::AppState;
 use crate::world::spawning::PopulateWorldEvent;
-use crate::world::utils::ActiveLevel;
+use crate::world::travel::WorldTickEvent;
 
 pub struct WorldPlugin;
 impl Plugin for WorldPlugin {
@@ -30,6 +34,7 @@ impl Plugin for WorldPlugin {
 			.init_resource::<WorldZones>()
 			.init_resource::<EncounterState>()
 			.add_event::<PopulateWorldEvent>()
+			.add_event::<WorldTickEvent>()
 			.add_enter_system(AppState::InGame, |mut commands: Commands| {
 				commands.insert_resource(ActiveLevel {
 					map: String::from("Grantswaith"),
@@ -44,8 +49,9 @@ impl Plugin for WorldPlugin {
 					.with_system(spawning::spawn_world_data)
 					.with_system(spawning::populate_world)
 					.with_system(travel::tick_travelling_merchant)
+					.with_system(travel::track_travel)
 					.with_system(encounters::notify_new_zone)
-
+					.with_system(trading::tick_manifests)
 					.into(),
 			);
 	}
diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs
index 73240a8920a8e59b75d644c6f4c26f1dc0864d3a..58e64efdd0401d51aec3542120d1d6afc8e7c648 100644
--- a/game_core/src/world/spawning.rs
+++ b/game_core/src/world/spawning.rs
@@ -1,5 +1,8 @@
+use std::collections::HashMap;
+
 use bevy::prelude::*;
 use bevy_ecs_tilemap::prelude::*;
+use ldtk_rust::EntityInstance;
 use micro_banimate::definitions::SimpleAnimationBundle;
 use num_traits::AsPrimitive;
 use serde_json::Value;
@@ -9,11 +12,14 @@ use crate::persistance::PersistenceState;
 use crate::states::Player;
 use crate::system::camera::ChaseCam;
 use crate::world::encounters::WorldZones;
-use crate::world::towns::{CurrentResidence, TownPaths};
+use crate::world::towns::{CurrentResidence, TownBundle, TownPaths};
 use crate::world::travel::DistanceTravelled;
 use crate::world::utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32};
 use crate::world::world_query::MapQuery;
-use crate::world::{EncounterState, HungerState, TradingState, TravelPath, TravelTarget};
+use crate::world::{
+	EncounterState, HungerState, TownName, TradeManifestTickState, TradingState, TravelPath,
+	TravelTarget,
+};
 
 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
 pub struct PopulateWorldEvent;
@@ -280,6 +286,61 @@ pub fn populate_world(
 		}
 
 		let world_zones = WorldZones::from_entities(level.px_hei, MapQuery::get_entities_of(level));
+		MapQuery::get_entities_of(level)
+			.into_iter()
+			.filter(|instance| instance.identifier == *"Town")
+			.map(|instance| {
+				let mut name = String::new();
+				let mut manifest_name = String::new();
+
+				instance
+					.field_instances
+					.iter()
+					.for_each(|inst| match &*inst.identifier {
+						"name" => name = inst.value.as_ref().unwrap().as_str().unwrap().to_string(),
+						"trade_manifest" => {
+							manifest_name = inst
+								.value
+								.as_ref()
+								.map(|val| String::from(val.as_str().unwrap()))
+								.unwrap_or_default()
+						}
+						_ => {}
+					});
+
+				if manifest_name.is_empty() {
+					(name, String::from("whitestone"))
+				} else {
+					(name, manifest_name)
+				}
+			})
+			.map(|(name, manifest)| {
+				match pending_load
+					.as_ref()
+					.and_then(|pl| pl.0.town_states.get(&name))
+				{
+					Some((trade, hunger, tick)) => TownBundle {
+						town_name: TownName(name.clone()),
+						manifest: assets.trade_manifest(manifest),
+						trade_state: trade.clone(),
+						hunger_state: hunger.clone(),
+						manifest_state: tick.clone(),
+					},
+					None => TownBundle {
+						town_name: TownName(name.clone()),
+						manifest: assets.trade_manifest(manifest),
+						trade_state: TradingState {
+							gold: 250,
+							items: HashMap::default(),
+						},
+						hunger_state: HungerState::default(),
+						manifest_state: TradeManifestTickState::default(),
+					},
+				}
+			})
+			.for_each(|bundle| {
+				commands.spawn((WorldLinked, bundle));
+			});
 
 		commands.insert_resource(trade_routes);
 		commands.insert_resource(world_zones);
diff --git a/game_core/src/world/towns.rs b/game_core/src/world/towns.rs
index 39921b899957ef3cb050096b27710ce33a9137c4..d696048daeeb76869a5626f6870f644d8af3ebe0 100644
--- a/game_core/src/world/towns.rs
+++ b/game_core/src/world/towns.rs
@@ -1,11 +1,13 @@
 use bevy::math::{vec2, Vec2};
-use bevy::prelude::{Bundle, Component, Resource};
+use bevy::prelude::{Bundle, Component, Handle, Resource};
 use bevy::utils::HashMap;
 use ldtk_rust::{EntityInstance, Level};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 
+use crate::world::trading::TradeManifestTickState;
 use crate::world::utils::{grid_to_px, px_to_grid};
+use crate::world::{HungerState, TradeManifest, TradingState};
 
 pub type Town = String;
 
@@ -34,6 +36,9 @@ impl CurrentResidence {
 #[derive(Component, Serialize, Deserialize, Debug, Clone)]
 pub struct TravelTarget(pub String);
 
+#[derive(Component, Serialize, Deserialize, Debug, Clone)]
+pub struct TownName(pub String);
+
 #[derive(Component, Serialize, Deserialize, Debug, Clone)]
 pub struct TravelPath {
 	pub path: Vec<Vec2>,
@@ -257,3 +262,12 @@ impl From<Vec<&EntityInstance>> for TownPaths {
 		TownPaths { routes }
 	}
 }
+
+#[derive(Bundle)]
+pub struct TownBundle {
+	pub town_name: TownName,
+	pub trade_state: TradingState,
+	pub hunger_state: HungerState,
+	pub manifest: Handle<TradeManifest>,
+	pub manifest_state: TradeManifestTickState,
+}
diff --git a/game_core/src/world/trading.rs b/game_core/src/world/trading.rs
index 6b70332c73d832cb124840b9125af521daca31a0..cc43cc5f6dfd692173fb5c48e38fc21cff84f6bb 100644
--- a/game_core/src/world/trading.rs
+++ b/game_core/src/world/trading.rs
@@ -1,10 +1,15 @@
-use bevy::prelude::{Component, Resource};
-use bevy::utils::hashbrown::hash_map::Entry;
-use bevy::utils::HashMap;
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::hash::{Hash, Hasher};
+use std::ops::{Deref, DerefMut};
+
+use bevy::prelude::{Assets, Component, EventReader, Handle, Query, Res, Resource};
+use bevy::reflect::TypeUuid;
 use num_traits::AsPrimitive;
 use serde::{Deserialize, Serialize};
 
 use crate::ui::components::IconContent;
+use crate::world::travel::WorldTickEvent;
 
 pub type ItemName = String;
 
@@ -24,6 +29,94 @@ pub struct HungerState {
 pub struct TradeGood {
 	pub name: String,
 	pub icon: IconContent,
+	pub food_value: usize,
+}
+
+impl PartialEq for TradeGood {
+	fn eq(&self, other: &Self) -> bool {
+		self.name == other.name
+	}
+}
+impl PartialEq<String> for TradeGood {
+	fn eq(&self, other: &String) -> bool {
+		&self.name == other
+	}
+}
+impl Eq for TradeGood {}
+impl Hash for TradeGood {
+	fn hash<H: Hasher>(&self, state: &mut H) {
+		self.name.hash(state)
+	}
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RosterSupplyTrend {
+	#[serde(alias = "converge")]
+	#[default]
+	Converge,
+	#[serde(alias = "consume")]
+	Consume,
+	#[serde(alias = "produce")]
+	Produce,
+	#[serde(alias = "halt")]
+	Halt,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct RosterEntry {
+	pub tick_decay: f32,
+	pub natural_limit: usize,
+	pub cost_multipliers: Vec<(usize, f32)>,
+	#[serde(default)]
+	pub undersupply_trend: RosterSupplyTrend,
+	#[serde(default)]
+	pub oversupply_trend: RosterSupplyTrend,
+}
+
+#[derive(Debug, Clone, PartialEq, Default, TypeUuid, Serialize, Deserialize)]
+#[serde(from = "HashMap<String, RosterEntry>")]
+#[uuid = "7e395a74-7745-11ed-8b4e-ef01db57341b"]
+pub struct TradeManifest(HashMap<String, RosterEntry>);
+impl From<HashMap<String, RosterEntry>> for TradeManifest {
+	fn from(value: HashMap<String, RosterEntry>) -> Self {
+		Self(value)
+	}
+}
+impl Deref for TradeManifest {
+	type Target = HashMap<String, RosterEntry>;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for TradeManifest {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Component)]
+pub struct TradeManifestTickState(HashMap<String, f32>);
+impl From<HashMap<String, f32>> for TradeManifestTickState {
+	fn from(value: HashMap<String, f32>) -> Self {
+		Self(value)
+	}
+}
+impl Deref for TradeManifestTickState {
+	type Target = HashMap<String, f32>;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for TradeManifestTickState {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+#[derive(Debug, Clone, PartialEq, Component)]
+pub struct TradeRoster {
+	pub goods_relations: HashMap<TradeGood, RosterEntry>,
+	pub production_ticks: HashMap<TradeGood, f32>,
 }
 
 impl TradingState {
@@ -94,3 +187,67 @@ impl TradingState {
 		}
 	}
 }
+
+pub fn tick_manifests(
+	events: EventReader<WorldTickEvent>,
+	mut town_query: Query<(
+		&mut TradingState,
+		&Handle<TradeManifest>,
+		&mut TradeManifestTickState,
+	)>,
+	manifests: Res<Assets<TradeManifest>>,
+) {
+	if events.is_empty() {
+		return;
+	}
+	log::info!("TICK MANIFESTS");
+
+	for (mut state, manifest, mut tick_state) in &mut town_query {
+		if let Some(manifest) = manifests.get(manifest) {
+			for (good, entry) in manifest.iter() {
+				let quantity = *state.items.get(good).to_owned().unwrap_or(&0);
+
+				let ts = tick_state.entry(good.clone()).or_default();
+
+				match if quantity >= entry.natural_limit {
+					&entry.oversupply_trend
+				} else {
+					&entry.undersupply_trend
+				} {
+					RosterSupplyTrend::Converge => {
+						if quantity != entry.natural_limit {
+							*ts += entry.tick_decay;
+							while ts > &mut 1.0 {
+								*ts -= 1.0;
+								if quantity > entry.natural_limit {
+									let mut item = state.items.entry(good.clone()).or_insert(0);
+									*item = item.saturating_sub(1);
+								} else {
+									let mut item = state.items.entry(good.clone()).or_insert(0);
+									*item = item.saturating_add(1);
+								}
+							}
+						}
+					}
+					RosterSupplyTrend::Consume => {
+						*ts += entry.tick_decay;
+						while ts > &mut 1.0 {
+							*ts -= 1.0;
+							let mut item = state.items.entry(good.clone()).or_insert(0);
+							*item = item.saturating_sub(1);
+						}
+					}
+					RosterSupplyTrend::Produce => {
+						*ts += entry.tick_decay;
+						while ts > &mut 1.0 {
+							*ts -= 1.0;
+							let mut item = state.items.entry(good.clone()).or_insert(0);
+							*item = item.saturating_add(1);
+						}
+					}
+					RosterSupplyTrend::Halt => {}
+				}
+			}
+		}
+	}
+}
diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs
index 6d58b11cf9334954d22efcb528afeef4d617e503..e9e6e37948293be6b76011d374a6d328c95e26ce 100644
--- a/game_core/src/world/travel.rs
+++ b/game_core/src/world/travel.rs
@@ -1,16 +1,29 @@
-use std::ops::SubAssign;
+use std::ops::{Deref, DerefMut, SubAssign};
 
 use bevy::math::Vec3Swizzles;
 use bevy::prelude::*;
 use serde::{Deserialize, Serialize};
 
 use crate::persistance::SaveFileEvent;
+use crate::states::Player;
+use crate::system::utilities::format_distance;
 use crate::world::encounters::EncounterState;
 use crate::world::towns::{CurrentResidence, TravelPath, TravelTarget};
 use crate::world::PathingResult;
 
 #[derive(Component, Serialize, Deserialize, Debug, Copy, Clone, Default)]
 pub struct DistanceTravelled(pub f32);
+impl Deref for DistanceTravelled {
+	type Target = f32;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for DistanceTravelled {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
 
 pub fn tick_travelling_merchant(
 	mut commands: Commands,
@@ -56,3 +69,26 @@ pub fn tick_travelling_merchant(
 		}
 	}
 }
+
+pub struct WorldTickEvent;
+
+pub fn track_travel(
+	mut tracked: Local<DistanceTravelled>,
+	player_query: Query<&DistanceTravelled, With<Player>>,
+	mut events: EventWriter<WorldTickEvent>,
+) {
+	let before = **tracked.deref();
+	let after = {
+		if let Ok(player_dist) = player_query.get_single() {
+			*tracked = *player_dist;
+			*player_dist.deref()
+		} else {
+			before
+		}
+	};
+
+	if format_distance(before) % 5.0 > format_distance(after) % 5.0 {
+		log::info!("ANOTHER 5KM HAS PASSED");
+		events.send(WorldTickEvent);
+	}
+}
diff --git a/raw_assets/trade_goods.toml b/raw_assets/trade_goods.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a92a5d5485603c743570b002db62b7ec6cfde9fa
--- /dev/null
+++ b/raw_assets/trade_goods.toml
@@ -0,0 +1,77 @@
+["Armour"]
+name = "Armour"
+icon = ["icons", 12]
+food_value = 0
+gold_value = 12
+
+["Weapons"]
+name = "Weapons"
+icon = ["icons", 1]
+food_value = 0
+gold_value = 13
+
+["Luxury Goods"]
+name = "Luxury Goods"
+icon = ["icons", 13]
+food_value = 0
+gold_value = 27
+
+["Ale"]
+name = "Ale"
+icon = ["icons", 11]
+food_value = 0
+gold_value = 6
+
+["Milk"]
+name = "Milk"
+icon = ["icons", 2]
+food_value = 2
+gold_value = 5
+
+["Cheese"]
+name = "Cheese"
+icon = ["icons", 3]
+food_value = 3
+gold_value = 6
+
+["Spare Wheel"]
+name = "Spare Wheel"
+icon = ["icons", 28]
+food_value = 0
+gold_value = 125
+
+["Berries"]
+name = "Berries"
+icon = ["icons", 27]
+food_value = 1
+gold_value = 4
+
+["Corn"]
+name = "Corn"
+icon = ["icons", 26]
+food_value = 2
+gold_value = 6
+
+["Tomato"]
+name = "Tomato"
+icon = ["icons", 25]
+food_value = 2
+gold_value = 6
+
+["Chili"]
+name = "Chili"
+icon = ["icons", 24]
+food_value = 2
+gold_value = 8
+
+["Grapes"]
+name = "Grapes"
+icon = ["icons", 23]
+food_value = 1
+gold_value = 9
+
+["Wheat"]
+name = "Wheat"
+icon = ["icons", 22]
+food_value = 1
+gold_value = 3