Claude: written from 2026-10-05 as a starting point. The Ledger and the Android app work; Windows is not started.
An app and website blocker for Android and Windows where free time is earned, not requested. Finished tasks, workouts, and focused time reading or taking notes earn Vouchers into your Bank. Redeem one and social media and games open for a fixed Unlock; when it ends, everything locks again.
It exists because existing blockers let you switch them off whenever you like, protect themselves by blocking your own settings, and feel like punishment rather than reward.
- Ledger (runs on a home server): reads your Activity sources, credits Vouchers to the Bank, and signs an Unlock each time you Redeem one.
- Enforcers (one per device): block Distractions unless an Unlock is active. They can verify signatures but cannot create them, and they fail closed.
- Commitment rules: tightening is instant; loosening waits for the next morning. Nothing can be Redeemed during the nightly Curfew.
The vocabulary is defined in CONTEXT.md. Design decisions are in docs/adr.
| Step | What | State |
|---|---|---|
| 1 | Ledger, Todoist and ClickUp Activity sources, manual CLI Enforcer | Running on sprout against real accounts |
| 2 | Android app (Tauri v2 with Kotlin plugins, Device Owner): phone and tablet layouts, blocked-app screen, notification, tile, widgets | Working on an emulator; first real device next |
| 3 | Windows app (same Tauri project) and Enforcer service | Built: guard service, tray pop-up, blocked window, installer from CI; first real PC next |
| 4 | Focused time (usage stats) and exercise (Health Connect) Activity sources | Built; Focused time checked on an emulator |
| 5 | Tamper-hardening | Device Owner suspension, no force-stop or data clearing, fail-closed |
Voucher needs Android 13 or later, a Ledger reachable over Tailscale, and a computer with ADB for one step.
- Install the APK and open Voucher. Setup starts by itself.
- Connect to your Ledger. Enter its address (
http://<tailscale-ip>:8787). Voucher fetches the Ledger's public key; check that its first characters match the start of the Ledger'spublic.key. - App blocking (Device Owner). Android allows this only while no accounts and no other users exist, but nothing is erased. Freezing the apps that hold accounts hides the accounts without signing out; tested on an emulator, they come back unchanged when unfrozen.
- Turn on USB debugging. On Samsung, turn off Auto Blocker first. Don't remove the screen lock: that deletes wallet cards.
adb shell pm list usersmust list only user 0. Delete Secure Folder, Dual Messenger, and any work profile or private space first.adb shell dumpsys account | grep -E "Account \{|AuthenticatorDescription"lists the accounts and, for each account type, the app that holds it (Google accounts:com.google.android.gms; Samsung account:com.osp.app.signin).- Freeze each of those apps:
adb shell pm disable-user --user 0 <package>. - Reboot with them still frozen. Android only recounts accounts at boot or when one is added or removed, so without the reboot the next command is refused.
adb shell dpm set-device-owner io.github.comprehensivejason.voucher/.VoucherAdminReceiver- Unfreeze every app you froze:
adb shell pm enable <package>. If an app refuses to freeze, sign out of that account in Settings instead and sign back in after step 6. Signing out costs more: Google and Samsung wallets drop their payment cards, which then need re-adding and bank verification.
- Usage access and the blocked-app screen (an accessibility service) are switched on from Setup's buttons. Health Connect is optional, for the Workout source.
- Pick sources, blocklists, and starting limits. Until "Start Voucher", changes apply at once; after it, anything that loosens a rule waits for 06:00.
To leave, release the device in Rules, Protection. Like any Loosening it waits for 06:00; then Voucher lifts every block, gives up Device Owner, and can be uninstalled.
- Run the installer as an administrator (from the "Windows installer" workflow's artifacts). It installs Voucher, registers the Voucher guard service, and starts Voucher in the tray whenever anyone signs in.
- Open Voucher from the tray and connect to your Ledger, as on Android. The guard takes the same connection once; after that only an administrator can change it.
- For Focused time on the PC, run ActivityWatch. Voucher reads it locally every two minutes; Obsidian and Anki count by default.
The guard closes paused programs (Steam and other launchers, and anything in a game folder when "Every game" is on) and writes the site blocklist as browser policy for Chrome, Brave, Edge, and Firefox. Browsers that ignore policy, such as Opera and Vivaldi, are on the Other browsers list.
An administrator can stop any Windows service. If you use Windows as an administrator, stopping the guard can't be prevented, only seen: the Ledger logs it as a Gap. Using Windows from a standard account, with a separate administrator account for maintenance, makes the guard hold.
apps/
voucher/ the Voucher app (Tauri, for Android and Windows)
counterfoil/ Counterfoil, which reads the Ledger and never enforces (a browser app for now)
packages/
core/ what both apps share, as source: theme, Ledger calls, charts, the dashboard
crates/
voucher-protocol/ Unlock format and signature checking, shared by Ledger and Rust Enforcers
voucher-ledger/ the Ledger
voucher-cli/ manual Enforcer for step 1
voucher-guard/ the Windows Enforcer service
dev/ the test portal, palette and import checks, seeds
docs/adr/ decision records
The two apps and core are npm workspaces of the repository root (ADR 0010).
Needs a Rust toolchain (rustup) and, for the apps, Node 24.
cargo test
cargo build --release
npm ci # at the repository root: every workspace's packages
npm run check # svelte-check for core and both apps, and core's import check
npm run build
npm run dev:voucher # http://localhost:1420
npm run dev:counterfoil # http://localhost:1430In a plain browser both apps show sample data; ?state=, ?ledger=down, and ?fault= pick a situation (see packages/core/src/api.ts). To serve a dev build to a tablet on the network, set TAURI_DEV_HOST to this machine's address.
To have Counterfoil's dev build read a real Ledger instead, give its dev server the Ledger's address and access code; the server forwards /ledger/... with the code added, so the code never reaches the page, and it passes only the requests Counterfoil may make:
LEDGER_PROXY=http://<ledger-address>:8787 LEDGER_CODE=$(cat <ledger-data-dir>/access.code) npm run dev:counterfoilThe Ledger is one small program that keeps your Bank and signs Unlocks. Run it on an always-on machine (a home server, NAS, or Raspberry Pi) and reach it over Tailscale or your LAN. Don't expose it to the open internet.
Use deploy/compose.yaml (on TrueNAS SCALE: Apps, Discover Apps, Install via YAML), or:
docker run -d --name voucher-ledger --restart unless-stopped \
-p 8787:8787 -v voucher-data:/data -e TZ=America/Los_Angeles \
ghcr.io/comprehensive-jason/voucher-ledger:latest
docker logs voucher-ledger # shows the access code on first runThe image is built for x86-64 and ARM64.
Download voucher-ledger for your platform from the releases, or build it (cargo build --release -p voucher-ledger), and run it as a service (systemd, for example).
On first run the Ledger creates signing.key (readable only by its owner), public.key, and an access code in access.code, and prints the code. Every device asks for the Ledger's address and that code when it connects. signing.key never leaves the server.
In the app, Rules, Sources, Reconnect takes a Todoist or ClickUp API token and stores it on the Ledger.
| Variable | Meaning | Default |
|---|---|---|
VOUCHER_DATA_DIR |
Where the state, keys, access code, and tokens live | data (/data in the image) |
VOUCHER_LISTEN |
Address to listen on, such as the machine's Tailscale IP | 127.0.0.1:8787 (0.0.0.0:8787 in the image) |
VOUCHER_TIME_ZONE or TZ |
Time zone for a new Ledger's Days and Curfew | the machine's |
VOUCHER_ACCESS_CODE |
Use this access code instead of access.code |
created on first run |
VOUCHER_POLL_MINUTES |
How often to check Todoist and ClickUp | 5 |
VOUCHER_TODOIST_EXCLUDED_PROJECTS |
Comma-separated project IDs that never earn (Leisure) | none |
VOUCHER_TODOIST_TOKEN_FILE, VOUCHER_CLICKUP_TOKEN_FILE |
Where the API tokens are kept | todoist.token, clickup.token in the data folder |
VOUCHER_CLICKUP_TEAM_ID, VOUCHER_CLICKUP_USER_ID |
Which ClickUp Workspace and user | found from the token |
A Ledger set up before access codes existed keeps running without one; put a code in its access.code and reconnect each device to switch it on.
Every request carries the code as Authorization: Bearer <code> except GET /key. Endpoints include GET /status, POST /redeem?count=, POST /change (a Change, such as {"UnlockMinutes": 15}), GET /day?date=, GET /history?days=, POST /report, POST /check-in?device=, POST /sleep and POST /calendar (from devices), GET/POST /calendars (which calendars count, and the device that reports them), and POST /energy (deprecated: the app no longer asks, but older apps can still send answers). Each Day in /day and /history carries its sleep, calendar load, any old energy answer, and its planned time blocks: Todoist tasks with a due time and a duration, snapshotted as the Ledger sees them (no titles), each with how its minutes split between earning sources, Distraction or Unlocked, other, and unknown. Tightenings apply at once; Loosenings wait for the Morning boundary.
export VOUCHER_LEDGER=http://<ledger-address>:8787
export VOUCHER_PUBLIC_KEY=/path/to/public.key
voucher-cli status
voucher-cli redeem
voucher-cli set unlock-minutes 15 # also: bank-limit 20, curfew 22:30 06:00
voucher-cli check # the manual Enforcer: UNLOCKED or BLOCKEDcheck caches the last Unlock, so it keeps working when the Ledger is unreachable, until that Unlock runs out.
GPL-3.0. See LICENSE.


