Project · TokenTV riazrahaman.comHome → SourceCode →

A $5 clock that
never gets
reflashed.

TokenTV puts live AI usage — and a share watchlist — on a tiny Wi-Fi desk clock, without replacing a single byte of its firmware. The trick is that the clock never knows. It thinks it is showing a holiday photo. 🕰️

What this page covers → the design, the code flow, the hardware, and exactly what was built on top of someone else's good idea.
Hardware → $5 Wi-Fi Desk Clock (AliExpress)
Base → forked from click6067-ship-it/token-tv (MIT)
Edition → October 2026 · v0.4.3
Visits → counting...
Prologue

The clock thinks it is a photo frame 🖼️

These clocks ship with vendor firmware, no source, and no warranty worth the name. Replacing it means opening the case and soldering. So TokenTV doesn't. It uses a feature the stock firmware already has: a photo album with an HTTP upload endpoint, and a theme that shows a photo full-screen.

🖥️
A computer that stays on

Reads and draws

Reads your usage through each provider's own login, then draws a 240×240 picture with Pillow.

📤
Plain HTTP

Uploads a photo

Posts the image to the clock's album and selects the photo theme. No firmware, no agent, no SDK.

🕰️
The clock

Displays a picture

It has no idea what it is showing. That is the whole point: nothing on the device can break.

Why this constraint is the design. Everything awkward in the project descends from one fact: the only way to change a single pixel is to upload a whole image and have the device store it in flash. That is why there is a write budget, why countdowns are rounded, and why identical frames are never re-sent.
Chapter 01

What is actually inside 🔌

Identified by disassembling the vendor firmware and cross-checking against open-source ports for the same board. The USB-C port is power only — there is no serial chip — which is why the device is driven entirely over Wi-Fi.

THE VISION · CONCEPT TEASER

AI Product & Form Concept

The 3D generative vision for TokenTV as a dedicated desk appliance: retro TV casing, neon underglow, speed gauges, and token progress meters.

🎧 Click Unmute for voiceover & sound (10s)
THE REALITY · LIVE ON DESK

Real 240×240 IPS Hardware

The animated hybrid face running on the physical $5 white clock: pixel mascot strolling the divider rule, starfield twinkle, and live AI quotas + market changes with stock firmware.

Get the $5 Clock on AliExpress →
PartWhat it is
DeviceAliExpress mini weather clock / electronic photo album (1.54″ retro TV chassis)
MCUESP8266, ESP-12F class, running the Arduino ESP8266 core on the NONOS SDK — not an ESP32
Flash4 MB GD25Q32C, brought up in QIO mode
Panel240×240 IPS, ST7789 family, SPI mode 3 at 40 MHz, BGR order, inversion on
PinsSCLK 14, MOSI 13, DC 0, RST 2, backlight 5 (active-low), CS unused
FilesystemLittleFS, ~2.98 MB, holding photos, fonts and the web pages
SettingsEmulated EEPROM — a single 4 KB sector with no wear levelling
InputNone. No buttons, no touch. It is a display.
DiscoveryNone. No mDNS, no MQTT, no WebSocket — you read its address off a 1.5″ screen
The RAM wall. A full 240×240 16-bit frame buffer is 115,200 bytes. The ESP8266 has roughly 40–50 KB of usable heap. No firmware for this board — vendor or open-source — can buffer a whole frame, so JPEGs are decoded in small tiles and pushed straight at the panel. It is also why rendering on a real computer and shipping one finished image is the right division of labour.
FIRMWARE ARCHITECTURE

The Two Factory Channels & The Hidden GIF Slot

Disassembling and probing the vendor firmware revealed that the clock's factory brain is split into two mutually exclusive channels:

The Two Factory Channels: Photo Mode (TokenTV) vs Clock Mode (Firmware)
CHANNEL 1 · PHOTO MODE

Full 240×240 TokenTV Canvas

What TokenTV drives: the host computer renders full-screen 240×240 frames or animated GIFs over HTTP. The clock displays it 24/7 with zero local computation.

• 240×240 Full Screen • Host-Side Compositing • Zero Clock Lag
CHANNEL 2 · CLOCK MODE

Built-in Time, Weather & 80×80 GIF

The vendor mode: syncs NTP clock and OpenWeatherMap weather, playing a tiny 80×80 animated GIF sticker (the factory floating spaceman) in the lower corner.

• Stock NTP & OpenWeather • Undocumented 80×80 Slot • Confined to ~11% Screen
💡 The Discovery — The Hidden GIF Slot: The factory astronaut in Channel 2 is not hardcoded in ROM; the firmware exposes an undocumented internal upload slot where the GIF can be replaced with custom animated 80×80 stickers (like an Antigravity sparkle or mascot).
🧱 The Catch — The ~40 KB RAM Wall: Because the ESP8266 has only ~40–50 KB of heap (less than a single JPEG frame), the firmware lacks the memory to composite layers. It cannot overlay the animated sticker onto Channel 1's canvas. It is strictly Channel 1 OR Channel 2.
🚀 The Solution — Host-Side Compositing: TokenTV solves this by handling 100% of the layer compositing in Python on your computer: rendering walking pixel mascots, twinkling starfields, and live telemetry into a single pre-rendered animated GIF stream pushed straight to Channel 1.
Chapter 02

From an API call to a picture 🔀

Python 3.10+ with Pillow as the only runtime dependency. No database, no queue, no container, no cloud. Persistent state is a handful of JSON files and the last image uploaded.

HOST COMPUTER · PYTHON + PILLOW Anthropic Ollama Antigravity Finnhub / Yahoo sources.py state.py render device.py dashboard state files feeder snapshot Clock HTTP LAN only
One process owns the clock. The watchlist runner is a feeder: it writes a snapshot file and never touches the device, which is what stops two publishers fighting over the same photo.

↔ scroll the diagram sideways

One clock, one owner. A second publisher pointed at the same clock fights over the filename and over which saved backup is authoritative. So the watchlist runner drops its device address entirely and becomes a pure feeder, writing only a snapshot file. The usage runner reads that file and stays the single process that backs up, uploads and re-asserts the display. The conflict is removed by construction, not by coordination.
Follow-the-Sun markets. Financial markets don't share hours. The feeder continuously matches UTC timestamps against world trading sessions: routing to zero-config Yahoo Finance for Indian equities (09:15–15:30 IST) when Asian markets open, and switching over to Finnhub for US equities (09:30–16:00 ET) when New York wakes up. During off-hours, the ticker gracefully folds, returning 100% of the display to AI usage.
Unknown is never zero. A failed read keeps the previous values and marks the row stale rather than reporting 0%. A missing reset time renders as a dash. An identity mismatch blanks the numbers instead of showing another account's. A wrong number is worse than no number on a display you glance at.
LayerTechnologyRole & Rationale
Host RuntimePython 3.10+Native execution alongside official CLI tools; zero build or container footprint.
ImagingPillow (PIL)Sole dependency; draws 240×240 pixel glyphs, adaptive font scales, gauges, and animated GIFs.
Market DataFinnhub & Yahoo FinanceFinnhub for US equities (Keychain key); zero-config v8 chart API for Indian NSE/BSE equities.
Networkingurllib.requestDependency-free standard library HTTP client for upstream REST polling and LAN uploads.
Process SupervisormacOS launchdAlways-on background LaunchAgents (com.tokentv.clock and com.tokentv.stocks) with auto-restart.
StorageAtomic JSON & LittleFSPOSIX atomic snapshot files on host; wear-levelled copy-on-write LittleFS on clock.
Chapter 03

Eight faces, one snapshot 🎨

Every face is a Python function that receives the same normalised reading and draws a 240×240 image. Swapping face is a dropdown, not a redeploy. All renders below are from the project's sample data.

The combo face: three AI usage rows with gauges above four share rows with prices and day changes
combo

Both datasets on one screen with follow-the-sun global market routing. Three AI rows with usage gauges and the window each percentage is of, then four share rows. When Indian markets open (09:15–15:30 IST), it displays NSE large caps with dynamic INR font scaling; when US markets open (09:30–16:00 ET), it switches to US equities with an active exchange badge (NSE or US). Outside market sessions, it hands the entire canvas back to the full-size AI usage face.

The pixel face
pixel

Hand-drawn 5×7 glyphs, no font files

The digital face
digital

Seven-segment, with a clock for short windows

The neon face
neon

Glow and rings

The retro face
retro

Pixel scenes per provider

The hud face
hud

Instrument panel

The gameboy face
gameboy

Four shades of green

There is a ninth, space, which returns an animated GIF rather than a JPEG — so the publisher picks the filename from the image's magic bytes rather than from the style name.

Chapter 04

The part that was actually hard 💾

The clock keeps every frame in flash, and flash wears out. The obvious guard — skip the upload when the bytes are identical — turns out not to be enough, and the reason is worth stating because it is easy to get wrong.

The dedupe never fires when it matters. When a short usage window leads, the face shows a countdown in minutes. That countdown changes on every poll, so every frame differs and the byte comparison never matches. Measured at the default five-minute poll: twelve distinct frames an hour, every hour you are working.
  1. Byte dedupe — an identical frame is never rewritten.
  2. Countdown bucketing — the countdown is rounded up to whole buckets, so the face changes when the remaining time crosses an edge rather than every minute. A steady hour drops from twelve writes to four.
  3. A spaced write budget — uploads are spaced rather than given a quota. A quota lets a busy hour spend its whole allowance in the first twenty-five minutes and then freeze the display on a stale frame, which is the opposite of useful.
  4. A data-change override — the readings are fingerprinted separately from the countdown, so a percentage or price that actually moved bypasses the budget and goes out at once. A budget that defers real news is worse than no budget.
poll render frame upload skip defer bytes same? data changed? budget allows? no yes no yes yes no
The two red paths both end in an upload. A reading that genuinely moved never waits for the budget — the cap exists to absorb idle churn, not to withhold new numbers.

↔ scroll the diagram sideways

A watchdog, because once was not enough. The call that selects the photo theme used to run only when the output filename changed — that is, once. A clock that reboots, or that someone opens in its own web UI, silently reverts to its own theme while uploads keep succeeding into a photo nobody is looking at. It is now re-asserted periodically.
A quality ladder, because the limit is hard. The firmware rejects an oversized upload outright. A busy face can cross that limit on its worst frame and simply stop updating with no visible error, so the encoder steps quality down, then chroma, until the frame fits, and records what it chose.

Two flash regions, and only one of them forgives you

Disassembling the firmware settled a question that had been shaping the design. The photo album is LittleFS — copy-on-write and wear-levelled, so a rewritten file lands on fresh blocks. That moved the wear estimate from months to the order of a decade, and demoted the write budget from a safety mechanism to good hygiene.

The settings are a different story. Everything written through the settings endpoint — brightness, the night window, the accent colours — lands in the ESP8266's emulated EEPROM: one 4 KB sector, rewritten whole on every commit, no wear levelling, holding the Wi-Fi credentials beside the display settings. That asymmetry kills an idea that looks attractive from the outside: using the accent colours as a cheap status channel, since they need no image upload. They are not cheap. They are the most fragile flash on the device.

Chapter 05

What this was built from 🧬

TokenTV is a fork, and the original idea is not mine. This section is the honest ledger of which side of the line each piece came from.

Upstream · MIT © 2026 click6067-ship-it

🌱 The original

  • The core insight — drive a stock-firmware clock by uploading pictures to its photo album
  • The device layer for both clock API profiles, and the backup/restore flow
  • Five clock faces and the web dashboard
  • The Apple Watch endpoint
  • The published demo and the project's Hackaday write-up
  • SmallTV-Ultra support contributed by Ye Thi Ha Htwe
Research prior art

📚 Standing on

Chapter 06

What I contributed 🔨

Twenty-three commits over three days, on top of the fork point. Two new providers, a second data domain, the flash-write discipline, and the hardware study that corrected the assumption underneath it all.

23commits, excluding merges
+2,385lines of Python, including tests
143tests passing
2providers replaced
How this was built. Built in collaboration with multiple autonomous AI agents — Claude, Antigravity, and OpenCode — which is part of the point: the project is as much an exercise in orchestrating specialized agents and multi-agent pair programming as it is a desk clock. The firmware disassembly, the flash-wear measurements, the animated faces, and the design arguments came out of that collaborative loop; the orchestration, physical clock testing, and engineering decisions are mine.
Chapter 07

What would come next 🔭

Proposed · not built

The limitation worth attacking is not wear. It is the contract: the only way to change a pixel is to store a whole image. A custom firmware could change the device from a picture frame you rewrite into a display you draw on — accepting a frame over HTTP, decoding it straight to the panel in tiles, and never touching the filesystem at all. The write budget would stop existing, and every existing face would keep working, because the rendering stays on the host.

It is a weekend's work and a £5 serial adapter, plus a full flash backup first, because the vendor image restores the firmware but not the photos. Whether that is worth opening the case is a separate question from whether it would work.