diff --git a/docs/ggwave-gotchas.md b/docs/ggwave-gotchas.md
new file mode 100644
index 0000000..201f075
--- /dev/null
+++ b/docs/ggwave-gotchas.md
@@ -0,0 +1,29 @@
+# ggwave Gotchas
+
+## WASM
+
+- **WASM heap copies**: `encode()` returns Int8Array backed by WASM heap memory. Must copy data out (`new Uint8Array(rawBytes.length); copy.set(...)`) before using, or it gets corrupted.
+- **Same instance required**: Using separate ggwave instances for encode/decode causes WASM heap pointer corruption. Use one instance for both. Max 4 instances allowed.
+- **Encode output format**: `encode()` returns Int8Array of raw F32 *bytes*, not Float32Array. Reinterpret with `new Float32Array(bytesCopy.buffer)` for AudioBuffer.
+- **API naming**: README says `TxProtocolId` but actual API uses `ProtocolId`. Check `Object.keys(ggwave)` when in doubt.
+
+## iOS Safari Audio
+
+- **AudioContext must be created synchronously** inside a user gesture handler (click/tap). Any `await` before `new AudioContext()` breaks the gesture chain and Safari blocks audio permanently for that context.
+- **Silent mode bypass**: `navigator.audioSession.type = 'playback'` (iOS 17+) switches WebAudio from ringer channel to media channel, bypassing the hardware mute switch. Without this, the silent switch kills all WebAudio output.
+- **Unlock pattern**: Create AudioContext → play a silent buffer → then await async init. Never reverse this order.
+
+## macOS Microphone (sox + CoreAudio)
+
+- **Explicit device names produce all-zero data**: `sox -t coreaudio "MacBook Air Microphone"` gets zeros due to macOS TCC permission scoping. Only `sox -d` (default device) gets actual mic access granted to the terminal app.
+- **Workaround**: Change the default input device in System Settings > Sound > Input, then use `sox -d`. Or install `switchaudio-osx` (`brew install switchaudio-osx`) to change it programmatically.
+- **MacBook Air mic disabled when lid is closed**: If using a monitor, the laptop mic won't work. Use the monitor's mic (e.g., Studio Display) instead.
+- **Sample rate must match**: ggwave needs matching sample rates for encode/decode. Browser uses 48000Hz. Server must also use 48000Hz — sox will resample from the device's native rate automatically.
+
+## SSE with Bun
+
+- Bun's default idle timeout is 10 seconds, which kills SSE connections. Set `idleTimeout: 255` (max value) on `Bun.serve()`.
+
+## Half-Duplex Audio
+
+- Both sides use the same audible protocol (GGWAVE_PROTOCOL_AUDIBLE_FAST). A `playing` flag stops mic processing during playback to prevent self-hearing/feedback. 300ms gap after playback before resuming listening.
diff --git a/tmp/bun.lock b/tmp/bun.lock
new file mode 100644
index 0000000..c106d99
--- /dev/null
+++ b/tmp/bun.lock
@@ -0,0 +1,15 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "ggwave-poc",
+ "dependencies": {
+ "ggwave": "0.4.0",
+ },
+ },
+ },
+ "packages": {
+ "ggwave": ["ggwave@0.4.0", "", {}, "sha512-+sKq0aIEVJ7zHj4Vw+Sj/RPa91xp76ihaG5gsOKZ8ojM5+uUu3NFzAspozwBx/zeaThxP5VeIkA2bbsfWpUd2g=="],
+ }
+}
diff --git a/tmp/index.html b/tmp/index.html
new file mode 100644
index 0000000..e3864cb
--- /dev/null
+++ b/tmp/index.html
@@ -0,0 +1,146 @@
+
+
+