cd /news/generative-ai/show-hn-my-receipt-printer-prints-an… Β· home β€Ί topics β€Ί generative-ai β€Ί article
[ARTICLE Β· art-88185] src=github.com β†— pub= topic=generative-ai verified=true sentiment=↑ positive

Show HN: My receipt printer prints an original artwork every morning

A developer created MorningPrint, a system that uses a Raspberry Pi Zero W and an Epson thermal receipt printer to print a unique piece of character art every morning, designed by Anthropic's Claude AI. The art is themed to the day's weather, season, and news, and is rendered as ESC/POS bytes from a declarative spec, with the entire cloud backend running on a single Google Apps Script. The project, posted on Hacker News, showcases a creative application of generative AI and aims to replace phone feeds with a tangible daily artifact.

read10 min views1 publishedAug 5, 2026
Show HN: My receipt printer prints an original artwork every morning
Image: source

One original print, every morning.

A thermal receipt printer in my kitchen wakes up each day and prints a piece of character art that has never existed before, designed minutes earlier by Claude, themed to the day: the weather outside, the season, whatever the news feels like. A short verse underneath. One physical copy, 80mm wide, and that is the whole broadcast.

Two mornings, a day apart. Most days stand on their own; once in a while a piece answers the one before it, the way July 4 answered July 3 above.

This started as a bet against my phone. The first minutes of the day kept going to feeds, so I pointed the same inputs at a printer instead. It reads what the feeds read (forecasts, anniversaries, and the day's headlines when it goes looking for them) and hands back one finished thing you can hold. There is nothing to refresh; when the cutter drops, the day's edition is out. Yesterday's goes up on the corkboard.

The cloud half is a single Google Apps Script. The only hardware on my side is a Raspberry Pi Zero W running a small Python bridge (systemd plus an ngrok tunnel; runbook in docs/

) that pipes bytes into the printer's USB port.

graph LR
  subgraph cloud["Google Apps Script Β· daily trigger"]
    A(["daily brief"]) --> B["Claude designs<br/>an art spec"] --> C["render to<br/>ESC/POS"]
  end
  subgraph kitchen["the kitchen"]
    D["Pi Zero W"] --> E(["Epson prints<br/>and cuts"])
  end
  C -- "POST Β· ngrok Β·<br/>basic auth" --> D

Context.printDailyArt()

(src/art.ts

) builds a small brief: today's date, the season, current weather from the Google Weather API, and a rolling archive of the last 14 pieces (title + one-line style note each).Design. Claude gets a system prompt describing the medium (a 48-column monospace grid, 1-bit black, only CP437, the old IBM PC character set) and is asked for one committed idea that differs sharply from everything in the archive. On a day that genuinely earns it (a holiday after its eve, an event still unfolding, a resonant anniversary) it may instead answer an earlier piece; the pair at the top of this page is one of those. It can run a few web searches to feel out the day. Structured output (a JSON schema) constrains the reply to an art spec: an array of styled text ops plus a short verse.Render. A ~50-line renderer turns ops into raw ESC/POS bytes (ESC/POS is the byte-level command language receipt printers speak). No drivers, no images: the art is literally text with style commands.Print. The bytes are POSTed to the Pi, which writes them to the printer's character device. The receipt prints: a small date stamp, the artwork, the verse. Nothing else: no title, no branding.

The canvas: 203 dpi, one bit of color, 48 monospace columns, and a character set frozen in 1981. Everything the model can do, it does with CP437:

Trick How
Tone / gradients β–‘ β–’ β–“ β–ˆ ramps across rows
Sub-character edges half-blocks β–€ β–„ β–Œ ▐ β–  for silhouettes
Solid black fields inverted (white-on-black) runs; even spaces print solid
Seamless shapes line spacing set to exactly one glyph height, so rows tile
Giant type independent width/height scaling, 1–8Γ— each
Fine texture the printer's second font: 9Γ—17 glyphs, 64 columns
Structure full single/double box drawing, Β° Β· βˆ™ β‰ˆ ∞ Ο€ Ξ£ and friends

The "seamless shapes" row is the trick that makes block art possible at all: by default the printer leaves a white seam between text lines, but ESC 3

with the right value makes β–‘β–’β–“β–ˆ

rows fuse into continuous fields. The value was calibrated empirically with a built-in test page. That page, and every other byte this project sends, is documented in docs/escpos-protocol.md.

The model doesn't emit bytes; it emits a declarative spec the renderer executes. Abbreviated:

{
  "title": "GOLDEN RUN",
  "caption": "Test pattern: gradient, silhouette, invert, scale, texture.",
  "verse": "The mountains hold their breath;\nthe sun tries every shade of gray\nbefore committing to gold.",
  "style": "calibration plate; gradient + silhouette + type specimen",
  "continues": "",
  "ops": [
    { "text": "░░░░…\n▒▒▒▒…\n▓▓▓▓…", "gapless": true },
    { "text": " DAWN ", "width": 2, "height": 2, "bold": true, "invert": true },
    { "text": "every feature Β· one receipt", "font": "B", "align": "right" }
  ]
}

Only the artwork and the verse are printed. The rest are archive fields: the title and style land in a rolling ART_HISTORY

property that the next day's prompt receives as a list to differ from; the caption goes to the execution log only. That pressure keeps a daily generative loop from collapsing into the same sunset every morning, and the prompt pushes rotation across the whole space: landscapes, geometric abstraction, pattern studies, giant-type posters, constellation maps, weather glyphs, emblems, diagrams.

continues

is the deliberate exception. It stays empty almost every day, but when the day itself hands the model a thread (the Fourth of July arriving after its eve, an event still unfolding, a tenth anniversary), the model may build on one recent piece and record which. Those links show up as markers in the history it reads on later days, and a fresh marker raises the bar for the next one. That is what keeps continuations a surprise instead of a habit.

The renderer treats the spec as untrusted: sizes are clamped, rows are truncated to the column budget, control characters are stripped, output is capped at 150 rows (~45cm of paper), and anything CP437 can't print becomes a visible-but-harmless ?

.

Structured output guarantees the shape of the JSON, not the size of the strings inside it, and one run in July 2026 found that gap: valid JSON with a 3,155-character title of repeated filler. The archive saved it, every later prompt replayed it, and the model learned its own loop; within a week the receipt was a page of the model talking to itself instead of artwork. Specs are now checked against the length bounds the schema describes before anything is printed or written to the archive; a rejected spec throws to the alert email, and the next trigger retries. The archive is sanitized on read too, so an already poisoned entry is dropped once instead of fed back for fourteen days.

A few API notes, for the curious: the art spec comes back via structured output (output_config.format

with a JSON schema), and web search runs as a server-side tool, so the client just resumes on stop_reason: "_turn"

. The local harness enables the server-side fallback beta (another model re-serves a refused request); the production path does not, so a refusal there fails loudly to the alert email. Details in CLAUDE.md

.

Printer: Epson TM-T20III, an 80mm ESC/POS thermal printer with an auto-cutter. Any ESC/POS printer with a CP437 code page should work after recalibrating the column constants (node test-print.mjs ruler

).Bridge: Raspberry Pi Zero W running a ~40-line Pythonhttp.server

that writes request bodies straight to/dev/usb/lp0

, exposed through an ngrok static domain with basic auth, kept alive by systemd. The script itself lives on the Pi and is not committed here; its whole contract is "write each POST body to/dev/usb/lp0

", and the runbook covers the rest.

The full byte-level protocol (every command, the CP437 mapping, the geometry and calibration results) is specced in docs/escpos-protocol.md. The Pi's setup, maintenance, troubleshooting, and disaster recovery live in

.

docs/pi-print-server-runbook.md

You need an ESC/POS printer, something that can pipe bytes to its USB port (here, a Pi Zero W; see Hardware and the runbook), an ngrok static domain, an Anthropic API key, a Google account for Apps Script, and Node for the build.

TypeScript under src/

is the source of truth; esbuild bundles it to a single dist/main.gs

, which clasp pushes to Apps Script.

npm install        # dev tooling: clasp, typescript, esbuild, prettier
npm run build      # tsc --noEmit + esbuild bundle -> dist/main.gs
npm run status     # list the files clasp would push (dry check)
npm run push       # build, then clasp push (uploads dist/)

The Apps Script web editor is a mirror, not a second source of truth: edit locally and push. npm run pull

fetches remote back down if the editor was touched directly.

First-time setup on a new machine: npx clasp login

(writes ~/.clasprc.json

), then npm run push

. The committed .clasp.json

binds to my script; for your own deployment, run npx clasp create --type standalone --rootDir dist

(or put your own scriptId

in .clasp.json

) before the first push.

No secrets live in the repo. Runtime config is read from Script Properties (Apps Script editor β†’ Project Settings β†’ Script Properties):

Property Required What it is
PI_URL
yes ngrok HTTPS URL of the Pi print bridge
NGROK_USER
yes basic-auth username for the tunnel
NGROK_PASS
yes basic-auth password for the tunnel
ANTHROPIC_KEY
yes Anthropic API key (claude-fable-5 )
EMAIL_ALERTS_TO
no where failure alerts are emailed (rate-limited)
GEMINI_KEY
no Google API key for the Weather API
LAT / LON
no location for weather context

Weather is garnish, not a dependency: without GEMINI_KEY

/LAT

/LON

the art still prints, just uninformed about the sky.

State keys managed by the script itself (no setup): LAST_ART_DATE

(one art print per day), ART_HISTORY

(the rolling archive fed back into the prompt, with markers on pieces that continued an earlier one), and LAST_ALERT_TIME

(rate-limits alert emails).

Create one time-driven trigger on printDailyArt

(Apps Script editor β†’ Triggers). A morning hour works well. An hourly trigger is also safe: LAST_ART_DATE

limits it to one print per day, and because the guard is only set on success, extra runs double as retries after a failure.

testDailyArt()

prints the golden test plate (no API call) to verify the hardware path. Set DRY_RUN = true

in src/art.ts

to log specs instead of printing.

test-print.mjs

sends ESC/POS straight to the Pi bridge (the same endpoint Apps Script hits), so you can iterate without deploying or waiting on a trigger. The art

modes load the real builders from the built dist/main.gs

, so run npm run build

first; the preview then matches production exactly.

cp .env.example .env            # then fill in PI_URL / NGROK_USER / NGROK_PASS
npm run build                   # needed for modes that load dist/main.gs
node test-print.mjs hello       # minimal "SYSTEM ONLINE" connectivity test
node test-print.mjs text "Hi"   # arbitrary text
node test-print.mjs ruler       # column + gapless-spacing calibration page
node test-print.mjs art         # golden art spec through the real renderer (no API)
node test-print.mjs art:live    # LIVE end-to-end art (ANTHROPIC_KEY in .env)
node test-print.mjs art --dry   # print the hex payload instead of sending

This script is local only; it isn't bundled into dist/

or pushed to Apps Script.

Clone the repo, npm install

, and start Claude Code in the root; it picks up CLAUDE.md automatically, which carries the architecture, the deploy ritual, and the byte-level gotchas. You can iterate on the renderer with no hardware at all:

node test-print.mjs art --dry

shows the exact ESC/POS bytes a change produces. The longer version, including a good first prompt to hand Claude, is in .

CONTRIBUTING.md

src/
  appsscript.json   manifest (V8, America/Los_Angeles)
  escpos.ts         CMD command table, column constants, encodeCP437
  art.ts            the daily art job: prompt, schema, renderer, Anthropic client
  transport.ts      sendToPi (HTTP β†’ Pi bridge), retries, alert emails
  weather.ts        Google Weather API fetcher (context for the prompt)
  main.ts           entry points re-exported for the build footer
build.js            esbuild bundle β†’ dist/main.gs
test-print.mjs      local print harness (talks to the Pi directly)
docs/
  escpos-protocol.md         the full byte-level protocol spec
  pi-print-server-runbook.md the Pi side: setup, ops, troubleshooting
example/
  example_1.jpeg             the Liberty Bell piece, fresh off the printer
  example_2.jpeg             the Jul 3 -> Jul 4 sequence, pinned side by side

clasp

uploads only dist/

(.clasp.json

β†’ "rootDir": "dist"

), which is gitignored.

A personal project by Matt Horn, developed on personal time using personal equipment and personal accounts. Not affiliated with, sponsored by, or endorsed by Google, Anthropic, or any past or present employer. Views and content are my own. Copyright (c) 2026 Matt Horn. MIT-licensed; see LICENSE and NOTICE.

── more in #generative-ai 4 stories Β· sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/show-hn-my-receipt-p…] indexed:0 read:10min 2026-08-05 Β· β€”