This is a submission for the Presage SmartSpectra SDK Challenge
| Signal | Live pulse rate (Presage SmartSpectra, on-device) |
| Platform | Electron + strict TypeScript · macOS (Apple Silicon) and Windows x64 |
| Chess | Lichess, embedded in the app, via its public API |
| Privacy | Camera only between games, video never leaves the machine, telemetry off, one-click delete |
| Code | github.com/CodewithJha/zwischenzug (MIT) |
What I Built
The problem: tilt
Online chess puts the next game one click away. After a painful loss, the impulsive "one more to win it back" is where losing streaks start.
An analysis of 11.3 million Lichess games with player fixed effects (romanbreyten/chess-tilt) found:
| After… | Effect |
|---|---|
| 1 loss | about 7 rating points below your usual level in the next game |
| 5 losses in a row | about 24 rating points below |
| a 30-minute break | the effect is gone |
| 5 straight losses (blitz) | 56% of next games start within a minute |
The author notes the data can't separate emotional tilt from simply having an off day. Neither Lichess nor chess.com has a built-in cooldown. Existing extensions count losses and block play for a fixed time, whether or not you've actually calmed down.
The idea
A zwischenzug is the chess term for an "in-between move": the move you play before the recapture everyone expects. Zwischenzug puts an in-between move after a loss: a pause that ends when your body has settled, not when a fixed timer runs out.
How a cooldown works
- Consent. Choose "Allow camera between games" or "Timer only". Nothing runs before this.
- One-minute check-in. The webcam reads your pulse; the median of the stable readings becomes your baseline.
- Play. Lichess runs inside the app. The camera is off while you play. Games are only allowed while the embedded Lichess session is signed in as the account you chose, so a second account or anonymous play inside the app doesn't get around the lock.
- Loss detected through the Lichess public API within seconds.
- Lock. The Lichess view is hidden, and seek, challenge and rematch requests are cancelled at the network layer.
- Unlock, whichever comes first:
| Path | Rule |
|---|---|
| Pulse | at or below baseline + max(5 bpm, 7%) for 15 s, after at least 60 s |
| Timer | no valid reading for 180 s (timer-only mode, no face, poor light, SDK error) |
| Override | type a confirmation sentence (logged as an override) |
| Cap | always unlocks 15 minutes after the loss |
- Escalation. After 5 losses in a row, it suggests a 30-minute break, the point at which the study saw the effect disappear.
Demo
One real session, start to finish:
- check-in (baseline 77.5 bpm from 59 readings)
- Lichess sign-in
- a real bullet loss
- lock, and live pulse against the baseline (79.4 bpm right after the loss)
- unlocked by pulse after 64 s
The face is blurred and sped-up parts are labelled on screen. The narration is AI-generated (Kokoro TTS); the footage and every number are real.
| Check-in: live pulse | Loss, then lock | Unlocked by pulse |
|---|---|---|
![]() |
![]() |
![]() |
Code
CodewithJha
/
zwischenzug
Privacy-first chess tilt cooldown: after a Lichess loss, your next game unlocks when your pulse is back near baseline.
Zwischenzug
After a Lichess loss, your next game unlocks when your pulse is back near your baseline.
A privacy-first tilt cooldown for online chess: a desktop app for macOS (and Windows) built with Electron TypeScript, the Presage SmartSpectra SDK and the Lichess open API.
You play Lichess inside Zwischenzug. When you lose, new games are locked. With your consent the app reads your pulse from the webcam, on your own computer, and unlocks once it has stayed near your personal baseline. No usable camera signal? A timer unlocks instead. You can always skip a cooldown by typing a confirmation sentence.
The camera is never on while you play: only during a one-minute check-in and on the cooldown screen after a loss.
Zwischenzug is a habit tool, not a medical device. It does not diagnose, treat or monitor any condition, and it does not claim to detect "tilt". It only…
Try it: prebuilt installers are on the v0.1.0 release:
| Platform | File | Status |
|---|---|---|
| macOS (Apple Silicon) | Zwischenzug-0.1.0-arm64.dmg |
Main platform, tested with a real webcam and real games |
| Windows 10/11 (x64) | Zwischenzug-0.1.0-win-x64-setup.exe |
Built and launch-checked on GitHub's Windows runner (the app starts and the SDK loads); camera use on Windows not yet tested |
Both are unsigned (a solo hackathon project with no paid certificates); the README explains how to open them past Gatekeeper and SmartScreen.
For judges:
- You need a free Lichess account and a free Presage API key. There are no shared credentials: the key is tied to a personal free trial, and the Lichess login happens on lichess.org inside the app.
- Live Presage signal in about a minute: do the one-minute check-in, then open Settings › Advanced › Pulse diagnostics for the raw readings.
- Without a key: choose "Timer only".
Full steps are in the README's "How judges can test" section.
Engineering:
- Electron with context isolation, sandbox and a strict CSP. The Lichess page runs in a separate, permission-less view with no preload.
- Hardened Electron fuses: no
ELECTRON_RUN_AS_NODE, no--inspect, asar integrity on. - The pure logic (engine, parsing, persistence) is in
src/core, with 189 unit tests. - CI runs on macOS, and a separate workflow builds and checks the Windows installer.
How It Responds to Human Signals
Signal used: pulse rate, live. The pulse decides when you can play again; it isn't a decorative readout.
SDK integration
-
Setup:
@smartspectra/node-sdk3.4, integrated following Presage's Electron quickstart. - Where it runs: the native runtime loads in the Electron main process and is bound to the sandboxed app page through a narrow preload bridge. The app page captures the camera frames.
-
What it asks for: Zwischenzug requests only
PULSE_RATEand uses the SDK's stability and validation signals:- unstable readings never settle a cooldown;
- a reading older than 5 s counts as no reading;
- values outside 30–220 bpm are ignored at the boundary to the main process.
- Calibration: the median of stable readings over 60 s, with at least 10 needed.
- Hints: SDK hints such as "too dark" or "no face in view" are shown to the user and switch the screen to the timer path. After a retryable failure, the measurement restarts (up to 5 times, 15 s apart), so the pulse path resumes when you're back in view.
The unlock rule, in code
The band is relative to your baseline, not a population number:
// src/core/cooldown.ts
export function upperBand(baselineBpm: number, rules: CooldownRules): number {
return baselineBpm + Math.max(rules.toleranceBpm, (baselineBpm * rules.tolerancePct) / 100);
}
With a 77.5 bpm baseline, the band tops out at about 83 bpm. The next game unlocks after the pulse has stayed at or below that for 15 s, once at least 60 s have passed since the loss.
What the screen shows, in real time
- A big live bpm number, and a gauge with your baseline band and your pulse at the loss.
- A progress bar that fills while you stay in the band (15 s).
- The active path, always spelled out: "Live pulse" or "Timer", with the reason.
- A calm breathing pace (clearly labelled as a guide, not a measurement) and a quiet Skip cooldown.
Data flow
What I verified for real
With a real webcam, a real Presage key and real Lichess games (Nov 7–9):
- three check-ins (baselines 80.7, 77.3 and 77.5 bpm)
- three real losses unlocked by pulse
- the timer fallback (camera covered, "too dark")
- the typed override
- restarting the app mid-cooldown (the lock and its start time were kept)
- sign-in and sign-out gating
Automated: 189 unit tests.
One honest finding: in my casual bullet games, my pulse right after a loss stayed inside my band, so the 60 s minimum decided the unlock rather than the pulse. Longer sessions and loss streaks are where the pulse condition should matter most, and that's the next thing I want to measure.
Prize Categories
Best Thoughtful Real-World Use, and Most Creative or Delightful Use.
Best Thoughtful Real-World Use
- The need: tilt-driven requeueing in online chess, measured across 11.3 million games.
- Who it's for: Lichess players who know they spiral after losses and want a pause that adapts to them instead of a fixed penalty.
-
Usability:
- consent comes first;
- a timer fallback and a typed escape hatch, so you're never trapped by a bad camera reading;
- works with no key ("Timer only");
- plain status text explains why it's waiting.
- Responsible data handling: everything stays on your machine (details below).
Most Creative or Delightful Use: a chess clock for your nervous system. The camera doesn't watch your game; it waits with you after it, and lets you back in when you're ready.
Consent and Data Handling
- Camera consent comes first. Nothing uses the camera until you choose "Allow camera between games". "Timer only" never touches it.
- The camera is on only between games: during the one-minute check-in, on the cooldown screen after a loss, and on the diagnostics screen if you start a measurement. Never while you play. Only the app's own page can request it; the embedded Lichess page gets no device permissions.
- On-device processing. The SmartSpectra SDK estimates pulse on your computer. No video or frames are uploaded or saved.
- What reaches Presage: its servers are contacted only to validate the API key and record usage, never with video.
-
SDK telemetry is turned off (
enableTelemetry: false). Per Presage's Telemetry & Privacy doc, the SDK then starts no telemetry session and transmits nothing. -
Stored locally only:
- consent choice
- Lichess username
- baseline pulse
- a game log: result, losses in a row, baseline, first pulse after the loss, time to unlock, unlock reason
Individual pulse readings are not stored. Diagnostic logs contain app events (for example game ids and outcomes), never pulse values or keys.
-
No sharing. Nothing is sent to me or anyone else. The only network traffic is lichess.org and Presage's key and usage checks:
- lichess.org means the site, its public API, and a sign-in check using the embedded session's own cookies;
- the app never sees your Lichess password.
-
Retention and deletion: data is kept until you delete it; nothing expires on its own. Delete all my data removes:
- the game log, baseline, username and consent
- backups and damaged copies of those files
- the saved Presage key
- the log files
- the embedded Lichess session
It keeps only your own optional config file. Export (CSV/JSON) happens only when you ask, to a file you choose.
- No AI services are used by the app.
- Not a medical device. Zwischenzug does not diagnose, treat or monitor any condition and does not claim to detect "tilt"; it only compares a pulse estimate with your own baseline. There's no covert monitoring: the camera only runs with consent, between games.
Thanks to Presage for the SmartSpectra SDK, Lichess for its open API, and romanbreyten/chess-tilt for the research behind the time floors.
If this is useful to you, a ❤️ or a comment helps, and I'd love feedback from chess players on the thresholds.







Top comments (0)