Inspiration
A spirit-board planchette moves because several people push it a little, without meaning to. That is the ideomotor effect. Twitch chat is the same thing at a much larger scale: thousands of small pushes. We wanted an overlay where the crowd literally moves one object together, and where you can watch the tug of war as it happens.
What it does
- Planchette is an OBS browser source. It reads a channel's public chat anonymously: no Twitch account, OAuth token or backend. It is a static folder; serve it locally or from any static host and paste the URL into OBS.
- A message that is a single letter, a single digit, or YES / NO / GOODBYE is a vote. Each viewer has one vote, which pulls the wooden planchette toward that glyph. Every other message is a faint tremble, so a busy chat visibly shakes the wood but cannot spell anything.
- Damped spring physics make the planchette drift, overshoot and hesitate. When it rests on a glyph long enough, the glyph is spoken: it glows, a soft chime plays, and it joins the message from beyond. GOODBYE ends the séance.
- The broadcaster and mods can use
!seanceto start a new séance and!goodbyeto end the current one. - An offline demo crowd lets streamers try it without going live.
- Options: dark or parchment board, dwell time, pull strength, sound, and a transparent background for OBS.
How we built it (how Codex was used)
- Spec. We wrote a one-page spec (SPEC.md in the repo) covering anonymous IRC, the board layout, the physics, dwell, moderation and the look.
- First version. Codex (
codex exec, session01a18fa0-2d85-7653-9f7c-387757626619) built the whole first version from that spec: board rendering, physics, IRC client, demo crowd and settings. It checked its own work in a headless browser. - Second round. We tested the first version against real chat. We recorded chat anonymously from busy channels and replayed it through the physics. Two problems came out of that:
- Twitch rejected the login when every command went out in a single WebSocket frame.
- The centroid of ordinary English chat sits on "S", so a busy chat just spelled "SSSS".
We gave Codex those recordings and a brief. A fresh Codex session (01a19030-6bad-7d53-9c42-9baaf51edfbf) fixed the login, redesigned the input model into votes and drift, re-laid the board as two concentric arcs, and wrote test/replay.test.cjs, which replays the scrambled recordings. In the final build, a 903-message recording from a busy channel speaks only the few real chat-wide votes in it (O, then YES much later) and stays silent once those votes are removed; 16 synthetic voters added to the same noise spell "HI" in 8.85 seconds.
- Third round. An independent read-only Codex review (gpt-6-astra) checked round 2. One finding was real: votes aged on the animation clock, so a suspended OBS source could speak a stale vote. A third Codex session (
01a19060-ea10-75a1-8f2c-caa6a8cf6df1) moved expiry to wall time and added a test for a 10 s suspension. - Fourth round. Testing alone in our own channel showed that one viewer could not spell: a single vote faded before the planchette finished its dwell. A fourth Codex session (
01a19075-5260-7c31-9e71-4dc78ae11f2a) made spring stiffness depend on agreement (the winner's share of recent room activity) instead of headcount. One viewer now spells H, I and GOODBYE (simulated at 60 Hz: 2.6 s, 7.5 s, 13.0 s). - Quorum. A live check on a busy public channel then showed one
D:emote spelling D. We (Claude Code) made emoticons count as chatter and added a speaking quorum ofceil(√room / 2)voters, where room is the recently tracked chat activity (last 30 s). One viewer alone is still their own quorum. Replays: two stray votes in a busy room stay silent; when about 25 of ~300 chatters in a recorded busy channel typed YES, the board said YES. - Review. Claude Code wrote the briefs, reviewed and tested each round (including a live check on a busy public channel: 90 messages/min, 55 hands, nothing spelled), and drafted this write-up.
- Plain HTML, CSS and JavaScript. No build step and no dependencies. MIT licensed.
Challenges
- Collective input averages out. Many people pulling toward different letters cancel each other, and the average point is a letter nobody asked for. The final model treats only single-glyph messages as votes, one per viewer. The planchette heads for the plurality glyph and only switches when another glyph leads by 15%; the minority only makes it wobble. All other chat is a seeded tremble capped at 12% of one hand, so it can shake the wood but never spell.
- Twitch answers
CAP * NAKand never joins when the login commands share one WebSocket frame. We found it with a small probe that counted messages: 0 when batched, 28 in 10 s when sent one per frame. - Small rooms and big rooms want opposite things. Stiffness scaled by headcount made busy rooms safe but left a lone viewer unable to finish a letter. Scaling by agreement fixed both: a unanimous room of one is decisive, and a scattered room of hundreds stays loose.
What's next
- Bilibili live-comment support.
- Channel-point redemptions for a "strong hand".
- A replay of each séance as a clip.
Try it
- Live offline demo (no Twitch needed): https://aschenbath.github.io/planchette/?demo=1
- On your channel: add an OBS Browser Source with
https://aschenbath.github.io/planchette/?channel=your_channel&bg=0, 1920×1080. No login. - Code (MIT): https://github.com/Aschenbath/planchette
Codex session IDs
- 01a18fa0-2d85-7653-9f7c-387757626619 (first version)
- 01a19030-6bad-7d53-9c42-9baaf51edfbf (round 2)
- 01a19060-ea10-75a1-8f2c-caa6a8cf6df1 (round 3)
- 01a19075-5260-7c31-9e71-4dc78ae11f2a (round 4)
- 01a19055-42c8-77d2-96e6-75ec489deea9 (independent read-only review)
Built With
- canvas
- html5
- javascript
- obs
- openai-codex
- twitch
- twitch-irc
- web-audio
Log in or sign up for Devpost to join the conversation.