diff --git a/typed-doc/copy-to-clipboard-feature.tsx b/typed-doc/copy-to-clipboard-feature.tsx
new file mode 100644
index 0000000..3e422ce
--- /dev/null
+++ b/typed-doc/copy-to-clipboard-feature.tsx
@@ -0,0 +1,102 @@
+/**
+ * Issue #322: Add copy-to-clipboard on recommended fee
+ *
+ * Implements single-click copy of the raw stroop value from the
+ * RecommendationPanel, with "Copied!" feedback and graceful fallback.
+ */
+
+// packages/ui/src/components/dashboard/RecommendationPanel.tsx
+import { useState, useCallback } from "react";
+
+interface RecommendationPanelProps {
+ recommendedFee: number; // raw stroop value (e.g. 3849)
+ lowFee: number;
+ mediumFee: number;
+ highFee: number;
+}
+
+export function RecommendationPanel({
+ recommendedFee,
+ lowFee,
+ mediumFee,
+ highFee,
+}: RecommendationPanelProps) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(String(recommendedFee));
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ } catch {
+ // clipboard API unavailable — fallback
+ const ta = document.createElement("textarea");
+ ta.value = String(recommendedFee);
+ ta.style.position = "fixed";
+ ta.style.opacity = "0";
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }
+ }, [recommendedFee]);
+
+ return (
+
+
+ Recommended Fee
+
+
+
+
+ {recommendedFee.toLocaleString()}
+
+ stroops
+
+
+
+
+
+
Low: {lowFee.toLocaleString()}
+
Medium: {mediumFee.toLocaleString()}
+
High: {highFee.toLocaleString()}
+
+
+ );
+}
+
+// Icons (inline to avoid import complexity)
+function CopyIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+function CheckIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/typed-doc/simulation-instrumentation.rs b/typed-doc/simulation-instrumentation.rs
new file mode 100644
index 0000000..a3663df
--- /dev/null
+++ b/typed-doc/simulation-instrumentation.rs
@@ -0,0 +1,127 @@
+/**
+ * Issue #340: Add span instrumentation to simulation module
+ *
+ * Wraps all public simulation functions with tracing spans using
+ * #[tracing::instrument] for observability of the fee simulation pipeline.
+ *
+ * Requirements:
+ * - Each span includes: function name, input config fields, output sample count
+ * - Spans visible in both JSON and human log formats
+ */
+
+// packages/devkit/src/simulation/fee_model.rs
+use tracing::instrument;
+
+/// Parameters controlling fee model behaviour.
+#[derive(Debug)]
+pub struct FeeModelConfig {
+ pub min_fee: u64,
+ pub max_fee: u64,
+ pub base_reserve: u64,
+ pub ledger_capacity: u64,
+}
+
+/// Output from the fee computation.
+pub struct FeeEstimate {
+ pub recommended: u64,
+ pub low: u64,
+ pub medium: u64,
+ pub high: u64,
+ pub sample_count: usize,
+}
+
+impl FeeModel {
+ /// Compute fee estimates from the current ledger state.
+ #[instrument(skip(self), fields(config = ?self.config))]
+ pub fn estimate(&self, ledger: &LedgerState) -> FeeEstimate {
+ let samples = self.collect_samples(ledger);
+ let sorted = self.sort_samples(samples);
+
+ let estimate = FeeEstimate {
+ recommended: sorted.percentile(50),
+ low: sorted.percentile(10),
+ medium: sorted.percentile(25),
+ high: sorted.percentile(75),
+ sample_count: sorted.len(),
+ };
+
+ tracing::info!(
+ sample_count = estimate.sample_count,
+ recommended = estimate.recommended,
+ "fee estimate computed"
+ );
+
+ estimate
+ }
+}
+
+// packages/devkit/src/simulation/network_load.rs
+use tracing::instrument;
+
+impl NetworkLoad {
+ /// Estimate the current network congestion level as a fraction 0.0 … 1.0.
+ #[instrument(skip(self))]
+ pub fn estimate_current_load(&self, recent_txs: &[Transaction]) -> f64 {
+ if recent_txs.is_empty() {
+ tracing::warn!("no recent transactions to estimate load");
+ return 0.0;
+ }
+
+ let load = self.compute_load(recent_txs);
+ let clamped = load.clamp(0.0, 1.0);
+
+ tracing::debug!(
+ raw_load = load,
+ clamped_load = clamped,
+ tx_count = recent_txs.len(),
+ "network load estimated"
+ );
+
+ clamped
+ }
+}
+
+// packages/devkit/src/simulation/congestion_predictor.rs
+use tracing::instrument;
+
+pub enum CongestionLevel {
+ Low,
+ Medium,
+ High,
+ Extreme,
+}
+
+impl CongestionPredictor {
+ /// Predict short-term congestion based on recent and historical data.
+ #[instrument(skip(self, recent_loads), fields(window = recent_loads.len()))]
+ pub fn predict_congestion_short_term(
+ &self,
+ recent_loads: &[f64],
+ historical_avg: f64,
+ ) -> CongestionLevel {
+ let current_avg = recent_loads.iter().copied().sum::()
+ / recent_loads.len().max(1) as f64;
+
+ tracing::debug!(
+ current_avg = current_avg,
+ historical_avg = historical_avg,
+ "congestion prediction computed"
+ );
+
+ if current_avg > historical_avg * 1.5 {
+ CongestionLevel::High
+ } else if current_avg > historical_avg * 1.2 {
+ CongestionLevel::Medium
+ } else {
+ CongestionLevel::Low
+ }
+ }
+
+ /// Predict long-term congestion trend.
+ #[instrument(skip(self))]
+ pub fn predict_congestion_trend(&self, loads: &[f64]) -> f64 {
+ let trend = self.compute_trend(loads);
+ tracing::info!(trend = trend, "long-term congestion trend");
+ trend
+ }
+}
diff --git a/typed-doc/simulation-test-plan.ts b/typed-doc/simulation-test-plan.ts
new file mode 100644
index 0000000..a1a3b43
--- /dev/null
+++ b/typed-doc/simulation-test-plan.ts
@@ -0,0 +1,60 @@
+/**
+ * Issue #338: Test plan — fee recommendation & monitoring modules
+ *
+ * Tests cover the simulation pipeline, fee recommendation output,
+ * structured logging, and clipboard interaction.
+ */
+
+// ── 1. Fee Simulation Tests ───────────────────────────────────────────────────
+//
+// Test fee_model.rs
+// ✓ compute_base_fee returns correct value for given ledger data
+// ✓ compute_base_fee handles empty / missing ledger entries
+// ✓ compute_base_fee clamps result to [MIN_FEE, MAX_FEE]
+//
+// Test network_load.rs
+// ✓ estimate_current_load returns 0.0 … 1.0 range
+// ✓ estimate_current_load handles no recent transactions gracefully
+// ✓ load multiplier correctly inflates base fee
+//
+// Test congestion_predictor.rs
+// ✓ predict_congestion returns enum variant for each load band
+// ✓ predict_congestion_short_term matches historical pattern
+// ✓ predict_congestion integrates with tracing span
+
+// ── 2. Fee Recommendation Output Tests ────────────────────────────────────────
+//
+// Test RecommendationPanel rendering
+// ✓ displays recommendedFee, lowFee, mediumFee, highFee
+// ✓ formats large numbers with toLocaleString separators
+//
+// Test clipboard interaction
+// ✓ click copies raw integer value (not formatted string)
+// ✓ shows "Copied!" label for 1.5 s after click
+// ✓ reverts to "Copy" after timeout
+// ✓ uses navigator.clipboard.writeText when available
+// ✓ falls back to document.execCommand("copy") when clipboard API unavailable
+// ✓ does not throw when clipboard write is rejected
+
+// ── 3. Structured Logging Tests ──────────────────────────────────────────────
+//
+// Test logging.rs
+// ✓ init_logger respects DEVKIT_LOG env var levels (trace/debug/info/warn/error)
+// ✓ JSON output format when stdout is not a TTY
+// ✓ human-readable format when stdout is a TTY
+// ✓ subscriber captures tracing events and spans
+// ✓ subscriber does not panic on malformed level strings
+
+// ── 4. Integration Tests ─────────────────────────────────────────────────────
+//
+// Test end-to-end fee recommendation
+// ✓ simulation → fee calculation → panel render → copy
+// ✓ structured log output contains expected fields
+// ✓ tracing spans are present in JSON log output
+
+// ── Test File Locations ───────────────────────────────────────────────────────
+//
+// packages/core/src/simulation/ fee_model.rs, network_load.rs, congestion_predictor.rs
+// packages/core/src/monitoring/ logging.rs
+// packages/ui/src/components/ RecommendationPanel.tsx
+// packages/ui/src/__tests__/ RecommendationPanel.test.tsx
diff --git a/typed-doc/structured-logging-module.rs b/typed-doc/structured-logging-module.rs
new file mode 100644
index 0000000..0b2a7fe
--- /dev/null
+++ b/typed-doc/structured-logging-module.rs
@@ -0,0 +1,83 @@
+/**
+ * Issue #339: Add structured logging module
+ *
+ * Implements a tracing-based structured logger for the devkit.
+ *
+ * Requirements:
+ * - Log level configurable via DEVKIT_LOG env var
+ * - JSON output for machine readability
+ * - Human-readable output for local dev (auto-detect TTY)
+ */
+
+// packages/devkit/src/monitoring/logging.rs
+use std::env;
+use tracing_subscriber::{fmt, prelude::*, EnvFilter};
+use tracing_subscriber::fmt::format::FmtSpan;
+
+/// Initialise the global tracing subscriber.
+///
+/// Behaviour:
+/// - Reads `DEVKIT_LOG` env var (default: "info")
+/// - Auto-detects TTY: human-readable in terminals, JSON elsewhere
+/// - Captures span open/close events for accurate timing
+pub fn init_logger() {
+ let log_level = env::var("DEVKIT_LOG").unwrap_or_else(|_| "info".into());
+ let is_tty = atty::is(atty::Stream::Stdout);
+
+ let env_filter = EnvFilter::try_new(&log_level)
+ .unwrap_or_else(|_| EnvFilter::new("info"));
+
+ let fmt_layer = if is_tty {
+ // Human-friendly output with ANSI colours
+ fmt::layer()
+ .pretty()
+ .with_line_number(true)
+ .with_target(true)
+ .with_span_events(FmtSpan::CLOSE)
+ .boxed()
+ } else {
+ // JSON output for machine consumption
+ fmt::layer()
+ .json()
+ .with_current_span(true)
+ .with_span_list(true)
+ .with_line_number(true)
+ .with_file(true)
+ .boxed()
+ };
+
+ tracing_subscriber::registry()
+ .with(env_filter)
+ .with(fmt_layer)
+ .init();
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tracing::info;
+
+ #[test]
+ fn test_default_level_is_info() {
+ // When DEVKIT_LOG is unset, info!() should produce output
+ // but debug!() should not
+ init_logger();
+ info!("this should appear");
+ }
+
+ #[test]
+ fn test_env_var_override() {
+ env::set_var("DEVKIT_LOG", "debug");
+ init_logger();
+ // Both info!() and debug!() should produce output now
+ }
+}
+
+// packages/devkit/src/monitoring/mod.rs
+pub mod logging;
+
+// packages/devkit/Cargo.toml additions:
+// [dependencies]
+// tracing = "0.1"
+// tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
+// atty = "0.2"