DEV Community

Mudit Palvadi
Mudit Palvadi

Posted on

I Touched Grass And All I Got Was This Absurd Receipt

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

I Touched Grass And All I Got Was This Absurd Receipt

Built for the Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass.

One button. Tap it on the way out, tap it on the way back. While the phone is in your pocket it counts your steps and notices when you stop moving. When you come back, a language model running entirely on the device prints a thermal receipt of your time outdoors.

The screen is the shortest part of the experience β€” you look at the phone for ten seconds going out and ten seconds coming back.


What is actually here

                GRASS TOUCHED
              RECEIPT NO. 1
------------------------------------------------
WENT OUTSIDE                      12:32 AM
COME BACK                         12:33 AM
TIME BAND                      AFTER DARK
WALK TYPE                          STEP OUT
------------------------------------------------
0x   STEPS, ON YOUR GOOD LEG
1x   MINUTES AWAY FROM THE SCREEN
1x   THE FRONT DOOR
1x   COLD AIR, BRIEFLY
1x   A CHANGE OF MIND
------------------------------------------------
TOTAL                        $0.00

WHAT THE GRID MISSED
LESS THAN A MINUTE OFF THE GRID.

YOUR REVIEW
2 stars. Did not get far.

SIGNOFF
Come back tomorrow. Grass misses you.
Enter fullscreen mode Exit fullscreen mode

That last receipt is from a real 20-second walk. It is not a screenshot from a design tool. It is what the app actually printed.


The most interesting thing that happened: Qwen3 would not stop thinking

The first model I tried was Qwen3 0.6B. The first receipt on a real device came back as a template receipt β€” the app's honest fallback β€” and the log named the reason:

ReceiptService: rejected (tooFewLines) on both attempts.
raw: <|channel>thought | Okay<channel|><|channel>thought | ,<channel|> ...
Enter fullscreen mode Exit fullscreen mode

Every token of its 512-token output budget went into thought blocks. The receipt itself never appeared.

I tried the obvious fix first. flutter_gemma's InferenceChat appends /no_think to the prompt for Qwen3 when isThinking: false β€” so I switched from the raw createSession() to createChat(), which is the only layer that knows about thinking models. That was necessary and not sufficient.

Grepping flutter_gemma_litertlm for enableThinking, no_think or extraContext returns nothing. The .litertlm engine never wires thinking off at all. On a real phone Qwen3 still thinks until it runs out of budget.

So the default is now SmolLM2 360M β€” ungated, not a thinking model, and fast enough to answer between two taps. Qwen3 stays selectable with a note saying what it does, because a receipt that names its model should be honest about which ones do not work.

This is also why the fallback exists and why the UI says "Template receipt printed: … Your steps and minutes are still the real ones" instead of hiding it. A silent failure here would have looked like a working app with bad jokes.


What the AI model is actually for

The honest answer, because it is the question a submission should be able to survive: the sensors measure, and the model judges.

The phone measures steps, minutes, time band and the longest standstill. It cannot decide what kind of walk those add up to, how many times something plausibly happened, or what the offline minutes meant for the natural world. Those are the model's job.

Four things come from the model and not from the phone:

  1. A verdict on the walk β€” WALKTYPE, one of a closed set: STEP OUT, ERRAND, A SIT, LOOP, A PROPER WALK, WANDER, ESCAPE, COMMUTE. It is the line a reader can disagree with, which is the point.
  2. Item quantities. The model writes 44x BIRD THAT SAW YOU, so a three-hour walk and a bathroom break do not produce receipts that look interchangeable.
  3. The nature line, which must quote the measured minutes.
  4. The comedy β€” item names, review, signoff.

Why the quantities are bounded, not trusted

The model chooses them, so its output cannot be fully verified: nobody knows how many birds there were. What can be checked is whether a number is possible, and that is the one rule enforced in parseOutput:

// Nothing happened more times than there were minutes to happen in.
final maxQty = 1 + facts.minutes;
Enter fullscreen mode Exit fullscreen mode

A ten-minute walk cannot claim twenty birds. A claim of 999 steps on a 43-minute walk is thrown out entirely.

ReceiptLine.measured records which lines are the phone's. The measured step and duration lines are built in Dart and prepended, so a model that writes its own 12x STEPS line prints next to the pedometer's real count without ever replacing it.

The nature line has its own check: it must contain the measured minutes as a whole number. A line that says "43 MINUTES" about a six-minute walk is decoration pretending to be fact. And a walk under a minute has no number to quote, so both the prompt and the check fall back to the phrase "LESS THAN A MINUTE" instead.


Why open

Every number above is true in the boring, checkable sense:

  • No server. There is no backend, no database, no account, no analytics.
  • No signal needed. The receipt is written on the device. It works on a trail with no bars.
  • Nothing leaves the phone. Location is never requested. Steps and timestamps live in a value object and are discarded when the process ends.
  • Nothing to pay. No API keys, no token metering, no inference bill.
  • Swappable. The prompt is model-agnostic and the model is a constant in gemma_runtime.dart. Three are wired up and selectable in the app, so "open" is a switch you can flip rather than a claim in a README.

That last one is the point worth making in the write-up. A closed-API version of this app would be a fetch and an API key. The open version is a 356 MB file, a prompt you own, and three models you can replace without asking anyone.


The printing

The receipt does not appear already printed. It feeds out of the slot in 14 discrete steps over 1.7 seconds, with a dark band tracking its leading edge while it is still inside the machine.

The steps are the part that matters. A single eased glide reads as a web page loading; the jumps read as a roller tick-tick-ticking. Each step reveals another ~1/14th of the paper, so the header prints first and the torn edge appears last.

The torn edge itself is a CustomClipper with 40 teeth at 4px depth, and the dashed rules are a CustomPainter that clamps its last dash instead of letting it overshoot. Both are unit tested without rendering anything, because a clipper that closes at the wrong place still renders as a grey shape and looks like paper.


What I would do differently

  • The walk ends if the process dies. Sensor streams are held in the app; there is no background service. Closing the app mid-walk loses the walk. A workmanager periodic sample would fix it and is the obvious next step.
  • Standing still is measured from gaps between step events, not a motion classifier. Ten seconds without a step counts as a stop, so a red light does not register but a bench does.
  • I would not have hardcoded an example number in the prompt. A 20-second walk printed a template receipt because the model copied "43 MINUTES" out of the example and then failed the check it was meant to demonstrate. The example now carries the walk's own numbers.
  • The receipt could be taller. A six-line receipt with the machine above it fills a phone screen, so the paper is sized to its content rather than allowed to scroll.

Code

Grass Receipts

lines; the idea is older than both projects Flutter, and the inventor is credited rather than ignored. The maths is three

Grass Receipts

I touched grass and all I got was this absurd receipt.

One button. Tap it on the way out, tap it on the way back. While the phone is in your pocket it counts your steps and notices when you stop moving. When you come back, a language model running entirely on the device prints a thermal receipt of your time outdoors:

                GRASS TOUCHED
              RECEIPT NO. 42
------------------------------------------------
WENT OUTSIDE                        4:04 PM
CAME BACK                           4:47 PM
TIME BAND                    LATE AFTERNOON
------------------------------------------------
1234x STEPS, ON YOUR GOOD LEG
43x   MINUTES AWAY FROM THE SCREEN
1x    BENCH OCCUPIED, BRIEFLY
3x    BIRD THAT SAW YOU
1x    BREEZE, MILD, SUSPICIOUS
------------------------------------------------
TOTAL                        $0.00

YOUR REVIEW
3 stars. Great breeze, no wifi.

SIGNOFF
Come back tomorrow. Grass misses you.

The…


Running it

flutter pub get
flutter run            # downloads SmolLM2 on first launch, automatically
flutter test           # 45 tests, no device or model needed
Enter fullscreen mode Exit fullscreen mode

First launch downloads 356 MB because the receipt writer has to live on the phone. Choosing Gemma adds a token field on the setup screen; it is held in memory for the length of one download and never written to disk.

The repo is at github.com/your-handle/grass-receipts. MIT licensed. The Gemma weights are downloaded at runtime under Google's Gemma Terms of Use.


Built for Hacktoberfest Open-Source AI Challenge: Week 1. Tag: #hf26challenge.

Top comments (0)