← cd ..

Receipts from any cash register

A .NET plugin that sits next to retail POS systems it has never seen, captures every sale, keeps working offline on 32-bit Windows 7 tills, and feeds a receipt pipeline I moved into our own backend.

Role
Software Engineer · OKR owner
Company
hello again, Linz
Timeline
01/2026 – now
Stack
C# · WPF · Win32 · Python · Vue · GCP · Claude

Context

The problem

hello again is a loyalty platform. Shops already have a cash register, often from a vendor we have never worked with and sometimes decades old, and we may not change it.

The plugin runs next to it on the till. It reads each sale from whatever the POS gives off (a print job, a file, a barcode, a screen, a UI element), finds the loyalty customer, redeems rewards and sends the receipt to the backend.

The real screens are under NDA, so this page shows the design and the decisions instead.

Requirements

ConstraintWhat it means
HardwareWindows 7 to 11, 32-bit processes, 4 GB RAM, weak CPUs, often over remote desktop
POSKnown and unknown vendors: native Win32, Java, browser-based; often running elevated
AvailabilityNever block or crash the cashier's sale; every wait is bounded
NetworkFlaky; receipts survive offline periods and restarts, with no duplicates
PrivacyGDPR: diagnostics never record what is typed on the till

Architecture

From the till to the loyalty platform

Hover or tap any box for a plain-English explanation. Dashed boxes are systems I integrate with, not ones I built.

on the till (Windows) cloud sale stage sync queue observes daily data asks why a till went quiet Cash registerany vendor POS plugin.NET 6 · WPF · Win32 Diagnostics modeonboards unknown POS Transaction journaloffline-first · retries Receipt intakePython · Django · Celery Extractionrules first → LLM Loyalty platformreceipt → points Config builderVue · test before save Fleet dashboardCloud Run · IAP AI support agentClaude · read-only
tipHover or tap a box to see what it does and why it's there.

What I built

Three workstreams, two of them as OKR owner

C# · .NET 6 · WPF

The plugin on the till

  • Offline-first transaction journal with a background retry worker
  • Direct capture from the Windows print spooler, behind a kill switch
  • Elevated start through a Scheduled Task, so input reaches elevated POS software
  • Diagnostics mode for onboarding unknown POS systems
  • UI Automation for POS systems without receipt output, running at a client
  • Pay With Points, in-till registration (EN/DE/HR), reward rules
OKR owner · Q2 2026 · 100%

Receipt processing in-house

  • Wrote the RFC and owned the objective
  • Moved processing off a third-party workflow platform into our own backend
  • Async Python pipeline with an audited state machine
  • Vue config builder with test-gated saves and replay
  • Rolled out till by till, reversible at any time; completed
OKR owner · Q3 2026 · 85%+

Fleet visibility and support

  • Fleet dashboard built solo: versions, health, rollout waves
  • Reworked the "migrated" metric so progress reflects real clients
  • Statistical receipt-outage detector with alert roll-up
  • Production AI support agent with guardrails

Key decisions

The trade-offs that shaped it

Short architecture decision records: the situation, what I chose, and what it cost.

ADR-01Write every sale to disk before sending it

Context
Tills lose network for hours, and a receipt sent from memory is lost if the app restarts.
Decision
One atomic record per transaction with a state machine, retried by a background worker with exponential backoff, then a dead-letter state. Written as a decision record first: never replay a call whose result is already printed on the customer's paper receipt.
Trade-off
More disk I/O and more states to test; in exchange receipts survive offline periods and crashes without duplicates.

ADR-02Read print jobs through the spooler, not from its folder

Context
Copying spool files from disk races the spooler and can miss or truncate a receipt.
Decision
Read the job through the Win32 spooler API with bounded memory and a bounded wait, shipped behind a kill switch with the old path as fallback.
Trade-off
Two code paths to maintain during rollout, but a safe switch-over on live tills.

ADR-03Never redirect a child process's output without reading it

Context
Start-up and updates hung on loaded tills. The cause was a pipe deadlock around a child process.
Decision
No stream redirection, a hard timeout and a kill on expiry, applied everywhere the plugin starts a process.
Trade-off
Less output to log from those processes; no more hangs.

ADR-04Test before save, and move one till at a time

Context
Moving receipt processing in-house could break receipts for live merchants.
Decision
A config can only be saved after a dry run on real receipts with zero side effects. Each till's receipt endpoint comes from remote config, so merchants move one by one and can be switched back instantly.
Trade-off
A slower rollout than a big-bang switch, with no risky cut-over day.

ADR-05No agent framework next to production credentials

Context
Agent frameworks are quick to start with, but give the model a shell inside a container that holds production credentials.
Decision
A hand-written streaming tool loop on the Anthropic Messages API, with only read-only tools.
Trade-off
More code to own; in exchange a small, auditable surface with no channel out.

Failure modes

Bugs I traced to the root

Generic bug classes from a long-running Windows process on small machines.

SymptomRoot cause and fix
Process crash
→
A native callback ran after its handle was closedGuarded re-registration; nothing may escape a thread-pool callback.
CPU spinning at 100%
→
Waiting on a handle that is always signalledRe-register only after a successful reset.
Wrong image on a receipt
→
Per-job state held in a shared singletonState scoped to each job with AsyncLocal<T>.
Lost receipts
→
Transaction IDs reused or colliding in one millisecondA lock-free, strictly increasing ID generator.
Long cashier waits when offline
→
Default HTTP timeoutA short bounded wait, then the journal takes over.
Input freezing machine-wide
→
A blocking UI Automation call on the keyboard-hook threadMoved to a background thread, one lookup in flight at a time.

AI developments

A production AI agent, with guardrails

Finding out why a till stopped sending receipts used to take one person's laptop and memory. Now support asks a shared, audited agent from the dashboard. Every answer follows one report shape: verdict, timeline, code location, root cause, what was ruled out, how to verify.

RiskControl
Runaway cost
→
A dollar ceiling per requestPriced per token type and checked after every turn; plus a turn cap and bounded tool output.
Unauthorised use
→
Two gatesCloud Run IAM plus a constant-time shared secret; the identity token is verified to record which person asked.
Prompt injection
→
No channel outCallers cannot add system roles; logs are treated as untrusted data; no web, write or URL-fetch tools.
Leaking secrets
→
Removed at the sourceSecrets are stripped before the data reaches the agent, with redaction on every tool result as a backstop.
No accountability
→
Full audit trailA log line per request (question hashed, not stored) and an archive of every conversation, cancelled ones included.

AI in my daily work

  • Claude Code and GitHub Copilot for coding, tests and refactoring; I review every AI-generated change.
  • Rules-first, LLM-fallback extraction in the receipt pipeline.
  • A prototype AI reviewer for pull requests: label-triggered, budget-capped, read-only, checking threading, memory on small tills and compatibility.
  • Wrote the team's agentic coding rules.

Outcome

Where it landed

3×
quarters in a row as OKR owner
100%
Q2 objective: receipt processing in-house
85%+
Q3 objective: fleet visibility and support
4 GB
32-bit tills the plugin is built to run on
← Back to all work